pyedb 0.60.0__py3-none-any.whl → 0.61.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 (34) hide show
  1. pyedb/__init__.py +1 -1
  2. pyedb/configuration/cfg_components.py +35 -7
  3. pyedb/dotnet/database/cell/hierarchy/component.py +8 -6
  4. pyedb/dotnet/database/cell/hierarchy/model.py +1 -28
  5. pyedb/dotnet/database/cell/hierarchy/s_parameter_model.py +10 -14
  6. pyedb/dotnet/database/cell/hierarchy/spice_model.py +13 -7
  7. pyedb/dotnet/database/components.py +5 -1
  8. pyedb/dotnet/database/edb_data/padstacks_data.py +5 -3
  9. pyedb/dotnet/database/modeler.py +2 -1
  10. pyedb/dotnet/database/padstack.py +187 -1
  11. pyedb/dotnet/edb.py +70 -1
  12. pyedb/generic/general_methods.py +21 -0
  13. pyedb/grpc/database/definition/materials.py +1 -1
  14. pyedb/grpc/database/definition/padstack_def.py +16 -9
  15. pyedb/grpc/database/padstacks.py +201 -6
  16. pyedb/grpc/database/primitive/padstack_instance.py +90 -0
  17. pyedb/grpc/edb.py +70 -1
  18. pyedb/grpc/rpc_session.py +16 -3
  19. pyedb/workflows/__init__.py +21 -0
  20. pyedb/workflows/job_manager/__init__.py +21 -0
  21. pyedb/workflows/job_manager/backend/__init__.py +21 -0
  22. pyedb/workflows/job_manager/backend/job_manager_handler.py +910 -0
  23. pyedb/workflows/job_manager/backend/job_submission.py +1169 -0
  24. pyedb/workflows/job_manager/backend/service.py +1663 -0
  25. pyedb/workflows/job_manager/backend/start_service.py +86 -0
  26. pyedb/workflows/job_manager/backend/submit_job_on_scheduler.py +168 -0
  27. pyedb/workflows/job_manager/backend/submit_local_job.py +166 -0
  28. pyedb/workflows/utilities/__init__.py +21 -0
  29. pyedb/workflows/utilities/cutout.py +1 -1
  30. pyedb/workflows/utilities/hfss_log_parser.py +446 -0
  31. {pyedb-0.60.0.dist-info → pyedb-0.61.0.dist-info}/METADATA +7 -4
  32. {pyedb-0.60.0.dist-info → pyedb-0.61.0.dist-info}/RECORD +34 -24
  33. {pyedb-0.60.0.dist-info → pyedb-0.61.0.dist-info}/WHEEL +0 -0
  34. {pyedb-0.60.0.dist-info → pyedb-0.61.0.dist-info}/licenses/LICENSE +0 -0
pyedb/__init__.py CHANGED
@@ -59,7 +59,7 @@ deprecation_warning()
59
59
  #
60
60
 
61
61
  pyedb_path = os.path.dirname(__file__)
62
- __version__ = "0.60.0"
62
+ __version__ = "0.61.0"
63
63
  version = __version__
64
64
 
65
65
  #
@@ -98,14 +98,42 @@ class CfgComponent(CfgBase):
98
98
  m = self._pedb._edb.Cell.Hierarchy.PinPairModel()
99
99
  for i in self.pin_pair_model:
100
100
  p = self._pedb._edb.Utility.PinPair(str(i["first_pin"]), str(i["second_pin"]))
101
+ res = i.get("resistance")
102
+ if res is None:
103
+ # If resistance is not defined, set it to 0 and disable it
104
+ res = "0ohm"
105
+ en_res = False
106
+ else:
107
+ # If resistance is defined, use the provided value and enabled status
108
+ res = i["resistance"]
109
+ en_res = i.get("resistance_enabled", True)
110
+ ind = i.get("inductance")
111
+ if ind is None:
112
+ # If inductance is not defined, set it to 0 and disable it
113
+ ind = "0nH"
114
+ en_ind = False
115
+ else:
116
+ # If inductance is defined, use the provided value and enabled status
117
+ ind = i["inductance"]
118
+ en_ind = i.get("inductance_enabled", True)
119
+ cap = i.get("capacitance")
120
+ if cap is None:
121
+ # If capacitance is not defined, set it to 0 and disable it
122
+ cap = "0pF"
123
+ en_cap = False
124
+ else:
125
+ # If capacitance is defined, use the provided value and enabled status
126
+ cap = i["capacitance"]
127
+ en_cap = i.get("capacitance_enabled", True)
128
+
101
129
  rlc = self._pedb._edb.Utility.Rlc(
102
- self._pedb.edb_value(i["resistance"]),
103
- i["resistance_enabled"],
104
- self._pedb.edb_value(i["inductance"]),
105
- i["inductance_enabled"],
106
- self._pedb.edb_value(i["capacitance"]),
107
- i["capacitance_enabled"],
108
- i["is_parallel"],
130
+ self._pedb.edb_value(res),
131
+ en_res,
132
+ self._pedb.edb_value(ind),
133
+ en_ind,
134
+ self._pedb.edb_value(cap),
135
+ en_cap,
136
+ i.get("is_parallel", False),
109
137
  )
