pyedb 0.54.0__py3-none-any.whl → 0.55.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 (95) hide show
  1. pyedb/__init__.py +1 -8
  2. pyedb/configuration/cfg_boundaries.py +69 -151
  3. pyedb/configuration/cfg_components.py +201 -460
  4. pyedb/configuration/cfg_data.py +4 -2
  5. pyedb/configuration/cfg_general.py +13 -36
  6. pyedb/configuration/cfg_modeler.py +2 -1
  7. pyedb/configuration/cfg_nets.py +21 -35
  8. pyedb/configuration/cfg_operations.py +22 -151
  9. pyedb/configuration/cfg_package_definition.py +56 -112
  10. pyedb/configuration/cfg_padstacks.py +292 -688
  11. pyedb/configuration/cfg_pin_groups.py +32 -79
  12. pyedb/configuration/cfg_ports_sources.py +19 -6
  13. pyedb/configuration/cfg_s_parameter_models.py +67 -172
  14. pyedb/configuration/cfg_setup.py +102 -295
  15. pyedb/configuration/configuration.py +64 -5
  16. pyedb/dotnet/database/cell/connectable.py +38 -9
  17. pyedb/dotnet/database/cell/hierarchy/component.py +28 -28
  18. pyedb/dotnet/database/cell/hierarchy/model.py +1 -1
  19. pyedb/dotnet/database/cell/layout.py +63 -2
  20. pyedb/dotnet/database/cell/layout_obj.py +2 -2
  21. pyedb/dotnet/database/cell/primitive/path.py +6 -8
  22. pyedb/dotnet/database/cell/primitive/primitive.py +3 -24
  23. pyedb/dotnet/database/cell/terminal/edge_terminal.py +2 -2
  24. pyedb/dotnet/database/cell/terminal/padstack_instance_terminal.py +1 -1
  25. pyedb/dotnet/database/cell/terminal/pingroup_terminal.py +1 -1
  26. pyedb/dotnet/database/cell/terminal/point_terminal.py +1 -1
  27. pyedb/dotnet/database/cell/terminal/terminal.py +24 -24
  28. pyedb/dotnet/database/cell/voltage_regulator.py +0 -21
  29. pyedb/dotnet/database/components.py +96 -88
  30. pyedb/dotnet/database/definition/component_def.py +4 -4
  31. pyedb/dotnet/database/definition/component_model.py +1 -1
  32. pyedb/dotnet/database/definition/package_def.py +2 -3
  33. pyedb/dotnet/database/dotnet/database.py +3 -199
  34. pyedb/dotnet/database/dotnet/primitive.py +3 -3
  35. pyedb/dotnet/database/edb_data/control_file.py +5 -5
  36. pyedb/dotnet/database/edb_data/hfss_extent_info.py +6 -6
  37. pyedb/dotnet/database/edb_data/layer_data.py +23 -23
  38. pyedb/dotnet/database/edb_data/padstacks_data.py +63 -88
  39. pyedb/dotnet/database/edb_data/primitives_data.py +5 -5
  40. pyedb/dotnet/database/edb_data/sources.py +6 -6
  41. pyedb/dotnet/database/edb_data/variables.py +1 -1
  42. pyedb/dotnet/database/geometry/point_data.py +14 -10
  43. pyedb/dotnet/database/geometry/polygon_data.py +3 -3
  44. pyedb/dotnet/database/hfss.py +46 -48
  45. pyedb/dotnet/database/layout_validation.py +14 -11
  46. pyedb/dotnet/database/materials.py +10 -11
  47. pyedb/dotnet/database/modeler.py +97 -91
  48. pyedb/dotnet/database/nets.py +19 -22
  49. pyedb/dotnet/database/padstack.py +84 -83
  50. pyedb/dotnet/database/siwave.py +42 -42
  51. pyedb/dotnet/database/stackup.py +140 -72
  52. pyedb/dotnet/database/utilities/heatsink.py +4 -4
  53. pyedb/dotnet/database/utilities/obj_base.py +2 -2
  54. pyedb/dotnet/database/utilities/simulation_setup.py +2 -2
  55. pyedb/dotnet/database/utilities/value.py +16 -16
  56. pyedb/dotnet/edb.py +228 -150
  57. pyedb/edb_logger.py +12 -27
  58. pyedb/extensions/via_design_backend.py +6 -3
  59. pyedb/generic/design_types.py +67 -29
  60. pyedb/generic/general_methods.py +0 -120
  61. pyedb/generic/process.py +44 -108
  62. pyedb/generic/settings.py +75 -19
  63. pyedb/grpc/database/components.py +2 -0
  64. pyedb/grpc/database/control_file.py +5 -5
  65. pyedb/grpc/database/definition/materials.py +1 -1
  66. pyedb/grpc/database/definition/package_def.py +3 -3
  67. pyedb/grpc/database/definition/padstack_def.py +53 -0
  68. pyedb/grpc/database/geometry/polygon_data.py +1 -1
  69. pyedb/grpc/database/layout/layout.py +8 -5
  70. pyedb/grpc/database/layout_validation.py +3 -3
  71. pyedb/grpc/database/modeler.py +9 -4
  72. pyedb/grpc/database/net/net.py +15 -14
  73. pyedb/grpc/database/nets.py +70 -0
  74. pyedb/grpc/database/padstacks.py +35 -17
  75. pyedb/grpc/database/primitive/padstack_instance.py +175 -7
  76. pyedb/grpc/database/siwave.py +1 -1
  77. pyedb/grpc/database/source_excitations.py +2 -4
  78. pyedb/grpc/database/stackup.py +1 -1
  79. pyedb/grpc/database/terminal/bundle_terminal.py +1 -1
  80. pyedb/grpc/database/terminal/padstack_instance_terminal.py +1 -1
  81. pyedb/grpc/database/terminal/pingroup_terminal.py +1 -1
  82. pyedb/grpc/database/utility/xml_control_file.py +5 -5
  83. pyedb/grpc/edb.py +73 -27
  84. pyedb/grpc/edb_init.py +3 -3
  85. pyedb/grpc/rpc_session.py +10 -10
  86. pyedb/libraries/common.py +366 -0
  87. pyedb/libraries/rf_libraries/base_functions.py +1358 -0
  88. pyedb/libraries/rf_libraries/planar_antennas.py +628 -0
  89. pyedb/misc/decorators.py +61 -0
  90. pyedb/misc/misc.py +0 -13
  91. pyedb/siwave.py +2 -2
  92. {pyedb-0.54.0.dist-info → pyedb-0.55.0.dist-info}/METADATA +1 -2
  93. {pyedb-0.54.0.dist-info → pyedb-0.55.0.dist-info}/RECORD +95 -91
  94. {pyedb-0.54.0.dist-info → pyedb-0.55.0.dist-info}/WHEEL +0 -0
  95. {pyedb-0.54.0.dist-info → pyedb-0.55.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1358 @@
1
+ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates.
2
+ # SPDX-License-Identifier: MIT
3
+ #
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ from __future__ import annotations
24
+
25
+ import math
26
+ from typing import List, Optional, Tuple, Union
27
+
28
+ from pyedb import Edb
29
+ from pyedb.libraries.common import Substrate
30
+
31
+
32
+ class HatchGround:
33
+ """
34
+ Create a square demo board whose ground layer is made of an
35
+ orthogonal hatched copper pattern. Any requested copper fill
36
+ ratio between 10 % and 90 % can be realised.
37
+
38
+ Parameters
39
+ ----------
40
+ pitch : float, default 17.07 mm
41
+ Centre-to-centre distance of the hatch bars [m].
42
+ width : float, default 5 mm
43
+ Width of each copper bar [m].
44
+ fill_target : float, default 50 %
45
+ Desired copper area in percent (10 … 90).
46
+ board_size : float, default 100 mm
47
+ Edge length of the square board [m].
48
+ layer_gnd : str, default "GND"
49
+ Name of the layer that receives the hatch pattern.
50
+
51
+ Examples
52
+ --------
53
+ >>> hatch = HatchGround(pitch=0.5e-3, width=0.2e-3,
54
+ ... fill_target=70, board_size=5e-3)
55
+ >>> edb = Edb("demo.aedb")
56
+ >>> hatch._edb = edb
57
+ >>> hatch.create()
58
+ >>> round(hatch.copper_fill_ratio, 1)
59
+ 70.0
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ edb_cell: Optional[Edb] = None,
65
+ pitch: Union[str, float] = 17.07e-3,
66
+ width: Union[str, float] = 5.0e-3,
67
+ fill_target: Union[str, float] = 50.0,
68
+ board_size: Union[str, float] = 100e-3,
69
+ layer_gnd: str = "GND",
70
+ ):
71
+ """Initialize the hatch ground object."""
72
+ self._edb = edb_cell
73
+ self.pitch = self._edb.value(pitch)
74
+ self.width = self._edb.value(width)
75
+ self.fill_target = self._edb.value(fill_target)
76
+ self.board_size = self._edb.value(board_size)
77
+ self.layer_gnd = layer_gnd
78
+ self._outline = None
79
+
80
+ @property
81
+ def copper_fill_ratio(self) -> float:
82
+ """
83
+ Return the **actual** copper fill ratio in percent.
84
+
85
+ Returns
86
+ -------
87
+ float
88
+ Percentage of the board area that is copper after the hatch
89
+ has been generated.
90
+ """
91
+ cu_area = self._edb.modeler.polygons[0].area()
92
+ return 100.0 * cu_area / (self.board_size**2)
93
+
94
+ def _generate_hatch(self) -> None:
95
+ """Draw orthogonal stripes, then punch gaps for the requested fill."""
96
+ # ---------- horizontal bars ----------
97
+ y = 0.0
98
+ while y < self.board_size:
99
+ self._add_stripe(0.0, y, self.board_size, y + self.width)
100
+ y += self.pitch
101
+
102
+ # ---------- vertical bars ------------
103
+ x = 0.0
104
+ while x < self.board_size:
105
+ self._add_stripe(x, 0.0, x + self.width, self.board_size)
106
+ x += self.pitch
107
+
108
+ # ---------- punch square gaps --------
109
+ gaps: List[List[Tuple[float, float]]] = []
110
+ x = 0.0
111
+ while x < self.board_size:
112
+ y = 0.0
113
+ while y < self.board_size:
114
+ gaps.append(
115
+ [
116
+ (x, y),
117
+ (x + self.width, y),
118
+ (x + self.width, y + self.width),
119
+ (x, y + self.width),
120
+ (x, y),
121
+ ]
122
+ )
123
+ y += self.pitch
124
+ x += self.pitch
125
+ polygons = self._edb.modeler.polygons
126
+ polygons[0].unite(polygons[1:])
127
+
128
+ def _add_stripe(self, x0: float, y0: float, x1: float, y1: float) -> None:
129
+ """Create one rectangular copper bar on the GND layer."""
130
+ points = [(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)]
131
+ self._edb.modeler.create_polygon(points, layer_name=self.layer_gnd, net_name="GND")
132
+
133
+ def create(self) -> bool:
134
+ """
135
+ Generate the stack-up, board outline and hatch pattern.
136
+
137
+ Returns
138
+ -------
139
+ bool
140
+ True when geometry has been created successfully.
141
+ """
142
+ self._generate_hatch()
143
+ return True
144
+
145
+
146
+ class Meander:
147
+ """
148
+ Fully-parametric micro-strip meander line.
149
+
150
+ Parameters
151
+ ----------
152
+ pitch : float, default 1 mm
153
+ Vertical spacing between successive meander rows [m].
154
+ trace_width : float, default 0.3 mm
155
+ Width of the micro-strip [m].
156
+ amplitude : float, default 5 mm
157
+ Horizontal excursion of each U-turn [m].
158
+ num_turns : int, default 8
159
+ Number of 180° bends.
160
+ layer : str, default "TOP"
161
+ EDB metal layer.
162
+ net : str, default "SIG"
163
+ Net name assigned to the trace.
164
+
165
+ Examples
166
+ --------
167
+ >>> m = Meander(pitch=0.2e-3, trace_width=0.15e-3,
168
+ ... amplitude=2e-3, num_turns=4)
169
+ >>> edb = Edb("meander.aedb")
170
+ >>> m._pedb = edb
171
+ >>> m.create()
172
+ >>> f"{m.analytical_z0:.1f} Ω"
173
+ '50.1 Ω'
174
+ >>> m.electrical_length_deg(1e9)
175
+ 59.8
176
+ """
177
+
178
+ def __init__(
179
+ self,
180
+ edb_cell: Edb,
181
+ pitch: Union[str, float] = 1e-3,
182
+ trace_width: Union[str, float] = 0.3e-3,
183
+ amplitude: Union[str, float] = 5e-3,
184
+ num_turns: int = 8,
185
+ layer: str = "TOP",
186
+ net: str = "SIG",
187
+ ):
188
+ self._edb = edb_cell
189
+ self.pitch = self._edb.value(pitch)
190
+ self.trace_width = self._edb.value(trace_width)
191
+ self.amplitude = self._edb.value(amplitude)
192
+ self.num_turns = num_turns
193
+ self.layer = layer
194
+ self.net = net
195
+ self.substrate = Substrate()
196
+ self.length = 0.0
197
+
198
+ # ------------------------------------------------------------------ #
199
+ # Analytical models
200
+ # ------------------------------------------------------------------ #
201
+ @property
202
+ def analytical_z0(self) -> float:
203
+ """
204
+ Micro-strip characteristic impedance using the Hammerstad & Jensen
205
+ closed-form expression.
206
+
207
+ Returns
208
+ -------
209
+ float
210
+ Z0 in Ohm.
211
+ """
212
+ return 60 / math.sqrt(self.substrate.er) * math.log(5.98 * 1.6e-3 / (0.8 * self.trace_width + self.trace_width))
213
+
214
+ def electrical_length_deg(self, freq: float) -> float:
215
+ """
216
+ Electrical length of the meander at the specified frequency.
217
+
218
+ Parameters
219
+ ----------
220
+ freq : float
221
+ Frequency in Hz.
222
+
223
+ Returns
224
+ -------
225
+ float
226
+ Phase shift in degrees.
227
+ """
228
+ c = 299_792_458
229
+ v = c / math.sqrt(self.substrate.er)
230
+ beta = 2 * math.pi * freq / v
231
+ return math.degrees(beta * self.length)
232
+
233
+ # ------------------------------------------------------------------ #
234
+ # EDB creation
235
+ # ------------------------------------------------------------------ #
236
+ def create(self) -> bool:
237
+ """
238
+ Draw the meander in the attached EDB cell and calculate its
239
+ physical length.
240
+
241
+ Returns
242
+ -------
243
+ bool
244
+ True on success.
245
+ """
246
+
247
+ # Parameters
248
+ self._edb.add_design_variable("w", self.trace_width) # trace width
249
+ self._edb.add_design_variable("p", self.pitch) # pitch (centre-to-centre)
250
+ self._edb.add_design_variable("a", self.amplitude) # meander amplitude
251
+ self._edb.add_design_variable("n_turns", self.num_turns) # number of U-turns (integer)
252
+
253
+ # ----------------------------------------------------------
254
+ # 3. Build the point list
255
+ # ----------------------------------------------------------
256
+ pts = [(0, 0)] # start on the axis
257
+
258
+ for i in range(self.num_turns):
259
+ y = f"{i + 1}*p" # next row
260
+ if i % 2 == 0: # even → left
261
+ pts.extend([("-a/2", f"{i}*p"), ("-a/2", y), (0, y)]) # step left # vertical # back to axis
262
+ else: # odd → right
263
+ pts.extend([(" a/2", f"{i}*p"), (" a/2", y), (0, y)]) # step right # vertical # back to axis
264
+
265
+ self._edb.modeler.create_trace(
266
+ path_list=pts, layer_name=self.layer, width=self.trace_width, net_name=self.net, corner_style="Round"
267
+ )
268
+ self.length = self._edb.modeler.paths[0].length
269
+
270
+
271
+ class MIMCapacitor:
272
+ """
273
+ Metal–Insulator–Metal parallel-plate capacitor.
274
+
275
+ Parameters
276
+ ----------
277
+ area : float, default 0.1 mm²
278
+ Plate area [m²].
279
+ gap : float, default 1 µm
280
+ Dielectric thickness between plates [m].
281
+ er : float, default 7
282
+ Relative permittivity of the dielectric.
283
+ layer_top : str, default "M1"
284
+ Top plate layer.
285
+ layer_bottom : str, default "M2"
286
+ Bottom plate layer.
287
+ net : str, default "RF"
288
+ Net name for both plates.
289
+
290
+ Examples
291
+ --------
292
+ >>> cap = MIMCapacitor(area=200e-12, gap=0.5e-6, er=4.1)
293
+ >>> edb = Edb("mim.aedb")
294
+ >>> cap._pedb = edb
295
+ >>> cap.create()
296
+ >>> f"{cap.capacitance_f*1e12:.2f} pF"
297
+ '1.45 pF'
298
+ """
299
+
300
+ def __init__(
301
+ self,
302
+ edb_cell: Edb,
303
+ area: Union[str, float] = 0.1e-6,
304
+ gap: Union[str, float] = 1e-6,
305
+ layer_top: str = "M1",
306
+ layer_bottom: str = "M2",
307
+ net: str = "RF",
308
+ ):
309
+ self._edb = edb_cell
310
+ self.area = self._edb.value(area)
311
+ self.gap = self._edb.value(gap)
312
+ self.layer_top = layer_top
313
+ self.layer_bottom = layer_bottom
314
+ self.net = net
315
+ self.substrate = Substrate()
316
+
317
+ @property
318
+ def capacitance_f(self) -> float:
319
+ """
320
+ Analytical parallel-plate capacitance.
321
+
322
+ Returns
323
+ -------
324
+ float
325
+ Capacitance in Farads.
326
+ """
327
+ eps0 = 8.854e-12
328
+ return eps0 * self.substrate.er * self.area / self.gap
329
+
330
+ def create(self) -> bool:
331
+ """
332
+ Create the top plate, bottom plate and assign variables.
333
+
334
+ Returns
335
+ -------
336
+ bool
337
+ True on success.
338
+ """
339
+ self._edb["area"] = self.area
340
+ self._edb["gap"] = self.gap
341
+ side = math.sqrt(self.area)
342
+ self._edb["side"] = side
343
+
344
+ self._edb.modeler.create_rectangle(self.layer_top, self.net, [0, -side / 2, side, side])
345
+ self._edb.modeler.create_rectangle(self.layer_bottom, self.net, [0, -side / 2, side, side])
346
+ return True
347
+
348
+
349
+ class SpiralInductor:
350
+ """
351
+ Square spiral inductor with an optional under-pass bridge.
352
+
353
+ Parameters
354
+ ----------
355
+ turns : float, default 4.5
356
+ Number of half-turns (4.5 = 4 full turns + 1 half turn).
357
+ trace_width : float, default 20 µm
358
+ Width of the spiral trace.
359
+ spacing : float, default 12 µm
360
+ Gap between successive turns.
361
+ inner_diameter : float, default 60 µm
362
+ Side length of the innermost square.
363
+ layer : str, default "M1"
364
+ Layer on which the spiral is drawn.
365
+ bridge_layer : str, default "M2"
366
+ Layer used for the under-pass.
367
+ via_layer : str, default "M3"
368
+ Via layer connecting spiral end to the under-pass.
369
+ net : str, default "IN"
370
+ Net name.
371
+ inductor_center : tuple[float, float], default (0, 0)
372
+ Absolute centre coordinates of the structure.
373
+ via_size : float, default 25 µm
374
+ Side length of the square via pad.
375
+ bridge_width : float, default 12 µm
376
+ Width of the under-pass trace.
377
+ bridge_clearance : float, default 6 µm
378
+ Dielectric clearance under the bridge.
379
+ bridge_length : float, default 200 µm
380
+ Length of the under-pass beyond the via.
381
+ ground_layer : str, default "GND"
382
+ Layer on which the ground plane is drawn.
383
+
384
+ Examples
385
+ --------
386
+ >>> sp = SpiralInductor(turns=3.5, trace_width=25e-6,
387
+ ... inner_diameter=80e-6)
388
+ >>> edb = Edb("spiral.aedb")
389
+ >>> sp._pedb = edb
390
+ >>> sp.create()
391
+ >>> f"{sp.inductance_nh:.1f} nH"
392
+ '3.4 nH'
393
+ """
394
+
395
+ def __init__(
396
+ self,
397
+ edb_cell: Optional[Edb] = None,
398
+ turns: Union[int, float] = 4.5,
399
+ trace_width: Union[str, float] = 20e-6,
400
+ spacing: Union[str, float] = 12e-6,
401
+ inner_diameter: Union[str, float] = 60e-6,
402
+ layer: str = "M1",
403
+ bridge_layer: str = "M2",
404
+ via_layer: str = "M3",
405
+ net: str = "IN",
406
+ inductor_center: Tuple[Union[str, float], Union[str, float]] = (0, 0), # centre of spiral
407
+ via_size: Union[str, float] = 25e-6, # via metal pad
408
+ bridge_width: Union[str, float] = 12e-6, # under-pass trace width
409
+ bridge_clearance: Union[str, float] = 6e-6, # dielectric gap under bridge
410
+ bridge_length: Union[str, float] = 200e-6, # how far the bridge extends
411
+ ground_layer: str = "GND",
412
+ ):
413
+ self._edb = edb_cell
414
+ self.turns = turns # half-turns → 4.5 = 4 full + 1 half
415
+ self.trace_width = self._edb.value(trace_width)
416
+ self.spacing = self._edb.value(spacing)
417
+ self.inner_diameter = self._edb.value(inner_diameter) # first inner square side
418
+ self.via_size = self._edb.value(via_size) # centre via finished hole
419
+ self.inductor_center = [self._edb.value(v) for v in inductor_center] # via centre position
420
+ self.bridge_width = self._edb.value(bridge_width) # under-pass trace width
421
+ self.bridge_clearance = self._edb.value(bridge_clearance) # dielectric gap under bridge
422
+ self.bridge_length = self._edb.value(bridge_length) # how far the bridge extends
423
+ self.layer = layer
424
+ self.bridge_layer = bridge_layer
425
+ self.via_layer = via_layer # layer for the centre via
426
+ self.substrate = Substrate() # default substrate
427
+ self.net_name = net
428
+ self.ground_layer = ground_layer
429
+
430
+ @property
431
+ def inductance_nh(self) -> float:
432
+ """
433
+ Accurate inductance calculated with the improved Wheeler formula
434
+ for square spirals.
435
+
436
+ Returns
437
+ -------
438
+ float
439
+ Inductance in nano-Henries.
440
+ """
441
+ w = self.trace_width
442
+ s = self.spacing
443
+ N = self.turns
444
+ d_in = self.inner_diameter
445
+ d_out = d_in + 4 * N * (w + s) # outer side length
446
+
447
+ # 2. Parameters for the improved Wheeler formula (square)
448
+ d_avg = (d_out + d_in) / 2.0
449
+ rho = (d_out - d_in) / (d_out + d_in)
450
+
451
+ C1, C2, C3, C4 = 1.27, 2.07, 0.18, 0.13 # square coefficients
452
+ mu0 = 4e-7 * math.pi # H·m⁻¹
453
+
454
+ # 3. Wheeler formula in SI
455
+ L_h = (mu0 * N**2 * d_avg * C1 / 2.0) * (math.log(C2 / rho) + C3 * rho**2 + C4 * rho**3)
456
+
457
+ return L_h * 1e9 # → nH
458
+
459
+ def create(self) -> bool:
460
+ cx = self.inductor_center[0]
461
+ cy = self.inductor_center[1]
462
+ pts = []
463
+ x, y = cx, cy
464
+ side = self.inner_diameter
465
+ direction = 0 # 0=+y, 1=-x, 2=-y, 3=+x (vertical first)
466
+
467
+ for i in range(int(math.ceil(2 * self.turns))):
468
+ # choose direction and length
469
+ if direction == 0:
470
+ dx, dy = 0, side / 2
471
+ elif direction == 1:
472
+ dx, dy = -side / 2, 0
473
+ elif direction == 2:
474
+ dx, dy = 0, -side / 2
475
+ else:
476
+ dx, dy = side / 2, 0
477
+
478
+ x += dx
479
+ y += dy
480
+ pts.append((x, y))
481
+
482
+ direction = (direction + 1) % 4
483
+ if i % 2 == 1: # every two sides increase pitch
484
+ side += 2 * (self.trace_width + self.spacing)
485
+
486
+ via_pos = pts[0]
487
+ bridge_dir = -1 # -1 = down, +1 = up
488
+ bridge_end = (via_pos[0] + bridge_dir * self.bridge_length, via_pos[1])
489
+
490
+ self._edb.modeler.create_trace(
491
+ path_list=pts,
492
+ layer_name=self.layer,
493
+ width=self.trace_width,
494
+ net_name=self.net_name,
495
+ start_cap_style="Flat",
496
+ end_cap_style="Flat",
497
+ )
498
+
499
+ # Centre via (spiral inner end → bottom metal)
500
+ self._edb.modeler.create_rectangle(
501
+ layer_name=self.via_layer,
502
+ center_point=pts[0],
503
+ width=self.via_size,
504
+ height=self.via_size,
505
+ representation_type="CenterWidthHeight",
506
+ net_name=self.net_name,
507
+ )
508
+
509
+ # bridge trace on bottom metal
510
+ self._edb.modeler.create_trace(
511
+ path_list=[via_pos, bridge_end],
512
+ layer_name=self.bridge_layer,
513
+ width=self.bridge_width,
514
+ net_name=self.net_name,
515
+ start_cap_style="Flat",
516
+ end_cap_style="Flat",
517
+ )
518
+
519
+ # ground plane
520
+ self._edb.modeler.create_rectangle(
521
+ layer_name=self.ground_layer,
522
+ center_point=self.inductor_center,
523
+ width=self.substrate.size[0],
524
+ height=self.substrate.size[1],
525
+ representation_type="CenterWidthHeight",
526
+ net_name="ref",
527
+ )
528
+ return True
529
+
530
+
531
+ class CPW:
532
+ """
533
+ Coplanar waveguide with side ground planes.
534
+
535
+ Parameters
536
+ ----------
537
+ length : float, default 1 mm
538
+ Physical length of the line.
539
+ width : float, default 0.3 mm
540
+ Width of the centre conductor.
541
+ gap : float, default 0.1 mm
542
+ Gap between centre conductor and ground planes.
543
+ layer : str, default "TOP"
544
+ Layer on which the CPW is drawn.
545
+ ground_net : str, default "GND"
546
+ Net name for the ground planes.
547
+ ground_width : float, default 0.1 mm
548
+ Width of the side ground strips.
549
+ ground_layer : str, default "GND"
550
+ Layer for the underlying ground plane.
551
+ net : str, default "SIG"
552
+ Net name for the centre conductor.
553
+ substrate : Substrate, default 100 µm FR4
554
+ Substrate definition.
555
+
556
+ Examples
557
+ --------
558
+ >>> cpw = CPW(length=5e-3, width=0.4e-3, gap=0.2e-3)
559
+ >>> edb = Edb("cpw.aedb")
560
+ >>> cpw._pedb = edb
561
+ >>> cpw.create()
562
+ >>> f"{cpw.analytical_z0:.1f} Ω"
563
+ '46.5 Ω'
564
+ """
565
+
566
+ def __init__(
567
+ self,
568
+ edb_cell: Optional[Edb] = None,
569
+ length: Union[str, float] = 1e-3,
570
+ width: Union[str, float] = 0.3e-3,
571
+ gap: Union[str, float] = 0.1e-3,
572
+ layer: str = "TOP",
573
+ ground_net: str = "GND",
574
+ ground_width: float = 0.1e-3,
575
+ ground_layer: str = "GND",
576
+ net: str = "SIG",
577
+ substrate: Substrate = Substrate(100e-6, 4.4, 0.02, "SUB", (0.001, 0.001)),
578
+ ):
579
+ self._edb = edb_cell
580
+ self.length = self._edb.value(length)
581
+ self.width = self._edb.value(width)
582
+ self.gap = self._edb.value(gap)
583
+ self.layer = layer
584
+ self.ground_net = ground_net
585
+ self.ground_width = ground_width # width of ground plane
586
+ self.ground_layer = ground_layer # layer for the ground plane
587
+ self.net = net
588
+ self.substrate = substrate
589
+
590
+ @property
591
+ def analytical_z0(self) -> float:
592
+ """
593
+ Characteristic impedance obtained with the conformal-mapping
594
+ formula for CPW.
595
+
596
+ Returns
597
+ -------
598
+ float
599
+ Z0 in Ohm.
600
+ """
601
+ a = self.width / 2
602
+ b = a + self.gap
603
+ k = a / b
604
+ kpr = math.sqrt(1 - k**2)
605
+ # Complete elliptic integral ratio approx.
606
+ k_ratio = math.pi / (2 * math.log(2 * (1 + math.sqrt(kpr)) / (1 - math.sqrt(kpr))))
607
+ return 30 * math.pi / math.sqrt(self.substrate.er) * k_ratio
608
+
609
+ def create(self) -> bool:
610
+ """
611
+ Draw the centre strip, side grounds and bottom ground plane.
612
+
613
+ Returns
614
+ -------
615
+ bool
616
+ True on success.
617
+ """
618
+ self._edb["l"] = self.length
619
+ self._edb["w"] = self.width
620
+ self._edb["g"] = self.gap
621
+
622
+ # signal
623
+ self._edb.modeler.create_rectangle(
624
+ self.layer,
625
+ net_name=self.net,
626
+ lower_left_point=[-self.width / 2, 0],
627
+ upper_right_point=[self.width / 2, self.length],
628
+ )
629
+ # grounds
630
+ self._edb.modeler.create_rectangle(
631
+ self.layer,
632
+ net_name=self.ground_net,
633
+ lower_left_point=[-self.width / 2 - self.gap - self.ground_width, 0],
634
+ upper_right_point=[-self.width / 2 - self.gap, self.length],
635
+ )
636
+ self._edb.modeler.create_rectangle(
637
+ self.layer,
638
+ net_name=self.ground_net,
639
+ lower_left_point=[self.width / 2 + self.gap, 0],
640
+ upper_right_point=[self.width / 2 + self.gap + self.ground_width, self.length],
641
+ )
642
+ self._edb.modeler.create_rectangle(
643
+ self.ground_layer,
644
+ lower_left_point=[-self.width / 2 - self.gap - self.ground_width, 0],
645
+ upper_right_point=[self.width / 2 + self.gap + self.ground_width, self.length],
646
+ )
647
+ return True
648
+
649
+
650
+ class RadialStub:
651
+ """
652
+ Radial (fan) open stub for RF matching.
653
+
654
+ Parameters
655
+ ----------
656
+ radius : float, default 500 µm
657
+ Radius of the fan [m].
658
+ angle_deg : float, default 60°
659
+ Opening angle of the sector.
660
+ width : float, default 0.2 mm
661
+ Width of the feeding micro-strip line [m].
662
+ layer : str, default "TOP"
663
+ Metal layer.
664
+ net : str, default "RF"
665
+ Net name.
666
+
667
+ Examples
668
+ --------
669
+ >>> stub = RadialStub(radius=1e-3, angle_deg=90)
670
+ >>> edb = Edb("radial.aedb")
671
+ >>> stub._pedb = edb
672
+ >>> stub.create()
673
+ >>> f"{stub.electrical_length_deg(2e9):.1f}°"
674
+ '108.0°'
675
+ """
676
+
677
+ def __init__(
678
+ self,
679
+ edb_cell,
680
+ radius: Union[str, float] = 500e-6,
681
+ angle_deg: Union[str, float] = 60,
682
+ width: Union[str, float] = 0.2e-3,
683
+ layer: str = "TOP",
684
+ net: str = "RF",
685
+ ):
686
+ self._edb = edb_cell
687
+ self.radius = self._edb.value(radius)
688
+ self.angle_deg = self._edb.value(angle_deg)
689
+ self.width = self._edb.value(width)
690
+ self.layer = layer
691
+ self.net = net
692
+ self.substrate: Substrate = Substrate(100e-6, 4.4, 0.02, "SUB", (0.001, 0.001))
693
+
694
+ @property
695
+ def electrical_length_deg(self, freq: float = 2e9) -> float:
696
+ """
697
+ Electrical length of the radial stub at a given frequency.
698
+
699
+ Parameters
700
+ ----------
701
+ freq : float, default 2 GHz
702
+ Frequency in Hz.
703
+
704
+ Returns
705
+ -------
706
+ float
707
+ Phase shift in degrees contributed by the stub.
708
+ """
709
+ c = 299_792_458
710
+ v = c / math.sqrt(self.substrate.er)
711
+ beta = 2 * math.pi * freq / v
712
+ return math.degrees(beta * self.radius)
713
+
714
+ def create(self) -> bool:
715
+ """
716
+ Draw the fan-shaped polygon and the feeding line.
717
+
718
+ Returns
719
+ -------
720
+ bool
721
+ True on success.
722
+ """
723
+ self._edb["r"] = self.radius
724
+ self._edb["ang"] = self.angle_deg
725
+ self._edb["w"] = self.width
726
+
727
+ # Create wedge polygon
728
+ theta = math.radians(self.angle_deg)
729
+ pts = [
730
+ [0, 0],
731
+ [self.radius * math.cos(-theta / 2), self.radius * math.sin(-theta / 2)],
732
+ [self.radius * math.cos(theta / 2), self.radius * math.sin(theta / 2)],
733
+ ]
734
+ self._edb.modeler.create_polygon(main_shape=pts, layer_name=self.layer, net_name=self.net)
735
+ # feed
736
+ self._edb.modeler.create_rectangle(
737
+ layer_name=self.layer,
738
+ net_name=self.net,
739
+ lower_left_point=[0, -self.width / 2],
740
+ upper_right_point=[self.width, self.width / 2],
741
+ )
742
+ return True
743
+
744
+
745
+ class RatRace:
746
+ """
747
+ 180° rat-race (ring) hybrid coupler.
748
+
749
+ Parameters
750
+ ----------
751
+ z0 : float, default 50 Ω
752
+ Characteristic impedance of the ring.
753
+ freq : float, default 10 GHz
754
+ Centre frequency.
755
+ layer : str, default "TOP"
756
+ Layer on which the ring is drawn.
757
+ bottom_layer : str | None
758
+ Layer for the ground plane (if None, no ground is drawn).
759
+ net : str, default "RR"
760
+ Net name.
761
+ width : float, default 0.2 mm
762
+ Micro-strip width for the ring and port stubs.
763
+ nr_segments : int, default 32
764
+ Number of straight segments per 90° arc.
765
+
766
+ Examples
767
+ --------
768
+ >>> rr = RatRace(freq=5e9)
769
+ >>> edb = Edb("ratrace.aedb")
770
+ >>> rr._pedb = edb
771
+ >>> rr.create()
772
+ >>> f"{rr.circumference*1e3:.2f} mm"
773
+ '45.00 mm'
774
+ """
775
+
776
+ def __init__(
777
+ self,
778
+ edb_cell: Optional[Edb] = None,
779
+ z0: Union[float, str] = 50,
780
+ freq: Union[float, str] = 10e9,
781
+ layer: str = "TOP",
782
+ bottom_layer: Optional[str] = None,
783
+ net: str = "RR",
784
+ width: Union[float, str] = 0.2e-3,
785
+ nr_segments: int = 32,
786
+ ):
787
+ self._edb = edb_cell
788
+ self.z0 = self._edb.value(z0)
789
+ self.freq = self._edb.value(freq)
790
+ self.layer = layer
791
+ self.bottom_layer = bottom_layer
792
+ self.net = net
793
+ self.width = self._edb.value(width)
794
+ self.nr_segments = nr_segments
795
+ self.substrate = Substrate()
796
+
797
+ @property
798
+ def circumference(self) -> float:
799
+ """
800
+ Physical circumference of the ring.
801
+
802
+ Returns
803
+ -------
804
+ float
805
+ Circumference in metres (1.5 guided wavelengths).
806
+ """
807
+ c = 299_792_458
808
+ v = c / math.sqrt(self.substrate.er)
809
+ return 1.5 * v / self.freq
810
+
811
+ @property
812
+ def radius(self) -> float:
813
+ """
814
+ Mean radius of the ring.
815
+
816
+ Returns
817
+ -------
818
+ float
819
+ Radius in metres.
820
+ """
821
+ return self.circumference / (2 * math.pi)
822
+
823
+ # ------------------------------------------------------------------
824
+ # Geometry builders
825
+ # ------------------------------------------------------------------
826
+ def _arc_points(
827
+ self,
828
+ centre: Tuple[float, float],
829
+ radius: float,
830
+ start_angle: float, # rad
831
+ delta_angle: float, # rad
832
+ ) -> List[Tuple[float, float]]:
833
+ """Return a list of (x,y) for a discretised arc."""
834
+ points = []
835
+ step = delta_angle / self.nr_segments
836
+ for i in range(self.nr_segments + 1):
837
+ ang = start_angle + i * step
838
+ x = centre[0] + radius * math.cos(ang)
839
+ y = centre[1] + radius * math.sin(ang)
840
+ points.append((x, y))
841
+ return points
842
+
843
+ def _port_stub(self, start: Tuple[float, float], length: float, angle: float):
844
+ """Return a two-point list for a straight stub."""
845
+ dx = length * math.cos(angle)
846
+ dy = length * math.sin(angle)
847
+ return [start, (start[0] + dx, start[1] + dy)]
848
+
849
+ # ------------------------------------------------------------------
850
+ # Main creation routine
851
+ # ------------------------------------------------------------------
852
+ def create(self) -> bool:
853
+ """
854
+ Draw the discretised ring and four 50 Ω port stubs.
855
+
856
+ Returns
857
+ -------
858
+ bool
859
+ True on success.
860
+ """
861
+ self._edb["c"] = self.circumference
862
+ self._edb["r"] = self.radius
863
+ self._edb["w"] = self.width
864
+
865
+ r = self.radius
866
+ w = self.width
867
+ stub_len = 1e-3 # 1 mm straight sections at the four ports
868
+
869
+ # Build the ring as four connected arcs
870
+ # ----------------------------------------------------------------------
871
+ # We go counter-clockwise starting at the right-hand port (0°, port 1).
872
+ #
873
+ # port 1 (0°) -------- arc A (90°) ------- port 2 (90°)
874
+ # | |
875
+ # | |
876
+ # | 270° arc D |
877
+ # | |
878
+ # port 4 (270°) ----- arc C (90°) ------ port 3 (180°)
879
+
880
+ # Arc A: 0° -> 90°
881
+ pts_a = self._arc_points((0, 0), r, math.radians(0), math.radians(90))
882
+
883
+ # Arc B: 90° -> 180°
884
+ pts_b = self._arc_points((0, 0), r, math.radians(90), math.radians(90))
885
+
886
+ # Arc C: 180° -> 270°
887
+ pts_c = self._arc_points((0, 0), r, math.radians(180), math.radians(90))
888
+
889
+ # Arc D: 270° -> 360° (=0°) (the long 270° section)
890
+ pts_d = self._arc_points((0, 0), r, math.radians(270), math.radians(90))
891
+
892
+ # Stitch them together into a single closed path
893
+ ring_points = pts_a + pts_b[1:] + pts_c[1:] + pts_d[1:]
894
+ self._edb.modeler.create_trace(
895
+ path_list=ring_points,
896
+ layer_name=self.layer,
897
+ net_name=self.net,
898
+ width=w,
899
+ )
900
+
901
+ # Add the four 50 Ω port stubs
902
+ # ----------------------------------------------------------------------
903
+ # Angles of the ports on the ring
904
+ port_angles = [0, 90, 180, 270] # degrees
905
+
906
+ for idx, ang_deg in enumerate(port_angles, start=1):
907
+ ang = math.radians(ang_deg)
908
+ x_ring = r * math.cos(ang)
909
+ y_ring = r * math.sin(ang)
910
+
911
+ # Direction vector pointing outwards
912
+ stub_pts = self._port_stub((x_ring, y_ring), stub_len, ang)
913
+ self._edb.modeler.create_trace(
914
+ path_list=stub_pts,
915
+ layer_name=self.layer,
916
+ net_name=f"{self.net}_P{idx}",
917
+ width=w,
918
+ start_cap_style="flat",
919
+ end_cap_style="flat",
920
+ )
921
+
922
+ self._edb.modeler.create_rectangle(
923
+ lower_left_point=[-self.circumference / 4, -self.circumference / 4],
924
+ upper_right_point=[self.circumference / 4, self.circumference / 4],
925
+ layer_name=self.bottom_layer,
926
+ net_name=self.net,
927
+ )
928
+ return True
929
+
930
+
931
+ class InterdigitalCapacitor:
932
+ """
933
+ Inter-digitated comb capacitor with fully parametric fingers.
934
+
935
+ All dimensions are stored as native EDB variables so they remain
936
+ editable inside AEDT after the library cell is imported.
937
+
938
+ Parameters
939
+ ----------
940
+ fingers : int, default 8
941
+ Number of finger pairs.
942
+ finger_length : str, default "0.9 mm"
943
+ Length of each finger (string with units).
944
+ finger_width : str, default "0.08 mm"
945
+ Width of each finger.
946
+ gap : str, default "0.04 mm"
947
+ Gap between adjacent fingers.
948
+ comb_gap : str, default "0.06 mm"
949
+ Gap between the two combs at the open end.
950
+ bus_width : str, default "0.25 mm"
951
+ Width of the top/bottom bus bars.
952
+ layer : str, default "TOP"
953
+ Layer on which the capacitor is drawn.
954
+ net_a : str, default "PORT1"
955
+ Net for the bottom comb.
956
+ net_b : str, default "PORT2"
957
+ Net for the top comb.
958
+
959
+ Examples
960
+ --------
961
+ >>> idc = InterdigitalCapacitor(fingers=10,
962
+ ... finger_length="0.5mm",
963
+ ... gap="0.03mm")
964
+ >>> edb = Edb("idc.aedb")
965
+ >>> idc._pedb = edb
966
+ >>> idc.create()
967
+ >>> f"{idc.capacitance_pf:.2f} pF"
968
+ '0.74 pF'
969
+ """
970
+
971
+ VAR_PREFIX = "IDC" # prefix for every EDB variable
972
+
973
+ def __init__(
974
+ self,
975
+ edb_cell: Optional[Edb] = None,
976
+ fingers: int = 8,
977
+ finger_length: Union[float, str] = "0.9mm",
978
+ finger_width: Union[float, str] = "0.08mm",
979
+ gap: Union[float, str] = "0.04mm",
980
+ comb_gap: Union[float, str] = "0.06mm",
981
+ bus_width: Union[float, str] = "0.25mm",
982
+ layer: str = "TOP",
983
+ net_a: str = "PORT1",
984
+ net_b: str = "PORT2",
985
+ ):
986
+ self._edb = edb_cell
987
+ self.layer = layer
988
+ self.net_a = net_a
989
+ self.net_b = net_b
990
+ self.substrate = Substrate()
991
+
992
+ pfx = self.VAR_PREFIX
993
+ self._vars = {
994
+ "fingers": int(fingers),
995
+ "finger_length": self._edb.value(finger_length),
996
+ "finger_width": self._edb.value(finger_width),
997
+ "gap": self._edb.value(gap),
998
+ "comb_gap": self._edb.value(comb_gap),
999
+ "bus_width": self._edb.value(bus_width),
1000
+ }
1001
+ for k, v in self._vars.items():
1002
+ var_name = f"{pfx}_{k}"
1003
+ # Use design variable (no $ prefix) -> stored in cell
1004
+ self._edb[var_name] = v
1005
+
1006
+ @property
1007
+ def capacitance_pf(self) -> float:
1008
+ """
1009
+ Quick parallel-plate estimate of the total capacitance.
1010
+
1011
+ Returns
1012
+ -------
1013
+ float
1014
+ Capacitance in pico-Farads.
1015
+ """
1016
+ pfx = self.VAR_PREFIX
1017
+ eps0 = 8.854e-12
1018
+ er = self.substrate.er
1019
+ N = self._edb[f"{pfx}_fingers"]
1020
+ L = self._edb[f"{pfx}_finger_length"]
1021
+ W = self._edb[f"{pfx}_finger_width"]
1022
+ g = self._edb[f"{pfx}_gap"]
1023
+ return (eps0 * er * N * L * W / g) * 1e12
1024
+
1025
+ def create(self) -> bool:
1026
+ """
1027
+ Draw the two bus bars and interleaved fingers.
1028
+
1029
+ Returns
1030
+ -------
1031
+ bool
1032
+ True on success.
1033
+ """
1034
+ pfx = self.VAR_PREFIX
1035
+ pitch = f"({pfx}_finger_width + {pfx}_gap)"
1036
+ total_width = f"(2*{pfx}_bus_width + (2*{pfx}_fingers-1)*{pitch} + {pfx}_finger_width)"
1037
+ total_height = f"(2*{pfx}_bus_width + 2*{pfx}_finger_length + {pfx}_comb_gap)"
1038
+
1039
+ # 1. Bottom bus bar (NET_A)
1040
+ self._edb.modeler.create_rectangle(
1041
+ layer_name=self.layer,
1042
+ net_name=self.net_a,
1043
+ lower_left_point=["0", "0"],
1044
+ upper_right_point=[total_width, f"{pfx}_bus_width"],
1045
+ )
1046
+
1047
+ # 2. Top bus bar (NET_B)
1048
+ right_bar_y = f"({total_height} - {pfx}_bus_width)"
1049
+ self._edb.modeler.create_rectangle(
1050
+ layer_name=self.layer,
1051
+ net_name=self.net_b,
1052
+ lower_left_point=["0", right_bar_y],
1053
+ upper_right_point=[total_width, total_height],
1054
+ )
1055
+
1056
+ # 3. Fingers (interleaved)
1057
+ y_a = f"{pfx}_bus_width" # base of up fingers
1058
+ y_b = f"({total_height} - {pfx}_bus_width - {pfx}_finger_length)" # base of down fingers
1059
+
1060
+ for i in range(self._vars["fingers"] * 2):
1061
+ x = f"({pfx}_bus_width + {i}*{pitch})"
1062
+ if i % 2 == 0: # NET_A finger (points up)
1063
+ self._edb.modeler.create_rectangle(
1064
+ layer_name=self.layer,
1065
+ net_name=self.net_a,
1066
+ lower_left_point=[x, y_a],
1067
+ upper_right_point=[f"{x} + {pfx}_finger_width", f"{y_a} + {pfx}_finger_length"],
1068
+ )
1069
+ else: # NET_B finger (points down)
1070
+ self._edb.modeler.create_rectangle(
1071
+ layer_name=self.layer,
1072
+ net_name=self.net_b,
1073
+ lower_left_point=[x, y_b],
1074
+ upper_right_point=[f"{x} + {pfx}_finger_width", f"{y_b} + {pfx}_finger_length"],
1075
+ )
1076
+
1077
+ return True
1078
+
1079
+
1080
+ class DifferentialTLine:
1081
+ """
1082
+ Edge-coupled differential pair with fully parametric geometry.
1083
+
1084
+ Parameters
1085
+ ----------
1086
+ length : float, default 10 mm
1087
+ Total length of the pair.
1088
+ width : float, default 0.2 mm
1089
+ Width of each individual trace.
1090
+ spacing : float, default 0.2 mm
1091
+ Edge-to-edge separation between the traces.
1092
+ x0 : float, default 0
1093
+ Start x-coordinate (metres).
1094
+ y0 : float, default 0
1095
+ Start y-coordinate (metres).
1096
+ angle : float, default 0 rad
1097
+ Rotation angle of the pair (radians, CCW).
1098
+ layer : str, default "TOP"
1099
+ Layer on which the traces are drawn.
1100
+ net_p : str, default "P"
1101
+ Net name for the positive trace.
1102
+ net_n : str, default "N"
1103
+ Net name for the negative trace.
1104
+
1105
+ Examples
1106
+ --------
1107
+ >>> diff = DifferentialTLine(Edb("diff.aedb"),
1108
+ ... length=5e-3,
1109
+ ... width=0.15e-3,
1110
+ ... spacing=0.1e-3,
1111
+ ... angle=math.pi/4)
1112
+ >>> traces = diff.create()
1113
+ >>> f"{diff.diff_impedance:.1f} Ω"
1114
+ '95.6 Ω'
1115
+ """
1116
+
1117
+ def __init__(
1118
+ self,
1119
+ edb: Edb,
1120
+ length: Union[float, str] = 10e-3,
1121
+ width: Union[float, str] = 0.20e-3,
1122
+ spacing: Union[float, str] = 0.20e-3,
1123
+ x0: Union[float, str] = 0.0,
1124
+ y0: Union[float, str] = 0.0,
1125
+ angle: Union[float, str] = 0.0,
1126
+ layer: str = "TOP",
1127
+ net_p: str = "P",
1128
+ net_n: str = "N",
1129
+ ):
1130
+ self._edb = edb
1131
+ self.length = self._edb.value(length)
1132
+ self.width = self._edb.value(width)
1133
+ self.spacing = self._edb.value(spacing)
1134
+ self.x0 = self._edb.value(x0)
1135
+ self.y0 = self._edb.value(y0)
1136
+ self.layer = layer
1137
+ self.net_p = net_p
1138
+ self.net_n = net_n
1139
+
1140
+ self._edb["diff_len"] = self.length # total length
1141
+ self._edb["diff_w"] = self.width # single trace width
1142
+ self._edb["diff_s"] = self.spacing # edge-to-edge spacing
1143
+ self._edb["diff_x0"] = self.x0 # start-x
1144
+ self._edb["diff_y0"] = self.y0 # start-y
1145
+ self._edb["diff_angle"] = math.degrees(angle) # rotation in degrees
1146
+
1147
+ @property
1148
+ def diff_impedance(self) -> float:
1149
+ """
1150
+ Rough odd-mode impedance estimate for the differential pair.
1151
+
1152
+ Returns
1153
+ -------
1154
+ float
1155
+ Differential impedance in Ohms.
1156
+ """
1157
+ w = self._edb["diff_w"]
1158
+ s = self._edb["diff_s"]
1159
+ z0_single = 60.0
1160
+ return 2 * z0_single * (1 - 0.48 * math.exp(-0.96 * s / w))
1161
+
1162
+ def create(self) -> List[float]:
1163
+ """
1164
+ Create the two traces using only parameter strings so the
1165
+ geometry stays fully editable in AEDT.
1166
+
1167
+ Returns
1168
+ -------
1169
+ list[float]
1170
+ EDB object IDs of the positive and negative traces.
1171
+ """
1172
+ pos_trace = self._edb.modeler.create_trace(
1173
+ path_list=[
1174
+ ["diff_x0", "diff_y0"],
1175
+ ["diff_x0 + diff_len*cos(diff_angle*1deg)", "diff_y0 + diff_len*sin(diff_angle*1deg)"],
1176
+ ],
1177
+ layer_name=self.layer,
1178
+ width="diff_w",
1179
+ net_name=self.net_p,
1180
+ start_cap_style="Flat",
1181
+ end_cap_style="Flat",
1182
+ )
1183
+
1184
+ neg_y_expr = "diff_y0 - diff_w - diff_s"
1185
+ neg_trace = self._edb.modeler.create_trace(
1186
+ path_list=[
1187
+ ["diff_x0", neg_y_expr],
1188
+ ["diff_x0 + diff_len*cos(diff_angle*1deg)", f"{neg_y_expr} + diff_len*sin(diff_angle*1deg)"],
1189
+ ],
1190
+ layer_name=self.layer,
1191
+ width="diff_w",
1192
+ net_name=self.net_n,
1193
+ start_cap_style="Flat",
1194
+ end_cap_style="Flat",
1195
+ )
1196
+ return [pos_trace, neg_trace]
1197
+
1198
+ import math
1199
+ from typing import List, Optional
1200
+
1201
+
1202
+ class MicroStripLine:
1203
+ """
1204
+ Single-ended micro-strip line with fixed geometry and editable stack-up.
1205
+
1206
+ Parameters
1207
+ ----------
1208
+ edb : object
1209
+ EDB application / API handle.
1210
+ layer : str
1211
+ Conductor layer on which the trace is drawn.
1212
+ net : str
1213
+ Net name assigned to the trace.
1214
+ x0, y0 : float
1215
+ Global start coordinates (mm, m, or whatever length unit is active in AEDT).
1216
+ length : float
1217
+ Physical length of the line (same unit as x0/y0).
1218
+ angle : float
1219
+ Rotation angle in degrees (0° = along +X).
1220
+ freq : float, optional
1221
+ Reference frequency (Hz) used to compute the electrical length.
1222
+ If None, electrical_length will be None.
1223
+ """
1224
+
1225
+ def __init__(
1226
+ self,
1227
+ edb_cell,
1228
+ layer: str,
1229
+ net: str,
1230
+ x0: Union[float, str] = 0.0,
1231
+ y0: Union[float, str] = 0.0,
1232
+ length: Union[float, str] = "1mm",
1233
+ width: Union[float, str] = "0.2mm",
1234
+ angle: Union[float, str] = 0.0,
1235
+ freq: Optional[Union[float, str]] = None,
1236
+ ):
1237
+ self._edb = edb_cell
1238
+ self.layer = layer
1239
+ self.net = net
1240
+ self.x0 = self._edb.value(x0)
1241
+ self.y0 = self._edb.value(y0)
1242
+ self._length = self._edb.value(length)
1243
+ self._width = self._edb.value(width) # width of the trace
1244
+ self.angle = self._edb.value(angle) # degrees
1245
+ self.freq = freq
1246
+ self.substrate = Substrate()
1247
+ self.id = None
1248
+
1249
+ # Edb variable
1250
+ self._edb["w"] = self.width
1251
+ self._edb["l"] = self.length
1252
+
1253
+ @property
1254
+ def width(self):
1255
+ return self._width
1256
+
1257
+ @width.setter
1258
+ def width(self, value: Union[float, str]):
1259
+ """Set the trace width and update the EDB variable."""
1260
+ self._width = self._edb.value(value)
1261
+ self._edb["w"] = self._width
1262
+
1263
+ @property
1264
+ def length(self) -> float:
1265
+ """Physical length of the micro-strip line."""
1266
+ return self._length
1267
+
1268
+ @length.setter
1269
+ def length(self, value: Union[float, str]):
1270
+ """Set the trace length and update the EDB variable."""
1271
+ self._length = self._edb.value(value)
1272
+ self._edb["l"] = self._length
1273
+
1274
+ @property
1275
+ def _ereff(self) -> float:
1276
+ w = self._edb["w"]
1277
+ er = self.substrate.er
1278
+ h = self.substrate.h
1279
+
1280
+ u = w / h
1281
+ a = (
1282
+ 1
1283
+ + (1 / 49) * math.log((u**4 + (u / 52) ** 2) / (u**4 + 0.432))
1284
+ + (1 / 18.7) * math.log(1 + (u / 18.1) ** 3)
1285
+ )
1286
+ b = 0.564 * ((er - 0.9) / (er + 3)) ** 0.053
1287
+ return (er + 1) / 2 + (er - 1) / 2 * (1 + 10 / u) ** (-a * b)
1288
+
1289
+ # ------------------------------------------------------------------
1290
+ # Electrical properties
1291
+ # ------------------------------------------------------------------
1292
+ @property
1293
+ def impedance(self) -> float:
1294
+ """
1295
+ Accurate characteristic impedance (Ω) for a micro-strip line.
1296
+ Valid for 0.05 ≤ w/h ≤ 1000 and 1 ≤ εr ≤ 128.
1297
+ Source: G. Wheeler, E. Hammerstad & Jensen, IEEE-MTT 1980.
1298
+ """
1299
+ w = self._edb["w"]
1300
+ h = self.substrate.h
1301
+ er = self.substrate.er
1302
+
1303
+ u = w / h
1304
+
1305
+ # Effective permittivity (εeff)
1306
+ eta0 = 376.73 # Free-space impedance, Ω
1307
+ a0 = (
1308
+ 1
1309
+ + (1 / 49) * math.log((u**4 + (u / 52) ** 2) / (u**4 + 0.432))
1310
+ + (1 / 18.7) * math.log(1 + (u / 18.1) ** 3)
1311
+ )
1312
+ b0 = 0.564 * ((er - 0.9) / (er + 3)) ** 0.053
1313
+ er_eff = (er + 1) / 2 + (er - 1) / 2 * (1 + 10 / u) ** (-a0 * b0)
1314
+
1315
+ # Impedance (Z0)
1316
+ # Two regions: u ≤ 1 and u > 1
1317
+ if u <= 1:
1318
+ z0 = eta0 / (2 * math.pi * math.sqrt(er_eff + 1.41)) * math.log(8 / u + u / 4)
1319
+ else:
1320
+ f = 6 + (2 * math.pi - 6) * math.exp(-((30.666 / u) ** 0.7528))
1321
+ z0 = eta0 / (math.sqrt(er_eff) * (u + 1.98 * u**0.172))
1322
+
1323
+ return round(z0, 2)
1324
+
1325
+ @property
1326
+ def electrical_length(self) -> Optional[float]:
1327
+ """
1328
+ Electrical length in degrees at self.freq.
1329
+ Returns None if freq is None.
1330
+ """
1331
+ if self.freq is None:
1332
+ return None
1333
+ c0 = 299_792_458 # m/s
1334
+ beta_l = 2 * math.pi * self.freq * math.sqrt(self._ereff) / c0 * self.length
1335
+ return round(math.degrees(beta_l), 3)
1336
+
1337
+ # ------------------------------------------------------------------
1338
+ # Geometry creation
1339
+ # ------------------------------------------------------------------
1340
+ def create(self) -> List[float]:
1341
+ """Create the trace and return its EDB object."""
1342
+ angle_rad = math.radians(self.angle)
1343
+ trace = self._edb.modeler.create_trace(
1344
+ path_list=[
1345
+ [self.x0, self.y0],
1346
+ [
1347
+ self.x0 + self.length * math.cos(angle_rad),
1348
+ self.y0 + self.length * math.sin(angle_rad),
1349
+ ],
1350
+ ],
1351
+ layer_name=self.layer,
1352
+ width="w",
1353
+ net_name=self.net,
1354
+ start_cap_style="Flat",
1355
+ end_cap_style="Flat",
1356
+ )
1357
+ self.id = trace.id
1358
+ return trace