pyedb 0.54.0__py3-none-any.whl → 0.56.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.
- pyedb/__init__.py +1 -8
- pyedb/configuration/cfg_boundaries.py +69 -151
- pyedb/configuration/cfg_components.py +201 -460
- pyedb/configuration/cfg_data.py +4 -2
- pyedb/configuration/cfg_general.py +13 -36
- pyedb/configuration/cfg_modeler.py +2 -1
- pyedb/configuration/cfg_nets.py +21 -35
- pyedb/configuration/cfg_operations.py +22 -151
- pyedb/configuration/cfg_package_definition.py +56 -112
- pyedb/configuration/cfg_padstacks.py +292 -688
- pyedb/configuration/cfg_pin_groups.py +32 -79
- pyedb/configuration/cfg_ports_sources.py +19 -6
- pyedb/configuration/cfg_s_parameter_models.py +67 -172
- pyedb/configuration/cfg_setup.py +102 -295
- pyedb/configuration/configuration.py +64 -5
- pyedb/dotnet/database/Variables.py +26 -19
- pyedb/dotnet/database/cell/connectable.py +38 -9
- pyedb/dotnet/database/cell/hierarchy/component.py +28 -28
- pyedb/dotnet/database/cell/hierarchy/model.py +1 -1
- pyedb/dotnet/database/cell/layout.py +63 -2
- pyedb/dotnet/database/cell/layout_obj.py +2 -2
- pyedb/dotnet/database/cell/primitive/path.py +6 -8
- pyedb/dotnet/database/cell/primitive/primitive.py +3 -24
- pyedb/dotnet/database/cell/terminal/edge_terminal.py +2 -2
- pyedb/dotnet/database/cell/terminal/padstack_instance_terminal.py +1 -1
- pyedb/dotnet/database/cell/terminal/pingroup_terminal.py +1 -1
- pyedb/dotnet/database/cell/terminal/point_terminal.py +1 -1
- pyedb/dotnet/database/cell/terminal/terminal.py +24 -24
- pyedb/dotnet/database/cell/voltage_regulator.py +0 -21
- pyedb/dotnet/database/components.py +137 -124
- pyedb/dotnet/database/definition/component_def.py +4 -4
- pyedb/dotnet/database/definition/component_model.py +1 -1
- pyedb/dotnet/database/definition/package_def.py +2 -3
- pyedb/dotnet/database/dotnet/database.py +3 -199
- pyedb/dotnet/database/dotnet/primitive.py +3 -3
- pyedb/dotnet/database/edb_data/control_file.py +5 -5
- pyedb/dotnet/database/edb_data/hfss_extent_info.py +6 -6
- pyedb/dotnet/database/edb_data/layer_data.py +23 -23
- pyedb/dotnet/database/edb_data/padstacks_data.py +63 -88
- pyedb/dotnet/database/edb_data/primitives_data.py +5 -5
- pyedb/dotnet/database/edb_data/sources.py +6 -6
- pyedb/dotnet/database/edb_data/variables.py +1 -1
- pyedb/dotnet/database/geometry/point_data.py +14 -10
- pyedb/dotnet/database/geometry/polygon_data.py +3 -3
- pyedb/dotnet/database/hfss.py +46 -48
- pyedb/dotnet/database/layout_validation.py +14 -11
- pyedb/dotnet/database/materials.py +10 -11
- pyedb/dotnet/database/modeler.py +97 -91
- pyedb/dotnet/database/nets.py +19 -22
- pyedb/dotnet/database/padstack.py +171 -83
- pyedb/dotnet/database/siwave.py +42 -42
- pyedb/dotnet/database/stackup.py +140 -72
- pyedb/dotnet/database/utilities/heatsink.py +4 -4
- pyedb/dotnet/database/utilities/obj_base.py +2 -2
- pyedb/dotnet/database/utilities/simulation_setup.py +2 -2
- pyedb/dotnet/database/utilities/value.py +16 -16
- pyedb/dotnet/edb.py +230 -152
- pyedb/edb_logger.py +12 -27
- pyedb/extensions/create_cell_array.py +394 -0
- pyedb/extensions/via_design_backend.py +6 -3
- pyedb/generic/data_handlers.py +6 -7
- pyedb/generic/design_types.py +81 -30
- pyedb/generic/filesystem.py +5 -2
- pyedb/generic/general_methods.py +2 -122
- pyedb/generic/process.py +44 -108
- pyedb/generic/settings.py +79 -19
- pyedb/grpc/database/components.py +26 -4
- pyedb/grpc/database/control_file.py +5 -5
- pyedb/grpc/database/definition/materials.py +1 -1
- pyedb/grpc/database/definition/package_def.py +3 -3
- pyedb/grpc/database/definition/padstack_def.py +53 -0
- pyedb/grpc/database/geometry/polygon_data.py +1 -1
- pyedb/grpc/database/layout/layout.py +81 -5
- pyedb/grpc/database/layout_validation.py +5 -5
- pyedb/grpc/database/modeler.py +24 -16
- pyedb/grpc/database/net/net.py +15 -14
- pyedb/grpc/database/nets.py +70 -0
- pyedb/grpc/database/padstacks.py +122 -17
- pyedb/grpc/database/primitive/padstack_instance.py +175 -7
- pyedb/grpc/database/primitive/polygon.py +2 -2
- pyedb/grpc/database/simulation_setup/siwave_cpa_simulation_setup.py +3 -2
- pyedb/grpc/database/siwave.py +1 -1
- pyedb/grpc/database/source_excitations.py +12 -5
- pyedb/grpc/database/stackup.py +1 -1
- pyedb/grpc/database/terminal/bundle_terminal.py +1 -1
- pyedb/grpc/database/terminal/padstack_instance_terminal.py +1 -1
- pyedb/grpc/database/terminal/pingroup_terminal.py +1 -1
- pyedb/grpc/database/utility/value.py +1 -0
- pyedb/grpc/database/utility/xml_control_file.py +5 -5
- pyedb/grpc/edb.py +80 -30
- pyedb/grpc/edb_init.py +3 -3
- pyedb/grpc/rpc_session.py +14 -13
- pyedb/libraries/common.py +366 -0
- pyedb/libraries/rf_libraries/base_functions.py +1358 -0
- pyedb/libraries/rf_libraries/planar_antennas.py +628 -0
- pyedb/misc/decorators.py +61 -0
- pyedb/misc/misc.py +0 -13
- pyedb/modeler/geometry_operators.py +6 -6
- pyedb/siwave.py +6 -8
- pyedb/siwave_core/__init__.py +0 -0
- pyedb/siwave_core/cpa/__init__.py +0 -0
- {pyedb-0.54.0.dist-info → pyedb-0.56.0.dist-info}/METADATA +1 -2
- {pyedb-0.54.0.dist-info → pyedb-0.56.0.dist-info}/RECORD +105 -98
- {pyedb-0.54.0.dist-info → pyedb-0.56.0.dist-info}/WHEEL +0 -0
- {pyedb-0.54.0.dist-info → pyedb-0.56.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
|