110
138
  m.SetPinPairRlc(p, rlc)
111
139
  c_p.SetModel(m)
@@ -28,11 +28,11 @@ import warnings
28
28
  import numpy as np
29
29
 
30
30
  from pyedb.dotnet.database.cell.hierarchy.hierarchy_obj import Group
31
- from pyedb.dotnet.database.cell.hierarchy.model import PinPairModel, SPICEModel
31
+ from pyedb.dotnet.database.cell.hierarchy.model import PinPairModel
32
32
  from pyedb.dotnet.database.cell.hierarchy.netlist_model import NetlistModel
33
33
  from pyedb.dotnet.database.cell.hierarchy.pin_pair_model import PinPair
34
- from pyedb.dotnet.database.cell.hierarchy.s_parameter_model import SparamModel
35
- from pyedb.dotnet.database.cell.hierarchy.spice_model import SpiceModel
34
+ from pyedb.dotnet.database.cell.hierarchy.s_parameter_model import SParameterModel
35
+ from pyedb.dotnet.database.cell.hierarchy.spice_model import SPICEModel
36
36
  from pyedb.dotnet.database.definition.package_def import PackageDef
37
37
  from pyedb.dotnet.database.edb_data.padstacks_data import EDBPadstackInstance
38
38
  from pyedb.generic.general_methods import get_filename_without_extension
@@ -148,11 +148,13 @@ class EDBComponent(Group):
148
148
  def model(self):
149
149
  """Component model."""
150
150
  edb_object = self.component_property.GetModel().Clone()
151
- model_type = edb_object.ToString().split(".")[-1]
151
+ model_type = edb_object.GetModelType().ToString()
152
152
  if model_type == "PinPairModel":
153
153
  return PinPairModel(self._pedb, edb_object)
154
154
  elif model_type == "SPICEModel":
155
155
  return SPICEModel(self._pedb, edb_object)
156
+ elif model_type == "SParameterModel":
157
+ return SParameterModel(self._pedb, edb_object)
156
158
 
157
159
  @model.setter
158
160
  def model(self, value):
@@ -261,7 +263,7 @@ class EDBComponent(Group):
261
263
  if not self.model_type == "SPICEModel":
262
264
  return None
263
265
  else:
264
- return SpiceModel(self._edb_model)
266
+ return SPICEModel(self._pedb, self._edb_model)
265
267
 
266
268
  @property
267
269
  def s_param_model(self):
@@ -269,7 +271,7 @@ class EDBComponent(Group):
269
271
  if not self.model_type == "SParameterModel":
270
272
  return None
271
273
  else:
272
- return SparamModel(self._edb_model)
274
+ return SParameterModel(self._pedb, self._edb_model)
273
275
 
274
276
  @property
275
277
  def netlist_model(self):
@@ -33,7 +33,7 @@ class Model(ObjBase):
33
33
  @property
34
34
  def model_type(self):
35
35
  """Component model type."""
36
- return self._edb_object.GetModelType()
36
+ return self._edb_object.GetModelType().ToString()
37
37
 
38
38
 
39
39
  class PinPairModel(Model):
@@ -73,30 +73,3 @@ class PinPairModel(Model):
73
73
  bool
74
74
  """
75
75
  return self._edb_object.SetPinPairRlc(pin_pair, pin_par_rlc)
76
-
77
-
78
- class SParameterModel(Model):
79
- """Manages S-parameter model class."""
80
-
81
- def __init__(self, pedb, edb_object=None):
82
- super().__init__(pedb, edb_object)
83
-
84
- def component_model_name(self):
85
- self._edb_object.GetComponentModelName()
86
-
87
-
88
- class SPICEModel(Model):
89
- """Manages SPICE model class."""
90
-
91
- def __init__(self, pedb, edb_object=None):
92
- super().__init__(pedb, edb_object)
93
-
94
- @property
95
- def model_name(self):
96
- """SPICE model name."""
97
- return self._edb_object.GetModelName()
98
-
99
- @property
100
- def spice_file_path(self):
101
- """SPICE file path."""
102
- return self._edb_object.GetSPICEFilePath()
@@ -20,22 +20,18 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  # SOFTWARE.
22
22
 
23
+ from pyedb.dotnet.database.cell.hierarchy.model import Model
23
24
 
24
- class SparamModel(object): # pragma: no cover
25
- def __init__(self, edb_model):
26
- self._edb_model = edb_model
27
25
 
28
- @property
29
- def name(self):
30
- return self._edb_model.GetComponentModelName()
26
+ class SParameterModel(Model):
27
+ """Manages S-parameter model class."""
31
28
 
32
- @property
33
- def reference_net(self):
34
- return self._edb_model.GetReferenceNet()
29
+ def __init__(self, pedb, edb_object=None):
30
+ super().__init__(pedb, edb_object)
31
+
32
+ def component_model_name(self):
33
+ self._edb_object.GetComponentModelName()
35
34
 
36
35
  @property
37
- def is_null(self):
38
- """Adding this property to be compatible with grpc."""
39
- if self.name:
40
- return False
41
- return True
36
+ def reference_net(self):
37
+ return self._edb_object.GetReferenceNet()
@@ -20,15 +20,21 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  # SOFTWARE.
22
22
 
23
+ from pyedb.dotnet.database.cell.hierarchy.model import Model
23
24
 
24
- class SpiceModel(object): # pragma: no cover
25
- def __init__(self, edb_model):
26
- self._edb_model = edb_model
25
+
26
+ class SPICEModel(Model):
27
+ """Manages SPICE model class."""
28
+
29
+ def __init__(self, pedb, edb_object=None):
30
+ super().__init__(pedb, edb_object)
27
31
 
28
32
  @property
29
- def file_path(self):
30
- return self._edb_model.GetSPICEFilePath()
33
+ def model_name(self):
34
+ """SPICE model name."""
35
+ return self._edb_object.GetModelName()
31
36
 
32
37
  @property
33
- def name(self):
34
- return self._edb_model.GetSPICEName()
38
+ def spice_file_path(self):
39
+ """SPICE file path."""
40
+ return self._edb_object.GetSPICEFilePath()
@@ -2374,7 +2374,11 @@ class Components(object):
2374
2374
  continue
2375
2375
  part_name = comp.partname
2376
2376
  comp_type = comp.type
2377
- if comp_type == "Resistor":
2377
+ if not comp.model:
2378
+ print("")
2379
+ elif comp.model.model_type != "PinPairModel":
2380
+ value = ""
2381
+ elif comp_type == "Resistor":
2378
2382
  value = str(comp.res_value)
2379
2383
  elif comp_type == "Capacitor":
2380
2384
  value = str(comp.cap_value)
@@ -1586,10 +1586,12 @@ class EDBPadstackInstance(Connectable):
1586
1586
  False,
1587
1587
  )
1588
1588
 
1589
- def set_back_drill_by_layer(self, drill_to_layer, diameter, offset):
1589
+ def set_back_drill_by_layer(self, drill_to_layer, diameter, offset, from_bottom=True):
1590
1590
  """Method added to bring compatibility with grpc."""
1591
-
1592
- self.set_backdrill_bottom(drill_depth=drill_to_layer.name, drill_diameter=diameter, offset=offset)
1591
+ if from_bottom:
1592
+ if not isinstance(drill_to_layer, str):
1593
+ drill_to_layer = drill_to_layer.name
1594
+ self.set_backdrill_bottom(drill_depth=drill_to_layer, drill_diameter=diameter, offset=offset)
1593
1595
 
1594
1596
  def set_backdrill_bottom(self, drill_depth, drill_diameter, offset=0.0):
1595
1597
  """Set backdrill from bottom.
@@ -1474,7 +1474,8 @@ class Modeler(object):
1474
1474
  """Create a PinGroup.
1475
1475
 
1476
1476
  Parameters
1477
- name : str,
1477
+ ----------
1478
+ name : str
1478
1479
  Name of the PinGroup.
1479
1480
  pins_by_id : list[int] or None
1480
1481
  List of pins by ID.
@@ -26,7 +26,7 @@ This module contains the `EdbPadstacks` class.
26
26
 
27
27
  from collections import defaultdict
28
28
  import math
29
- from typing import Dict, List
29
+ from typing import Dict, List, Optional, Union
30
30
  import warnings
31
31
 
32
32
  import numpy as np
@@ -442,6 +442,192 @@ class EdbPadstacks(object):
442
442
  )
443
443
  PadStack.SetData(new_PadStackData)
444
444
 
445
+ def create_dielectric_filled_backdrills(
446
+ self,
447
+ layer: str,
448
+ diameter: Union[float, str],
449
+ material: str,
450
+ permittivity: float,
451
+ padstack_instances: Optional[List[EDBPadstackInstance]] = None,
452
+ padstack_definition: Optional[Union[str, List[str]]] = None,
453
+ dielectric_loss_tangent: Optional[float] = None,
454
+ nets: Optional[Union[str, List[str]]] = None,
455
+ ) -> bool:
456
+ r"""Create dielectric-filled back-drills for through-hole vias.
457
+
458
+ Back-drilling (a.k.a. controlled-depth drilling) is used to remove the
459
+ unused via stub that acts as an unterminated transmission-line segment,
460
+ thereby improving signal-integrity at high frequencies. This routine
461
+ goes one step further: after the stub is removed the resulting cylindrical
462
+ cavity is **completely filled** with a user-specified dielectric. The
463
+ fill material restores mechanical rigidity, prevents solder-wicking, and
464
+ keeps the original via’s electrical characteristics intact on the
465
+ remaining, still-plated, portion.
466
+
467
+ Selection criteria
468
+ ------------------
469
+ A via is processed only when **all** of the following are true:
470
+
471
+ 1. It is a through-hole structure (spans at least three metal layers).
472
+ 2. It includes the requested ``layer`` somewhere in its layer span.
473
+ 3. It belongs to one of the supplied ``padstack_definition`` names
474
+ (or to *any* definition if the argument is omitted).
475
+ 4. It is attached to one of the supplied ``nets`` (or to *any* net if
476
+ the argument is omitted).
477
+
478
+ Geometry that is created
479
+ ------------------------
480
+ For every qualified via the routine
481
+
482
+ * Generates a new pad-stack definition named ``<original_name>_BD``.
483
+ The definition is drilled from the **bottom-most signal layer** up to
484
+ and **including** ``layer``, uses the exact ``diameter`` supplied, and
485
+ is plated at 100 %.
486
+ * Places an additional pad-stack instance on top of the original via,
487
+ thereby filling the newly drilled cavity with the requested
488
+ ``material``.
489
+ * Leaves the original via untouched—only its unused stub is removed.
490
+
491
+ The back-drill is **not** subtracted from anti-pads or plane clearances;
492
+ the filling material is assumed to be electrically invisible at the
493
+ frequencies of interest.
494
+
495
+ Parameters
496
+ ----------
497
+ layer : :class:`str`
498
+ Signal layer name up to which the back-drill is performed (inclusive).
499
+ The drill always starts on the bottom-most signal layer of the stack-up.
500
+ diameter : :class:`float` or :class:`str`
501
+ Finished hole diameter for the back-drill. A numeric value is
502
+ interpreted in the database length unit; a string such as
503
+ ``"0.3mm"`` is evaluated with units.
504
+ material : :class:`str`
505
+ Name of the dielectric material that fills the drilled cavity. If the
506
+ material does not yet exist in the central material library it is
507
+ created on the fly.
508
+ permittivity : :class:`float`
509
+ Relative permittivity :math:`\varepsilon_{\mathrm{r}}` used when the
510
+ material has to be created. Must be positive.
511
+ padstack_instances : :class:`list` [:class:`PadstackInstance` ], optional
512
+ Explicit list of via instances to process. When provided,
513
+ ``padstack_definition`` and ``nets`` are ignored for filtering.
514
+ padstack_definition : :class:`str` or :class:`list` [:class:`str` ], optional
515
+ Pad-stack definition(s) to process. If omitted, **all** through-hole
516
+ definitions are considered.
517
+ dielectric_loss_tangent : :class:`float`, optional
518
+ Loss tangent :math:`\tan\delta` used when the material has to be
519
+ created. Defaults to ``0.0``.
520
+ nets : :class:`str` or :class:`list` [:class:`str` ], optional
521
+ Net name(s) used to filter vias. If omitted, vias belonging to
522
+ **any** net are processed.
523
+
524
+ Returns
525
+ -------
526
+ :class:`bool`
527
+ ``True`` when at least one back-drill was successfully created.
528
+ ``False`` if no suitable via was found or any error occurred.
529
+
530
+ Raises
531
+ ------
532
+ ValueError
533
+ If ``material`` is empty or if ``permittivity`` is non-positive when a
534
+ new material must be created.
535
+
536
+ Notes
537
+ -----
538
+ * The routine is safe to call repeatedly: existing back-drills are **not**
539
+ duplicated because the ``*_BD`` definition name is deterministic.
540
+ * The original via keeps its pad-stack definition and net assignment; only
541
+ its unused stub is removed.
542
+ * The back-drill is **not** subtracted from anti-pads or plane clearances;
543
+ the filling material is assumed to be electrically invisible at the
544
+ frequencies of interest.
545
+
546
+ Examples
547
+ --------
548
+ Create back-drills on all vias belonging to two specific pad-stack
549
+ definitions and two DDR4 nets:
550
+
551
+ >>> edb.padstacks.create_dielectric_filled_backdrills(
552
+ ... layer="L3",
553
+ ... diameter="0.25mm",
554
+ ... material="EPON_827",
555
+ ... permittivity=3.8,
556
+ ... dielectric_loss_tangent=0.015,
557
+ ... padstack_definition=["VIA_10MIL", "VIA_16MIL"],
558
+ ... nets=["DDR4_DQ0", "DDR4_DQ1"],
559
+ ... )
560
+ True
561
+ """
562
+ _padstack_instances = defaultdict(list)
563
+ if padstack_instances:
564
+ for inst in padstack_instances:
565
+ _padstack_instances[inst.padstack_definition].append(inst)
566
+ else:
567
+ if padstack_definition:
568
+ if isinstance(padstack_definition, str):
569
+ padstack_definition = [padstack_definition]
570
+
571
+ padstack_definitions = [
572
+ self.definitions.get(padstack_def, None) for padstack_def in padstack_definition
573
+ ]
574
+ if nets:
575
+ if isinstance(nets, str):
576
+ nets = [nets]
577
+ for padstack_definition in padstack_definitions:
578
+ _padstack_instances[padstack_definition.name] = self.get_instances(
579
+ definition_name=padstack_definition.name, net_name=nets
580
+ )
581
+ else:
582
+ for padstack_definition in padstack_definitions:
583
+ _padstack_instances[padstack_definition.name] = padstack_definition.instances
584
+ elif nets:
585
+ instances = self.get_instances(net_name=nets)
586
+ for inst in instances:
587
+ padsatck_def_name = inst.padstack_definition
588
+ padstack_def_layers = inst.layer_range_names
589
+ if layer in padstack_def_layers and len(padstack_def_layers) >= 3:
590
+ if not padsatck_def_name in _padstack_instances:
591
+ _padstack_instances[padsatck_def_name] = [inst]
592
+ else:
593
+ _padstack_instances[padsatck_def_name].append(inst)
594
+ else:
595
+ self._pedb.logger.info(
596
+ f"Drill layer {layer} not in padstack definition layers "
597
+ f"or layer number = {len(padstack_def_layers)} "
598
+ f"for padstack definition {padsatck_def_name}, skipping for backdrills"
599
+ )
600
+ if not material:
601
+ raise ValueError("`material` must be specified")
602
+ if not material in self._pedb.materials:
603
+ if not dielectric_loss_tangent:
604
+ dielectric_loss_tangent = 0.0
605
+ self._pedb.materials.add_dielectric_material(
606
+ name=material, permittivity=permittivity, dielectric_loss_tangent=dielectric_loss_tangent
607
+ )
608
+ for def_name, instances in _padstack_instances.items():
609
+ padstack_def_backdrill_name = f"{def_name}_BD"
610
+ start_layer = list(self._pedb.stackup.signal_layers.keys())[-1] # bottom layer
611
+ self.create(
612
+ padstackname=padstack_def_backdrill_name,
613
+ holediam=self._pedb.value(diameter),
614
+ paddiam="0.0",
615
+ antipaddiam="0.0",
616
+ start_layer=start_layer,
617
+ stop_layer=layer,
618
+ )
619
+ self.definitions[padstack_def_backdrill_name].material = material
620
+ self.definitions[padstack_def_backdrill_name].hole_plating_ratio = 100.0
621
+ for inst in instances:
622
+ inst.set_back_drill_by_layer(drill_to_layer=layer, offset=0.0, diameter=self._pedb.value(diameter))
623
+ self.place(
624
+ position=inst.position,
625
+ definition_name=padstack_def_backdrill_name,
626
+ fromlayer=start_layer,
627
+ tolayer=layer,
628
+ )
629
+ return True
630
+
445
631
  def delete_padstack_instances(self, net_names): # pragma: no cover
446
632
  """Delete padstack instances by net names.
447
633
 
pyedb/dotnet/edb.py CHANGED
@@ -102,6 +102,7 @@ from pyedb.misc.decorators import deprecate_argument_name, execution_timer
102
102
  from pyedb.modeler.geometry_operators import GeometryOperators
103
103
  from pyedb.siwave_core.product_properties import SIwaveProperties
104
104
  from pyedb.workflow import Workflow
105
+ from pyedb.workflows.job_manager.backend.job_manager_handler import JobManagerHandler
105
106
  from pyedb.workflows.utilities.cutout import Cutout
106
107
 
107
108
 
@@ -429,11 +430,23 @@ class Edb:
429
430
  self._core_primitives = Modeler(self)
430
431
  self._stackup2 = self._stackup
431
432
  self._materials = Materials(self)
433
+ self._job_manager = JobManagerHandler(self)
432
434
 
433
435
  @property
434
436
  def pedb_class(self):
435
437
  return pyedb.dotnet
436
438
 
439
+ @property
440
+ def job_manager(self):
441
+ """Job manager for handling simulation tasks.
442
+
443
+ Returns
444
+ -------
445
+ :class:`JobManagerHandler <pyedb.workflows.job_manager.job_manager_handler.JobManagerHandler>`
446
+ Job manager instance for submitting and managing simulation jobs.
447
+ """
448
+ return self._job_manager
449
+
437
450
  def value(self, val):
438
451
  """Convert a value into a pyedb value."""
439
452
  val_ = val if isinstance(val, self._edb.Utility.Value) else self.edb_value(val)
@@ -668,7 +681,8 @@ class Edb:
668
681
  # self.standalone = False
669
682
 
670
683
  self.core.Database.SetRunAsStandAlone(self.standalone)
671
-
684
+ if self._db: # pragma no cover
685
+ self._db.Close()
672
686
  self._db = self.core.Database.Create(self.edbpath)
673
687
 
674
688
  if not self._db:
@@ -834,6 +848,61 @@ class Edb:
834
848
  self.edbpath = os.path.join(working_dir, aedb_name)
835
849
  return self.open_edb()
836
850
 
851
+ def import_vlctech_stackup(
852
+ self,
853
+ vlctech_file,
854
+ working_dir="",
855
+ export_xml=None,
856
+ ):
857
+ """Import a vlc.tech file and generate an ``edb.def`` file in the working directory containing only the stackup.
858
+
859
+ Parameters
860
+ ----------
861
+ vlctech_file : str
862
+ Full path to the technology stackup file. It must be vlc.tech.
863
+ working_dir : str, optional
864
+ Directory in which to create the ``aedb`` folder. The name given to the AEDB file
865
+ is the same as the name of the board file.
866
+ export_xml : str, optional
867
+ Export technology file in XML control file format.
868
+
869
+ Returns
870
+ -------
871
+ Full path to the AEDB file : str
872
+
873
+ """
874
+ if not working_dir:
875
+ working_dir = os.path.dirname(vlctech_file)
876
+ command = os.path.join(self.base_path, "helic", "tools", "raptorh", "bin", "make-edb")
877
+ if is_windows:
878
+ command += ".exe"
879
+ else:
880
+ os.environ["HELIC_ROOT"] = os.path.join(self.base_path, "helic")
881
+ cmd_make_edb = [
882
+ command,
883
+ "-t",
884
+ "{}".format(vlctech_file),
885
+ "-o",
886
+ "{}".format(os.path.join(working_dir, "vlctech")),
887
+ ]
888
+ if export_xml:
889
+ cmd_make_edb.extend(["-x", "{}".format(export_xml)])
890
+ try:
891
+ subprocess.run(cmd_make_edb, check=True) # nosec
892
+ except subprocess.CalledProcessError as e: # nosec
893
+ raise RuntimeError(
894
+ "Failed to create edb. Please check if the executable is present in the base path."
895
+ ) from e
896
+
897
+ if not os.path.exists(os.path.join(working_dir, "vlctech.aedb")):
898
+ self.logger.error("Failed to create edb. Please check if the executable is present in the base path.")
899
+ return False
900
+ else:
901
+ self.logger.info("edb successfully created.")
902
+ self.edbpath = os.path.join(working_dir, "vlctech.aedb")
903
+ self.open_edb()
904
+ return self.edbpath
905
+
837
906
  def export_to_ipc2581(self, ipc_path=None, units="MILLIMETER"):
838
907
  """Create an XML IPC2581 file from the active EDB.
839
908
 
@@ -42,6 +42,7 @@ import sys
42
42
  import tempfile
43
43
  import time
44
44
  import traceback
45
+ from typing import Dict
45
46
 
46
47
  from pyedb.generic.constants import CSS4_COLORS
47
48
  from pyedb.generic.settings import settings
@@ -135,6 +136,26 @@ def _exception(ex_info, func, args, kwargs, message="Type Error"):
135
136
  )
136
137
 
137
138
 
139
+ def installed_ansys_em_versions() -> Dict[str, str]:
140
+ """
141
+ Scan environment variables and return a dict
142
+ {version: installation_path} for every ANSYS EM release found.
143
+ Versions are ordered from oldest → latest (latest appears last).
144
+ """
145
+ pattern = re.compile(r"^ANSYSEM_ROOT(\d{3})$", re.IGNORECASE)
146
+
147
+ # collect everything
148
+ versions = {}
149
+ for key, value in os.environ.items():
150
+ m = pattern.match(key)
151
+ if m:
152
+ version = m.group(1)
153
+ versions[version] = value.rstrip(os.sep)
154
+
155
+ # sort ascending so the latest is last
156
+ return dict(sorted(versions.items(), key=lambda kv: int(kv[0])))
157
+
158
+
138
159
  def get_filename_without_extension(path):
139
160
  """Get the filename without its extension.
140
161
 
@@ -722,7 +722,7 @@ class Materials(object):
722
722
  :class:`Material <pyedb.grpc.database.definition.materials.Material>`
723
723
  Material object.
724
724
  """
725
- extended_kwargs = {key: value for (key, value) in kwargs.items()}
725
+ extended_kwargs = {key: value for (key, value) in kwargs.items() if not key == "name"}
726
726
  extended_kwargs["permittivity"] = permittivity
727
727
  extended_kwargs["dielectric_loss_tangent"] = dielectric_loss_tangent
728
728
  material = self.add_material(name, **extended_kwargs)
@@ -366,6 +366,22 @@ class PadstackDef(GrpcPadstackDef):
366
366
  warnings.warn("via_stop_layer is deprecated. Use stop_layer instead.", DeprecationWarning)
367
367
  return self.stop_layer
368
368
 
369
+ @property
370
+ def material(self):
371
+ """Return hole material name.
372
+
373
+ Returns
374
+ -------
375
+ str
376
+ Hole material name.
377
+ """
378
+ return self.data.material.value
379
+
380
+ @material.setter
381
+ def material(self, value):
382
+ if isinstance(value, str):
383
+ self.data.material = value
384
+
369
385
  @property
370
386
  def hole_diameter(self) -> float:
371
387
  """Hole diameter.
@@ -647,15 +663,6 @@ class PadstackDef(GrpcPadstackDef):
647
663
  else: # pragma no cover
648
664
  self.data.hole_range = GrpcPadstackHoleRange.UNKNOWN_RANGE
649
665
 
650
- @property
651
- def material(self) -> str:
652
- """Return hole material name."""
653
- return self.data.material.value
654
-
655
- @material.setter
656
- def material(self, value):
657
- self.data.material.value = value
658
-
659
666
  def convert_to_3d_microvias(
660
667
  self, convert_only_signal_vias=True, hole_wall_angle=15, delete_padstack_def=True
661
668
  ) -> bool: