pyedb 0.57.0__py3-none-any.whl → 0.59.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyedb might be problematic. Click here for more details.

Files changed (46) hide show
  1. pyedb/__init__.py +1 -1
  2. pyedb/configuration/cfg_pin_groups.py +2 -0
  3. pyedb/dotnet/database/cell/hierarchy/component.py +2 -8
  4. pyedb/dotnet/database/cell/layout.py +1 -1
  5. pyedb/dotnet/database/components.py +38 -42
  6. pyedb/dotnet/database/edb_data/control_file.py +13 -5
  7. pyedb/dotnet/database/edb_data/padstacks_data.py +34 -12
  8. pyedb/dotnet/database/edb_data/sources.py +21 -2
  9. pyedb/dotnet/database/general.py +4 -8
  10. pyedb/dotnet/database/layout_validation.py +8 -0
  11. pyedb/dotnet/database/sim_setup_data/data/settings.py +2 -2
  12. pyedb/dotnet/database/sim_setup_data/io/siwave.py +53 -0
  13. pyedb/dotnet/database/stackup.py +5 -32
  14. pyedb/dotnet/database/utilities/hfss_simulation_setup.py +81 -0
  15. pyedb/dotnet/database/utilities/siwave_simulation_setup.py +259 -11
  16. pyedb/dotnet/edb.py +26 -13
  17. pyedb/extensions/create_cell_array.py +48 -44
  18. pyedb/generic/general_methods.py +24 -36
  19. pyedb/generic/plot.py +8 -23
  20. pyedb/generic/process.py +78 -10
  21. pyedb/grpc/database/components.py +7 -5
  22. pyedb/grpc/database/control_file.py +13 -5
  23. pyedb/grpc/database/definition/padstack_def.py +10 -5
  24. pyedb/grpc/database/hierarchy/component.py +2 -9
  25. pyedb/grpc/database/modeler.py +28 -8
  26. pyedb/grpc/database/padstacks.py +62 -103
  27. pyedb/grpc/database/primitive/padstack_instance.py +41 -12
  28. pyedb/grpc/database/primitive/path.py +13 -13
  29. pyedb/grpc/database/simulation_setup/hfss_simulation_setup.py +79 -0
  30. pyedb/grpc/database/source_excitations.py +7 -7
  31. pyedb/grpc/database/stackup.py +5 -33
  32. pyedb/grpc/database/terminal/padstack_instance_terminal.py +9 -11
  33. pyedb/grpc/database/terminal/point_terminal.py +30 -0
  34. pyedb/grpc/database/terminal/terminal.py +16 -2
  35. pyedb/grpc/database/utility/xml_control_file.py +13 -5
  36. pyedb/grpc/edb.py +46 -20
  37. pyedb/grpc/edb_init.py +7 -19
  38. pyedb/misc/aedtlib_personalib_install.py +2 -2
  39. pyedb/misc/downloads.py +18 -3
  40. pyedb/misc/siw_feature_config/emc_rule_checker_settings.py +2 -1
  41. pyedb/misc/siw_feature_config/xtalk_scan/scan_config.py +0 -1
  42. pyedb/workflows/sipi/hfss_auto_configuration.py +711 -0
  43. {pyedb-0.57.0.dist-info → pyedb-0.59.0.dist-info}/METADATA +4 -5
  44. {pyedb-0.57.0.dist-info → pyedb-0.59.0.dist-info}/RECORD +46 -45
  45. {pyedb-0.57.0.dist-info → pyedb-0.59.0.dist-info}/WHEEL +0 -0
  46. {pyedb-0.57.0.dist-info → pyedb-0.59.0.dist-info}/licenses/LICENSE +0 -0
@@ -50,13 +50,9 @@ is_linux = os.name == "posix"
50
50
  is_windows = not is_linux
51
51
  _pythonver = sys.version_info[0]
52
52
 
53
+ import xml.etree.cElementTree as ET # nosec B405
53
54
 
54
- try:
55
- import xml.etree.cElementTree as ET
56
-
57
- ET.VERSION
58
- except ImportError:
59
- ET = None
55
+ ET.VERSION
60
56
 
61
57
 
62
58
  class GrpcApiError(Exception):
@@ -694,13 +690,9 @@ def read_csv_pandas(filename, encoding="utf-8"): # pragma: no cover
694
690
  :class:`pandas.DataFrame`
695
691
 
696
692
  """
697
- try:
698
- import pandas as pd
693
+ import pandas as pd
699
694
 
700
- return pd.read_csv(filename, encoding=encoding, header=0, na_values=".")
701
- except ImportError:
702
- logging.error("Pandas is not available. Install it.")
703
- return None
695
+ return pd.read_csv(filename, encoding=encoding, header=0, na_values=".")
704
696
 
705
697
 
706
698
  def read_tab(filename): # pragma: no cover
@@ -734,14 +726,10 @@ def read_xlsx(filename): # pragma: no cover
734
726
  list
735
727
 
736
728
  """
737
- try:
738
- import pandas as pd
729
+ import pandas as pd
739
730
 
740
- lines = pd.read_excel(filename)
741
- return lines
742
- except ImportError:
743
- lines = []
744
- return lines
731
+ lines = pd.read_excel(filename)
732
+ return lines
745
733
 
746
734
 
747
735
  def write_csv(output, list_data, delimiter=",", quotechar="|", quoting=csv.QUOTE_MINIMAL): # pragma: no cover
@@ -871,11 +859,7 @@ def compute_fft(time_vals, value): # pragma: no cover
871
859
  tuple
872
860
  Frequency and Values.
873
861
  """
874
- try:
875
- import numpy as np
876
- except ImportError:
877
- logging.error("NumPy is not available. Install it.")
878
- return False
862
+ import numpy as np
879
863
 
880
864
  deltaT = time_vals[-1] - time_vals[0]
881
865
  num_points = len(time_vals)
@@ -924,11 +908,8 @@ def parse_excitation_file(
924
908
  tuple
925
909
  Frequency, magnitude and phase.
926
910
  """
927
- try:
928
- import numpy as np
929
- except ImportError:
930
- logging.error("NumPy is not available. Install it.")
931
- return False
911
+ import numpy as np
912
+
932
913
  df = read_csv_pandas(file_name, encoding=encoding)
933
914
  if is_time_domain:
934
915
  time = df[df.keys()[0]].values * x_scale
@@ -1165,6 +1146,11 @@ def install_with_pip(package_name, package_path=None, upgrade=False, uninstall=F
1165
1146
  """Install a new package using pip.
1166
1147
  This method is useful for installing a package from the AEDT Console without launching the Python environment.
1167
1148
 
1149
+ .. warning::
1150
+ Do not execute this function with untrusted function argument, environment
1151
+ variables or pyedb global settings.
1152
+ See the :ref:`security guide<ref_security_consideration>` for details.
1153
+
1168
1154
  Parameters
1169
1155
  ----------
1170
1156
  package_name : str
@@ -1177,10 +1163,12 @@ def install_with_pip(package_name, package_path=None, upgrade=False, uninstall=F
1177
1163
  Whether to install the package or uninstall the package.
1178
1164
  """
1179
1165
 
1180
- import subprocess
1166
+ import subprocess # nosec B404
1181
1167
 
1182
- executable = '"{}"'.format(sys.executable) if is_windows else sys.executable
1168
+ if not package_name or not isinstance(package_name, str):
1169
+ raise ValueError("A valid package name must be provided.")
1183
1170
 
1171
+ executable = sys.executable
1184
1172
  commands = []
1185
1173
  if uninstall:
1186
1174
  commands.append([executable, "-m", "pip", "uninstall", "--yes", package_name])
@@ -1194,12 +1182,12 @@ def install_with_pip(package_name, package_path=None, upgrade=False, uninstall=F
1194
1182
  command.append("-U")
1195
1183
 
1196
1184
  commands.append(command)
1185
+
1197
1186
  for command in commands:
1198
- if is_linux:
1199
- p = subprocess.Popen(command)
1200
- else:
1201
- p = subprocess.Popen(" ".join(command))
1202
- p.wait()
1187
+ try:
1188
+ subprocess.run(command, check=True) # nosec
1189
+ except subprocess.CalledProcessError as e: # nosec
1190
+ raise RuntimeError("An error occurred while installing with pip") from e
1203
1191
 
1204
1192
 
1205
1193
  class Help: # pragma: no cover
pyedb/generic/plot.py CHANGED
@@ -1,31 +1,16 @@
1
1
  import os
2
- import warnings
3
2
 
4
- try:
5
- import numpy # noqa: F401
6
- except ImportError:
7
- warnings.warn(
8
- "The NumPy module is required to run some functionalities of PostProcess.\n"
9
- "Install with \n\npip install numpy\n\nRequires CPython."
10
- )
11
- try:
12
- from matplotlib.patches import PathPatch
13
- from matplotlib.path import Path
3
+ from matplotlib.patches import PathPatch
4
+ from matplotlib.path import Path
5
+ import numpy # noqa: F401
14
6
 
15
- # Use matplotlib agg backend (non-interactive) when the CI is running.
16
- if bool(int(os.getenv("PYEDB_CI_NO_DISPLAY", "0"))): # pragma: no cover
17
- import matplotlib
7
+ # Use matplotlib agg backend (non-interactive) when the CI is running.
8
+ if bool(int(os.getenv("PYEDB_CI_NO_DISPLAY", "0"))): # pragma: no cover
9
+ import matplotlib
18
10
 
19
- matplotlib.use("Agg")
20
- import matplotlib.pyplot as plt
11
+ matplotlib.use("Agg")
21
12
 
22
- except ImportError:
23
- warnings.warn(
24
- "The Matplotlib module is required to run some functionalities of PostProcess.\n"
25
- "Install with \n\npip install matplotlib\n\nRequires CPython."
26
- )
27
- except Exception:
28
- warnings.warn("Unknown error occurred while attempting to import Matplotlib.")
13
+ import matplotlib.pyplot as plt
29
14
 
30
15
 
31
16
  def plot_matplotlib(
pyedb/generic/process.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import os.path
2
2
  from pathlib import Path
3
- import subprocess
3
+ import subprocess # nosec B404
4
4
 
5
5
  from pyedb.generic.general_methods import is_linux
6
6
 
@@ -23,7 +23,63 @@ class SiwaveSolve(object):
23
23
  full_path = Path(self._pedb.ansys_em_path) / executable
24
24
  return str(full_path)
25
25
 
26
+ def __create_exec(self, type):
27
+ base = os.path.splitext(self._pedb.edbpath)[0]
28
+ txt_path = base + ".txt"
29
+ exec_path = base + ".exec"
30
+ with open(txt_path, "w") as file:
31
+ if type:
32
+ if type == "DCIR":
33
+ file.write("ExecDcSim")
34
+ elif type == "SYZ":
35
+ file.write("ExecSyzSim")
36
+ elif type == "CPA":
37
+ file.write("ExecSentinelCpaSim")
38
+ elif type == "TimeCrosstalk":
39
+ file.write("ExecTimeDomainCrosstalkSim")
40
+ elif type == "FreqCrosstalk":
41
+ file.write("ExecCrosstalkSim")
42
+ elif type == "Impedance":
43
+ file.write("ExecZ0Sim")
44
+
45
+ os.rename(txt_path, exec_path)
46
+ return exec_path
47
+
48
+ def solve_siwave(self, edbpath, analysis_type):
49
+ """Solve an SIWave setup. Only non-graphical batch mode is supported.
50
+
51
+ Parameters
52
+ ----------
53
+ analysis_type: str
54
+ Type of SIWave analysis to perform. Available types are "SYZ", "DCIR", "CPA", "TimeCrosstalk",
55
+ "FreqCrosstalk", "Impedance".
56
+ edbpath: str
57
+ Full path to the .aedb folder, siw or siwz file to be solved.
58
+ siwave_ng: str, optinial
59
+ Path to the siwave_ng. Default is the SIWave installation path.
60
+ """
61
+
62
+ command = [
63
+ self.__siwave_ng_exe_path,
64
+ edbpath,
65
+ self.__create_exec(type=analysis_type),
66
+ "-formatOutput",
67
+ "-useSubdir",
68
+ ]
69
+ try:
70
+ subprocess.run(command, check=True)
71
+ except subprocess.CalledProcessError as e:
72
+ raise RuntimeError(f"An error occurred when launching the solver. Please check input paths") from e
73
+
26
74
  def solve(self, num_of_cores=4):
75
+ """Solve using siwave_ng.exe
76
+
77
+ .. warning::
78
+ Do not execute this function with untrusted function argument, environment
79
+ variables or pyedb global settings.
80
+ See the :ref:`security guide<ref_security_consideration>` for details.
81
+
82
+ """
27
83
  exec_file = os.path.splitext(self._pedb.edbpath)[0] + ".exec"
28
84
  if os.path.exists(exec_file):
29
85
  with open(exec_file, "r+") as f:
@@ -39,16 +95,21 @@ class SiwaveSolve(object):
39
95
  f = open(exec_file, "w")
40
96
  f.writelines(content)
41
97
  command = [self.__siwave_ng_exe_path, self._pedb.edbpath, exec_file, "-formatOutput -useSubdir"]
42
- command_ = command if os.name == "posix" else " ".join(command)
43
- # p = subprocess.Popen(command_)
44
- p = subprocess.Popen(command)
45
- p.wait()
98
+ try:
99
+ subprocess.run(command, check=True) # nosec
100
+ except subprocess.CalledProcessError as e: # nosec
101
+ raise RuntimeError("An error occurred while solving") from e
46
102
 
47
103
  def export_3d_cad(
48
104
  self, format_3d="Q3D", output_folder=None, net_list=None, num_cores=4, aedt_file_name=None, hidden=False
49
105
  ): # pragma: no cover
50
106
  """Export edb to Q3D or HFSS
51
107
 
108
+ .. warning::
109
+ Do not execute this function with untrusted function argument, environment
110
+ variables or pyedb global settings.
111
+ See the :ref:`security guide<ref_security_consideration>` for details.
112
+
52
113
  Parameters
53
114
  ----------
54
115
  format_3d : str, default ``Q3D``
@@ -97,10 +158,10 @@ class SiwaveSolve(object):
97
158
  command += ["-RunScriptAndExit", scriptname]
98
159
  print(command)
99
160
  try:
100
- result = subprocess.run(command, check=True, capture_output=True)
161
+ result = subprocess.run(command, check=True, capture_output=True) # nosec
101
162
  print(result.stdout.decode())
102
- except subprocess.CalledProcessError as e:
103
- print(f"Error occurred: {e.stderr.decode()}")
163
+ except subprocess.CalledProcessError as e: # nosec
164
+ raise RuntimeError("An error occurred while exporting 3D CAD") from e
104
165
  return os.path.join(output_folder, aedt_file_name)
105
166
 
106
167
  def export_dc_report(
@@ -119,6 +180,11 @@ class SiwaveSolve(object):
119
180
  ):
120
181
  """Close EDB and solve it with Siwave.
121
182
 
183
+ .. warning::
184
+ Do not execute this function with untrusted function argument, environment
185
+ variables or pyedb global settings.
186
+ See the :ref:`security guide<ref_security_consideration>` for details.
187
+
122
188
  Parameters
123
189
  ----------
124
190
  siwave_project : str
@@ -215,6 +281,8 @@ class SiwaveSolve(object):
215
281
  command.append("-RunScriptAndExit")
216
282
  command.append(scriptname)
217
283
  print(command)
218
- p = subprocess.Popen(command)
219
- p.wait()
284
+ try:
285
+ subprocess.run(command, check=True) # nosec
286
+ except subprocess.CalledProcessError as e: # nosec
287
+ raise RuntimeError("An error occurred while solving with Siwave") from e
220
288
  return output_list
@@ -1010,8 +1010,6 @@ class Components(object):
1010
1010
  if component_definition_pin.is_null:
1011
1011
  self._logger.error(f"Failed to create component definition pin {name}-{pin.name}")
1012
1012
  return None
1013
- else:
1014
- self._logger.warning("Found existing component definition for footprint {}".format(name))
1015
1013
  return component_definition
1016
1014
 
1017
1015
  def create(
@@ -1074,6 +1072,8 @@ class Components(object):
1074
1072
  if not compdef:
1075
1073
  return False
1076
1074
  new_cmp = GrpcComponentGroup.create(self._active_layout, component_name, compdef.name)
1075
+ if new_cmp.is_null:
1076
+ raise ValueError(f"Failed to create component {component_name}.")
1077
1077
  if hasattr(pins[0], "component") and pins[0].component:
1078
1078
  hosting_component_location = None
1079
1079
  if not pins[0].component.is_null:
@@ -1469,13 +1469,15 @@ class Components(object):
1469
1469
  sball_shape = GrpcSolderballShape.SOLDERBALL_SPHEROID
1470
1470
 
1471
1471
  cmp_property = cmp.component_property
1472
- if cmp.type == GrpcComponentType.IC:
1472
+ if cmp.component_type == GrpcComponentType.IC:
1473
1473
  ic_die_prop = cmp_property.die_property
1474
1474
  ic_die_prop.die_type = GrpcDieType.FLIPCHIP
1475
+ if not cmp.placement_layer == list(self._pedb.stackup.layers.keys())[0]:
1476
+ chip_orientation = "chip_up"
1475
1477
  if chip_orientation.lower() == "chip_up":
1476
- ic_die_prop.orientation = GrpDieOrientation.CHIP_UP
1478
+ ic_die_prop.die_orientation = GrpDieOrientation.CHIP_UP
1477
1479
  else:
1478
- ic_die_prop.orientation = GrpDieOrientation.CHIP_DOWN
1480
+ ic_die_prop.die_orientation = GrpDieOrientation.CHIP_DOWN
1479
1481
  cmp_property.die_property = ic_die_prop
1480
1482
 
1481
1483
  solder_ball_prop = cmp_property.solder_ball_property
@@ -23,10 +23,12 @@
23
23
  import copy
24
24
  import os
25
25
  import re
26
- import subprocess
26
+ import subprocess # nosec B404
27
27
  import sys
28
28
  from typing import Any, Dict, List, Optional, Union
29
29
 
30
+ from defusedxml.ElementTree import parse as defused_parse
31
+
30
32
  from pyedb.generic.general_methods import ET, env_path, env_value, is_linux
31
33
  from pyedb.generic.settings import settings
32
34
  from pyedb.misc.aedtlib_personalib_install import write_pretty_xml
@@ -36,6 +38,11 @@ from pyedb.misc.misc import list_installed_ansysem
36
38
  def convert_technology_file(tech_file, edbversion=None, control_file=None):
37
39
  """Convert a technology file to EDB control file (XML).
38
40
 
41
+ .. warning::
42
+ Do not execute this function with untrusted function argument, environment
43
+ variables or pyedb global settings.
44
+ See the :ref:`security guide<ref_security_consideration>` for details.
45
+
39
46
  Parameters
40
47
  ----------
41
48
  tech_file : str
@@ -212,10 +219,11 @@ def convert_technology_file(tech_file, edbversion=None, control_file=None):
212
219
  ]
213
220
  commands.append(command)
214
221
  commands.append(["rm", "-r", vlc_file_name + ".aedb"])
215
- my_env = os.environ.copy()
216
222
  for command in commands:
217
- p = subprocess.Popen(command, env=my_env)
218
- p.wait()
223
+ try:
224
+ subprocess.run(command, check=True) # nosec
225
+ except subprocess.CalledProcessError as e: # nosec
226
+ raise RuntimeError("An error occurred while converting a technology file to edb control file") from e
219
227
  if os.path.exists(control_file):
220
228
  settings.logger.info("XML file created.")
221
229
  return control_file
@@ -1652,7 +1660,7 @@ class ControlFile:
1652
1660
  bool
1653
1661
  ``True`` if successful, ``False`` otherwise.
1654
1662
  """
1655
- tree = ET.parse(xml_input)
1663
+ tree = defused_parse(xml_input)
1656
1664
  root = tree.getroot()
1657
1665
  for el in root:
1658
1666
  if el.tag == "Stackup":
@@ -35,6 +35,7 @@ from ansys.edb.core.hierarchy.structure3d import MeshClosure as GrpcMeshClosure,
35
35
  from ansys.edb.core.primitive.circle import Circle as GrpcCircle
36
36
 
37
37
  from pyedb.generic.general_methods import generate_unique_name
38
+ from pyedb.grpc.database.primitive.circle import Circle
38
39
  from pyedb.grpc.database.utility.value import Value
39
40
 
40
41
 
@@ -294,7 +295,11 @@ class PadstackDef(GrpcPadstackDef):
294
295
  List[:class:`PadstackInstance <pyedb.grpc.database.primitive.padstack_instance.PadstackInstance>`]
295
296
  List of PadstackInstance objects.
296
297
  """
297
- return [i for i in list(self._pedb.padstacks.instances.values()) if i.padstack_def.name == self.name]
298
+ return [
299
+ i
300
+ for i in list(self._pedb.padstacks.instances.values())
301
+ if not i.is_null and i.padstack_def.name == self.name
302
+ ]
298
303
 
299
304
  @property
300
305
  def layers(self) -> list[str]:
@@ -717,7 +722,7 @@ class PadstackDef(GrpcPadstackDef):
717
722
  net_name=via.net_name,
718
723
  )
719
724
  else:
720
- GrpcCircle.create(
725
+ Circle(self._pedb).create(
721
726
  layout,
722
727
  self.start_layer,
723
728
  via.net,
@@ -732,7 +737,7 @@ class PadstackDef(GrpcPadstackDef):
732
737
  net_name=via.net_name,
733
738
  )
734
739
  else:
735
- GrpcCircle.create(
740
+ Circle(self._pedb).create(
736
741
  layout,
737
742
  self.stop_layer,
738
743
  via.net,
@@ -745,7 +750,7 @@ class PadstackDef(GrpcPadstackDef):
745
750
  if layer_name == via.start_layer or started:
746
751
  start = layer_name
747
752
  stop = layer_names[layer_names.index(layer_name) + 1]
748
- cloned_circle = GrpcCircle.create(
753
+ cloned_circle = Circle(self._pedb).create(
749
754
  layout,
750
755
  start,
751
756
  via.net,
@@ -753,7 +758,7 @@ class PadstackDef(GrpcPadstackDef):
753
758
  Value(pos[1]),
754
759
  Value(rad1),
755
760
  )
756
- cloned_circle2 = GrpcCircle.create(
761
+ cloned_circle2 = Circle(self._pedb).create(
757
762
  layout,
758
763
  stop,
759
764
  via.net,
@@ -48,7 +48,9 @@ from ansys.edb.core.terminal.padstack_instance_terminal import (
48
48
  PadstackInstanceTerminal as GrpcPadstackInstanceTerminal,
49
49
  )
50
50
  from ansys.edb.core.utility.rlc import Rlc as GrpcRlc
51
+ import numpy as np
51
52
 
53
+ from pyedb.generic.general_methods import get_filename_without_extension
52
54
  from pyedb.grpc.database.hierarchy.pin_pair_model import PinPairModel
53
55
  from pyedb.grpc.database.hierarchy.s_parameter_model import SparamModel
54
56
  from pyedb.grpc.database.hierarchy.spice_model import SpiceModel
@@ -59,15 +61,6 @@ from pyedb.grpc.database.terminal.padstack_instance_terminal import (
59
61
  )
60
62
  from pyedb.grpc.database.utility.value import Value
61
63
 
62
- try:
63
- import numpy as np
64
- except ImportError:
65
- warnings.warn(
66
- "The NumPy module is required to run some functionalities of EDB.\n"
67
- "Install with \n\npip install numpy\n\nRequires CPython."
68
- )
69
- from pyedb.generic.general_methods import get_filename_without_extension
70
-
71
64
 
72
65
  class Component(GrpcComponentGroup):
73
66
  """Manages EDB functionalities for components.
@@ -25,16 +25,13 @@ This module contains these classes: `EdbLayout` and `Shape`.
25
25
  """
26
26
 
27
27
  import math
28
- from typing import Any, Dict, List, Optional, Union
28
+ from typing import Any, Dict, Iterable, List, Optional, Union
29
29
 
30
- from ansys.edb.core.geometry.arc_data import ArcData as GrpcArcData
31
30
  from ansys.edb.core.geometry.point_data import PointData as GrpcPointData
32
31
  from ansys.edb.core.geometry.polygon_data import (
33
32
  PolygonData as GrpcPolygonData,
34
- PolygonSenseType as GrpcPolygonSenseType,
35
33
  )
36
34
  from ansys.edb.core.hierarchy.pin_group import PinGroup as GrpcPinGroup
37
- from ansys.edb.core.inner.exceptions import InvalidArgumentException
38
35
  from ansys.edb.core.primitive.bondwire import BondwireType as GrpcBondwireType
39
36
  from ansys.edb.core.primitive.path import PathCornerType as GrpcPathCornerType, PathEndCapType as GrpcPathEndCapType
40
37
  from ansys.edb.core.primitive.rectangle import (
@@ -51,6 +48,25 @@ from pyedb.grpc.database.utility.layout_statistics import LayoutStatistics
51
48
  from pyedb.grpc.database.utility.value import Value
52
49
 
53
50
 
51
+ def normalize_pairs(points: Iterable[float]) -> List[List[float]]:
52
+ """
53
+ Convert any reasonable point description into [[x1, y1], [x2, y2], …]
54
+ """
55
+ pts = list(points)
56
+ if not pts: # empty input
57
+ return []
58
+
59
+ # Detect flat vs nested
60
+ if isinstance(pts[0], (list, tuple)):
61
+ # already nested – just ensure every item is a *list* (not tuple)
62
+ return [list(pair) for pair in pts]
63
+ else:
64
+ # flat list – chunk into pairs
65
+ if len(pts) % 2:
66
+ raise ValueError("Odd number of coordinates supplied")
67
+ return [[pts[i], pts[i + 1]] for i in range(0, len(pts), 2)]
68
+
69
+
54
70
  class Modeler(object):
55
71
  """Manages EDB methods for primitives management accessible from `Edb.modeler`.
56
72
 
@@ -683,7 +699,8 @@ class Modeler(object):
683
699
  else:
684
700
  corner_style = GrpcPathCornerType.MITER
685
701
  _points = []
686
- if isinstance(points, list):
702
+ if isinstance(points, (list, tuple)):
703
+ points = normalize_pairs(points)
687
704
  for pt in points:
688
705
  _pt = []
689
706
  for coord in pt:
@@ -714,7 +731,7 @@ class Modeler(object):
714
731
 
715
732
  def create_trace(
716
733
  self,
717
- path_list: Union[List[List[float]], GrpcPolygonData],
734
+ path_list: Union[Iterable[float], GrpcPolygonData],
718
735
  layer_name: str,
719
736
  width: float = 1,
720
737
  net_name: str = "",
@@ -726,8 +743,9 @@ class Modeler(object):
726
743
 
727
744
  Parameters
728
745
  ----------
729
- path_list : list
730
- List of [x,y] points.
746
+ path_list : Iterable
747
+ List of points [x,y] or [[x, y], ...]
748
+ or [(x, y)...].
731
749
  layer_name : str
732
750
  Layer name.
733
751
  width : float, optional
@@ -803,6 +821,8 @@ class Modeler(object):
803
821
  for void in voids:
804
822
  if isinstance(void, list):
805
823
  void_polygon_data = GrpcPolygonData(points=void)
824
+ elif isinstance(void, GrpcPolygonData):
825
+ void_polygon_data = void
806
826
  else:
807
827
  void_polygon_data = void.polygon_data
808
828
  if not void_polygon_data.points: