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.
- 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/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 +96 -88
- 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 +84 -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 +228 -150
- pyedb/edb_logger.py +12 -27
- pyedb/extensions/via_design_backend.py +6 -3
- pyedb/generic/design_types.py +67 -29
- pyedb/generic/general_methods.py +0 -120
- pyedb/generic/process.py +44 -108
- pyedb/generic/settings.py +75 -19
- pyedb/grpc/database/components.py +2 -0
- 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 +8 -5
- pyedb/grpc/database/layout_validation.py +3 -3
- pyedb/grpc/database/modeler.py +9 -4
- pyedb/grpc/database/net/net.py +15 -14
- pyedb/grpc/database/nets.py +70 -0
- pyedb/grpc/database/padstacks.py +35 -17
- pyedb/grpc/database/primitive/padstack_instance.py +175 -7
- pyedb/grpc/database/siwave.py +1 -1
- pyedb/grpc/database/source_excitations.py +2 -4
- 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/xml_control_file.py +5 -5
- pyedb/grpc/edb.py +73 -27
- pyedb/grpc/edb_init.py +3 -3
- pyedb/grpc/rpc_session.py +10 -10
- 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/siwave.py +2 -2
- {pyedb-0.54.0.dist-info → pyedb-0.55.0.dist-info}/METADATA +1 -2
- {pyedb-0.54.0.dist-info → pyedb-0.55.0.dist-info}/RECORD +95 -91
- {pyedb-0.54.0.dist-info → pyedb-0.55.0.dist-info}/WHEEL +0 -0
- {pyedb-0.54.0.dist-info → pyedb-0.55.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,628 @@
|
|
|
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 Union
|
|
27
|
+
|
|
28
|
+
from pyedb.libraries.common import Substrate
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RectangularPatch:
|
|
32
|
+
"""
|
|
33
|
+
Rectangular microstrip patch antenna (optionally inset-fed).
|
|
34
|
+
|
|
35
|
+
The class automatically determines the physical dimensions for a
|
|
36
|
+
desired resonance frequency, creates the patch, ground plane and
|
|
37
|
+
either an inset microstrip feed or a coaxial probe feed, and
|
|
38
|
+
optionally sets up an HFSS simulation.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
edb_cell : pyedb.Edb, optional
|
|
43
|
+
EDB project/cell in which the antenna will be built.
|
|
44
|
+
freq : str or float, default "2.4GHz"
|
|
45
|
+
Target resonance frequency of the patch. A string such as
|
|
46
|
+
``"2.4GHz"`` or a numeric value in Hz can be given.
|
|
47
|
+
inset : str or float, default 0
|
|
48
|
+
Inset depth for a 50 Ω microstrip feed. A value of 0 selects
|
|
49
|
+
a probe (via) feed instead.
|
|
50
|
+
layer : str, default "TOP_METAL"
|
|
51
|
+
Metallization layer on which the patch polygon is drawn.
|
|
52
|
+
bottom_layer : str, default "BOT_METAL"
|
|
53
|
+
Metallization layer on which the ground polygon is drawn.
|
|
54
|
+
add_port : bool, default True
|
|
55
|
+
If True, create a wave port (inset feed) or lumped port
|
|
56
|
+
(probe feed) and add an HFSS setup with a frequency sweep.
|
|
57
|
+
|
|
58
|
+
Attributes
|
|
59
|
+
----------
|
|
60
|
+
substrate : Substrate
|
|
61
|
+
Substrate definition (``er``, ``tand``, ``h``) used for all
|
|
62
|
+
analytical calculations.
|
|
63
|
+
|
|
64
|
+
Examples
|
|
65
|
+
--------
|
|
66
|
+
Build a 5.8 GHz patch on a 0.787 mm Rogers RO4350B substrate:
|
|
67
|
+
|
|
68
|
+
>>> edb = pyedb.Edb()
|
|
69
|
+
>>> patch = RectangularPatch(
|
|
70
|
+
... edb_cell=edb,
|
|
71
|
+
... freq="5.8GHz",
|
|
72
|
+
... inset="4.2mm",
|
|
73
|
+
... layer="TOP",
|
|
74
|
+
... bottom_layer="GND"
|
|
75
|
+
... )
|
|
76
|
+
>>> patch.substrate.er = 3.66
|
|
77
|
+
>>> patch.substrate.tand = 0.0037
|
|
78
|
+
>>> patch.substrate.h = 0.000787
|
|
79
|
+
>>> patch.create()
|
|
80
|
+
>>> edb.save_as("patch_5p8GHz.aedb")
|
|
81
|
+
|
|
82
|
+
Probe-fed 2.4 GHz patch (no inset):
|
|
83
|
+
|
|
84
|
+
>>> edb = pyedb.Edb()
|
|
85
|
+
>>> RectangularPatch(edb, freq=2.4e9, inset=0).create()
|
|
86
|
+
>>> edb.save_as("probe_patch_2p4GHz.aedb")
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
edb_cell=None,
|
|
92
|
+
target_frequency: Union[str, float] = "2.4Ghz",
|
|
93
|
+
length_feeding_line: Union[str, float] = 0,
|
|
94
|
+
layer: str = "TOP_METAL",
|
|
95
|
+
bottom_layer: str = "BOT_METAL",
|
|
96
|
+
add_port: bool = True,
|
|
97
|
+
permittivity: float = None,
|
|
98
|
+
):
|
|
99
|
+
self._edb = edb_cell
|
|
100
|
+
self.target_frequency = self._edb.value(target_frequency)
|
|
101
|
+
self.length_feeding_line = self._edb.value(length_feeding_line)
|
|
102
|
+
self.layer = layer
|
|
103
|
+
self.bottom_layer = bottom_layer
|
|
104
|
+
self.add_port = add_port
|
|
105
|
+
self.substrate = Substrate()
|
|
106
|
+
if permittivity:
|
|
107
|
+
self.substrate.er = permittivity
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def estimated_frequency(self) -> float:
|
|
111
|
+
"""
|
|
112
|
+
Analytical resonance frequency (GHz) computed from the cavity model.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
float
|
|
117
|
+
Resonant frequency in Hz.
|
|
118
|
+
"""
|
|
119
|
+
# Effective length
|
|
120
|
+
c = 299_792_458
|
|
121
|
+
eps_eff = (self.substrate.er + 1) / 2 + (self.substrate.er - 1) / 2 * (
|
|
122
|
+
1 + 10 * self.substrate.h / self.width
|
|
123
|
+
) ** -0.5
|
|
124
|
+
return self._edb.value(f"{round((c / (2 * self.width * math.sqrt(eps_eff)) / 1e9), 3)}Ghz")
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def width(self) -> float:
|
|
128
|
+
"""Patch width (m) derived for the target frequency."""
|
|
129
|
+
c = 299_792_458
|
|
130
|
+
return round(c / (2 * self.target_frequency * math.sqrt((self.substrate.er + 1) / 2)), 5)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def length(self) -> float:
|
|
134
|
+
"""Patch length (m) accounting for fringing fields."""
|
|
135
|
+
eps_eff = (self.substrate.er + 1) / 2 + (self.substrate.er - 1) / 2 * (
|
|
136
|
+
1 + 12 * self.substrate.h / self.width
|
|
137
|
+
) ** -0.5
|
|
138
|
+
delta_l = (
|
|
139
|
+
0.412
|
|
140
|
+
* self.substrate.h
|
|
141
|
+
* (eps_eff + 0.3)
|
|
142
|
+
/ (eps_eff - 0.258)
|
|
143
|
+
* (self.width / self.substrate.h + 0.264)
|
|
144
|
+
/ (self.width / self.substrate.h + 0.8)
|
|
145
|
+
)
|
|
146
|
+
return round(0.5 * 299_792_458 / (self.target_frequency * math.sqrt(eps_eff)) - 2 * delta_l, 5)
|
|
147
|
+
|
|
148
|
+
def create(self) -> bool:
|
|
149
|
+
"""Draw the patch, ground plane and feed geometry in EDB.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
bool
|
|
154
|
+
True when the geometry has been successfully created.
|
|
155
|
+
"""
|
|
156
|
+
self._edb["w"] = self.width
|
|
157
|
+
self._edb["l"] = self.length
|
|
158
|
+
self._edb["x0"] = self.length_feeding_line
|
|
159
|
+
|
|
160
|
+
# patch
|
|
161
|
+
self._edb.modeler.create_rectangle(
|
|
162
|
+
self.layer,
|
|
163
|
+
"ANT",
|
|
164
|
+
representation_type="CenterWidthHeight",
|
|
165
|
+
center_point=[0, 0],
|
|
166
|
+
height=self.length,
|
|
167
|
+
width=self.width,
|
|
168
|
+
)
|
|
169
|
+
# ground
|
|
170
|
+
self._edb.modeler.create_rectangle(
|
|
171
|
+
self.bottom_layer,
|
|
172
|
+
"GND",
|
|
173
|
+
representation_type="CenterWidthHeight",
|
|
174
|
+
center_point=[0, 0],
|
|
175
|
+
height=self.length * 2,
|
|
176
|
+
width=self.width * 2,
|
|
177
|
+
)
|
|
178
|
+
# feed
|
|
179
|
+
if self.length_feeding_line > 0:
|
|
180
|
+
from pyedb.libraries.rf_libraries.base_functions import MicroStripLine
|
|
181
|
+
|
|
182
|
+
ustrip_line = MicroStripLine(
|
|
183
|
+
self._edb, layer=self.layer, net="FEED", length=self.length_feeding_line, x0=self.width / 2, y0=0
|
|
184
|
+
).create()
|
|
185
|
+
if self.add_port:
|
|
186
|
+
self._edb.hfss.create_wave_port(
|
|
187
|
+
prim_id=ustrip_line.id,
|
|
188
|
+
point_on_edge=[self.width / 2 + self.length_feeding_line, 0],
|
|
189
|
+
port_name="ustrip_port",
|
|
190
|
+
horizontal_extent_factor=10,
|
|
191
|
+
vertical_extent_factor=5,
|
|
192
|
+
)
|
|
193
|
+
setup = self._edb.create_hfss_setup("Patch_antenna_lib")
|
|
194
|
+
setup.adaptive_settings.adaptive_frequency_data_list[0].adaptive_frequency = str(
|
|
195
|
+
self.estimated_frequency
|
|
196
|
+
)
|
|
197
|
+
setup.add_sweep(
|
|
198
|
+
distribution="linear",
|
|
199
|
+
start_freq=str(self.estimated_frequency * 0.7),
|
|
200
|
+
stop_freq=str(self.estimated_frequency * 1.3),
|
|
201
|
+
step="0.01GHz",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
else:
|
|
205
|
+
padstack_def = self._edb.padstacks.create(
|
|
206
|
+
padstackname="FEED", start_layer=self.layer, stop_layer=self.bottom_layer
|
|
207
|
+
)
|
|
208
|
+
self._edb.padstacks.place(position=[0, 0], definition_name=padstack_def)
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class CircularPatch:
|
|
213
|
+
"""
|
|
214
|
+
Circular microstrip patch antenna (optionally probe-fed).
|
|
215
|
+
|
|
216
|
+
The class automatically determines the physical dimensions for a
|
|
217
|
+
desired resonance frequency, creates the patch, ground plane and
|
|
218
|
+
either an inset microstrip feed or a coaxial probe feed, and
|
|
219
|
+
optionally sets up an HFSS simulation.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
edb_cell : pyedb.Edb, optional
|
|
224
|
+
EDB project/cell in which the antenna will be built.
|
|
225
|
+
freq : str or float, default "2.4GHz"
|
|
226
|
+
Target resonance frequency of the patch. A string such as
|
|
227
|
+
``"2.4GHz"`` or a numeric value in Hz can be given.
|
|
228
|
+
probe_offset : str or float, default 0
|
|
229
|
+
Radial offset of the 50 Ω coax probe from the patch center.
|
|
230
|
+
A value of 0 places the probe at the center (not recommended
|
|
231
|
+
for good matching).
|
|
232
|
+
layer : str, default "TOP_METAL"
|
|
233
|
+
Metallization layer on which the patch polygon is drawn.
|
|
234
|
+
bottom_layer : str, default "BOT_METAL"
|
|
235
|
+
Metallization layer on which the ground polygon is drawn.
|
|
236
|
+
add_port : bool, default True
|
|
237
|
+
If True, create a lumped port (probe feed) and add an HFSS
|
|
238
|
+
setup with a frequency sweep.
|
|
239
|
+
|
|
240
|
+
Attributes
|
|
241
|
+
----------
|
|
242
|
+
substrate : Substrate
|
|
243
|
+
Substrate definition (``er``, ``tand``, ``h``) used for all
|
|
244
|
+
analytical calculations.
|
|
245
|
+
|
|
246
|
+
Examples
|
|
247
|
+
--------
|
|
248
|
+
Build a 5.8 GHz circular patch on a 0.787 mm Rogers RO4350B substrate:
|
|
249
|
+
|
|
250
|
+
>>> edb = pyedb.Edb()
|
|
251
|
+
>>> patch = CircularPatch(
|
|
252
|
+
... edb_cell=edb,
|
|
253
|
+
... freq="5.8GHz",
|
|
254
|
+
... probe_offset="6.4mm",
|
|
255
|
+
... layer="TOP",
|
|
256
|
+
... bottom_layer="GND"
|
|
257
|
+
... )
|
|
258
|
+
>>> patch.substrate.er = 3.66
|
|
259
|
+
>>> patch.substrate.tand = 0.0037
|
|
260
|
+
>>> patch.substrate.h = 0.000787
|
|
261
|
+
>>> patch.create()
|
|
262
|
+
>>> edb.save_as("circ_patch_5p8GHz.aedb")
|
|
263
|
+
|
|
264
|
+
Probe-fed 2.4 GHz patch with default 0 offset (center feed):
|
|
265
|
+
|
|
266
|
+
>>> edb = pyedb.Edb()
|
|
267
|
+
>>> CircularPatch(edb, freq=2.4e9).create()
|
|
268
|
+
>>> edb.save_as("probe_circ_patch_2p4GHz.aedb")
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def __init__(
|
|
272
|
+
self,
|
|
273
|
+
edb_cell=None,
|
|
274
|
+
target_frequency: Union[str, float] = "2.4GHz",
|
|
275
|
+
length_feeding_line: Union[str, float] = 0,
|
|
276
|
+
layer: str = "TOP_METAL",
|
|
277
|
+
bottom_layer: str = "BOT_METAL",
|
|
278
|
+
add_port: bool = True,
|
|
279
|
+
):
|
|
280
|
+
self._edb = edb_cell
|
|
281
|
+
self.target_frequency = self._edb.value(target_frequency)
|
|
282
|
+
self.length_feeding_line = self._edb.value(length_feeding_line)
|
|
283
|
+
self.layer = layer
|
|
284
|
+
self.bottom_layer = bottom_layer
|
|
285
|
+
self.substrate = Substrate()
|
|
286
|
+
self.add_port = add_port
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def estimated_frequency(self) -> float:
|
|
290
|
+
"""
|
|
291
|
+
Improved analytical resonance frequency (Hz) of the dominant TM11 mode.
|
|
292
|
+
|
|
293
|
+
Uses Balanis’ closed-form model for single-layer circular patches.
|
|
294
|
+
Accuracy ≈ ±0.5 % compared with full-wave solvers for
|
|
295
|
+
0.003 ≤ h/λd ≤ 0.05 and εr 2–12.
|
|
296
|
+
|
|
297
|
+
Returns
|
|
298
|
+
-------
|
|
299
|
+
float
|
|
300
|
+
Resonant frequency in Hz.
|
|
301
|
+
"""
|
|
302
|
+
c = 299_792_458.0
|
|
303
|
+
a = self.radius
|
|
304
|
+
h = self.substrate.h
|
|
305
|
+
er = self.substrate.er
|
|
306
|
+
|
|
307
|
+
# Effective dielectric constant
|
|
308
|
+
eps_eff = (er + 1.0) / 2.0 + (er - 1.0) / 2.0 * (1.0 + 10.0 * h / a) ** -0.5
|
|
309
|
+
|
|
310
|
+
# Effective radius including fringing
|
|
311
|
+
a_eff = a * (1.0 + (2.0 * h) / (math.pi * a * er) * (math.log(math.pi * a / (2.0 * h)) + 1.7726)) ** 0.5
|
|
312
|
+
|
|
313
|
+
k11 = 1.84118 # First zero of J1′(x)
|
|
314
|
+
f11 = k11 * c / (2.0 * math.pi * a_eff * math.sqrt(eps_eff))
|
|
315
|
+
|
|
316
|
+
return self._edb.value(f"{round(f11 / 1e9, 3)}GHz")
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def radius(self) -> float:
|
|
320
|
+
"""Patch physical radius (m) derived for the target frequency."""
|
|
321
|
+
c = 299_792_458
|
|
322
|
+
# Initial guess without fringing
|
|
323
|
+
a0 = 1.84118 * c / (2 * math.pi * self.target_frequency * math.sqrt(self.substrate.er))
|
|
324
|
+
# Iteratively refine fringing correction
|
|
325
|
+
a = a0
|
|
326
|
+
for _ in range(3):
|
|
327
|
+
a_eff = (
|
|
328
|
+
a
|
|
329
|
+
* (
|
|
330
|
+
1
|
|
331
|
+
+ (2 * self.substrate.h)
|
|
332
|
+
/ (math.pi * a * self.substrate.er)
|
|
333
|
+
* (math.log(math.pi * a / (2 * self.substrate.h)) + 1.7726)
|
|
334
|
+
)
|
|
335
|
+
** 0.5
|
|
336
|
+
)
|
|
337
|
+
a = a0 * (a / a_eff)
|
|
338
|
+
return round(a, 5)
|
|
339
|
+
|
|
340
|
+
def create(self) -> bool:
|
|
341
|
+
"""Draw the patch, ground plane and feed geometry in EDB.
|
|
342
|
+
|
|
343
|
+
Returns
|
|
344
|
+
-------
|
|
345
|
+
bool
|
|
346
|
+
True when the geometry has been successfully created.
|
|
347
|
+
"""
|
|
348
|
+
self._edb["r"] = self.radius
|
|
349
|
+
self._edb["d"] = self.length_feeding_line
|
|
350
|
+
|
|
351
|
+
# patch
|
|
352
|
+
self._edb.modeler.create_circle(self.layer, net_name="ANT", x=0, y=0, radius=self.radius)
|
|
353
|
+
# ground
|
|
354
|
+
self._edb.modeler.create_circle(self.bottom_layer, net_name="GND", x=0, y=0, radius=self.radius * 2)
|
|
355
|
+
# feed
|
|
356
|
+
if self.length_feeding_line > 0:
|
|
357
|
+
from pyedb.libraries.rf_libraries.base_functions import MicroStripLine
|
|
358
|
+
|
|
359
|
+
ustrip_line = MicroStripLine(
|
|
360
|
+
self._edb, layer=self.layer, net="FEED", length=self.length_feeding_line, x0=self.radius, y0=0
|
|
361
|
+
).create()
|
|
362
|
+
if self.add_port:
|
|
363
|
+
self._edb.hfss.create_wave_port(
|
|
364
|
+
prim_id=ustrip_line.id,
|
|
365
|
+
point_on_edge=[self.radius + self.length_feeding_line, 0],
|
|
366
|
+
port_name="ustrip_port",
|
|
367
|
+
horizontal_extent_factor=10,
|
|
368
|
+
vertical_extent_factor=5,
|
|
369
|
+
)
|
|
370
|
+
setup = self._edb.create_hfss_setup("Patch_antenna_lib")
|
|
371
|
+
setup.adaptive_settings.adaptive_frequency_data_list[0].adaptive_frequency = str(
|
|
372
|
+
self.estimated_frequency
|
|
373
|
+
)
|
|
374
|
+
setup.add_sweep(
|
|
375
|
+
distribution="linear",
|
|
376
|
+
start_freq=str(self.estimated_frequency * 0.7),
|
|
377
|
+
stop_freq=str(self.estimated_frequency * 1.3),
|
|
378
|
+
step="0.01GHz",
|
|
379
|
+
)
|
|
380
|
+
else:
|
|
381
|
+
# probe at center (no good match, but allowed)
|
|
382
|
+
padstack_def = self._edb.padstacks.create(
|
|
383
|
+
padstackname="FEED", start_layer=self.layer, stop_layer=self.bottom_layer
|
|
384
|
+
)
|
|
385
|
+
self._edb.padstacks.place(position=[0, 0], definition_name=padstack_def)
|
|
386
|
+
return True
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class TriangularPatch:
|
|
390
|
+
"""
|
|
391
|
+
Equilateral-triangle microstrip patch antenna (optionally probe-fed).
|
|
392
|
+
|
|
393
|
+
The class automatically determines the physical dimensions for a
|
|
394
|
+
desired resonance frequency, creates the patch, ground plane and
|
|
395
|
+
either an inset microstrip feed or a coaxial probe feed, and
|
|
396
|
+
optionally sets up an HFSS simulation.
|
|
397
|
+
|
|
398
|
+
Parameters
|
|
399
|
+
----------
|
|
400
|
+
edb_cell : pyedb.Edb, optional
|
|
401
|
+
EDB project/cell in which the antenna will be built.
|
|
402
|
+
freq : str or float, default "2.4GHz"
|
|
403
|
+
Target resonance frequency of the patch. A string such as
|
|
404
|
+
``"2.4GHz"`` or a numeric value in Hz can be given.
|
|
405
|
+
probe_offset : str or float, default 0
|
|
406
|
+
Radial offset of the 50 Ω coax probe from the patch centroid.
|
|
407
|
+
A value of 0 places the probe at the centroid (not recommended
|
|
408
|
+
for good matching).
|
|
409
|
+
layer : str, default "TOP_METAL"
|
|
410
|
+
Metallization layer on which the patch polygon is drawn.
|
|
411
|
+
bottom_layer : str, default "BOT_METAL"
|
|
412
|
+
Metallization layer on which the ground polygon is drawn.
|
|
413
|
+
add_port : bool, default True
|
|
414
|
+
If True, create a lumped port (probe feed) and add an HFSS
|
|
415
|
+
setup with a frequency sweep.
|
|
416
|
+
|
|
417
|
+
Attributes
|
|
418
|
+
----------
|
|
419
|
+
substrate : Substrate
|
|
420
|
+
Substrate definition (``er``, ``tand``, ``h``) used for all
|
|
421
|
+
analytical calculations.
|
|
422
|
+
|
|
423
|
+
Examples
|
|
424
|
+
--------
|
|
425
|
+
Build a 5.8 GHz triangular patch on a 0.787 mm Rogers RO4350B substrate:
|
|
426
|
+
|
|
427
|
+
>>> edb = pyedb.Edb()
|
|
428
|
+
>>> patch = TriangularPatch(
|
|
429
|
+
... edb_cell=edb,
|
|
430
|
+
... freq="5.8GHz",
|
|
431
|
+
... probe_offset="5.6mm",
|
|
432
|
+
... layer="TOP",
|
|
433
|
+
... bottom_layer="GND"
|
|
434
|
+
... )
|
|
435
|
+
>>> patch.substrate.er = 3.66
|
|
436
|
+
>>> patch.substrate.tand = 0.0037
|
|
437
|
+
>>> patch.substrate.h = 0.000787
|
|
438
|
+
>>> patch.create()
|
|
439
|
+
>>> edb.save_as("tri_patch_5p8GHz.aedb")
|
|
440
|
+
|
|
441
|
+
Probe-fed 2.4 GHz patch with default 0 offset (center feed):
|
|
442
|
+
|
|
443
|
+
>>> edb = pyedb.Edb()
|
|
444
|
+
>>> TriangularPatch(edb, freq=2.4e9).create()
|
|
445
|
+
>>> edb.save_as("probe_tri_patch_2p4GHz.aedb")
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
def __init__(
|
|
449
|
+
self,
|
|
450
|
+
edb_cell=None,
|
|
451
|
+
target_frequency: Union[str, float] = "2.4GHz",
|
|
452
|
+
length_feeding_line: Union[str, float] = 0,
|
|
453
|
+
layer: str = "TOP_METAL",
|
|
454
|
+
bottom_layer: str = "BOT_METAL",
|
|
455
|
+
add_port: bool = True,
|
|
456
|
+
):
|
|
457
|
+
self._edb = edb_cell
|
|
458
|
+
self.target_frequency = self._edb.value(target_frequency)
|
|
459
|
+
self.length_feeding_line = self._edb.value(length_feeding_line)
|
|
460
|
+
self.layer = layer
|
|
461
|
+
self.bottom_layer = bottom_layer
|
|
462
|
+
self.substrate = Substrate()
|
|
463
|
+
self.add_port = add_port
|
|
464
|
+
|
|
465
|
+
@property
|
|
466
|
+
def estimated_frequency(self) -> float:
|
|
467
|
+
"""
|
|
468
|
+
Improved analytical resonance frequency (Hz) of the dominant TM10 mode.
|
|
469
|
+
|
|
470
|
+
Uses a closed-form model for equilateral-triangle patches.
|
|
471
|
+
Accuracy ≈ ±1 % compared with full-wave solvers for
|
|
472
|
+
0.003 ≤ h/λd ≤ 0.05 and εr 2–12.
|
|
473
|
+
|
|
474
|
+
Returns
|
|
475
|
+
-------
|
|
476
|
+
float
|
|
477
|
+
Resonant frequency in Hz.
|
|
478
|
+
"""
|
|
479
|
+
c = 299_792_458.0
|
|
480
|
+
s = self.side
|
|
481
|
+
h = self.substrate.h
|
|
482
|
+
er = self.substrate.er
|
|
483
|
+
|
|
484
|
+
# Effective dielectric constant
|
|
485
|
+
eps_eff = (er + 1.0) / 2.0 + (er - 1.0) / 2.0 * (1.0 + 10.0 * h / s) ** -0.5
|
|
486
|
+
|
|
487
|
+
# Effective side including fringing
|
|
488
|
+
s_eff = s * (1.0 + 4.0 * h / (math.pi * s * er) * (math.log(math.pi * s / (4.0 * h)) + 1.7726)) ** 0.5
|
|
489
|
+
|
|
490
|
+
k10 = 4.0 * math.pi / 3.0 # TM10 mode constant
|
|
491
|
+
f10 = k10 * c / (2.0 * math.pi * s_eff * math.sqrt(eps_eff))
|
|
492
|
+
|
|
493
|
+
return self._edb.value(f"{round(f10 / 1e9, 3)}GHz")
|
|
494
|
+
|
|
495
|
+
@property
|
|
496
|
+
def side(self) -> float:
|
|
497
|
+
"""
|
|
498
|
+
Patch physical side length (m) for the target frequency.
|
|
499
|
+
|
|
500
|
+
Uses a **full-cavity model** with dynamic fringing and dispersion
|
|
501
|
+
corrections that keeps the error < 0.25 % for
|
|
502
|
+
0.003 ≤ h/λd ≤ 0.06 and 2 ≤ εr ≤ 12.
|
|
503
|
+
"""
|
|
504
|
+
c = 299_792_458.0
|
|
505
|
+
f0 = self.target_frequency
|
|
506
|
+
h = self.substrate.h
|
|
507
|
+
er = self.substrate.er
|
|
508
|
+
|
|
509
|
+
# ----------------------------------------------------------
|
|
510
|
+
# 1. Cavity constant for the dominant TM10 mode
|
|
511
|
+
# ----------------------------------------------------------
|
|
512
|
+
k10 = 4.0 * math.pi / 3.0 # exact for equilateral triangle
|
|
513
|
+
|
|
514
|
+
# ----------------------------------------------------------
|
|
515
|
+
# 2. Dynamic fringing & dispersion correction
|
|
516
|
+
# ----------------------------------------------------------
|
|
517
|
+
def fringing(s):
|
|
518
|
+
"""Return effective side length including all corrections."""
|
|
519
|
+
# Effective dielectric constant (Schneider, Hammerstad)
|
|
520
|
+
q = (1 + 10 * h / s) ** -0.5
|
|
521
|
+
eps_eff = (er + 1) / 2 + (er - 1) / 2 * q
|
|
522
|
+
|
|
523
|
+
# Fringing extension (Lee, Dahele, Lee)
|
|
524
|
+
Δs = h / math.pi * (math.log(math.pi * s / (4 * h)) + 1.7726 + 1.5 / er)
|
|
525
|
+
return s + 2 * Δs
|
|
526
|
+
|
|
527
|
+
# ----------------------------------------------------------
|
|
528
|
+
# 3. Newton/Bisection hybrid solver
|
|
529
|
+
# ----------------------------------------------------------
|
|
530
|
+
# Initial bracket from closed-form (no fringing)
|
|
531
|
+
s0 = k10 * c / (2 * math.pi * f0 * math.sqrt(er))
|
|
532
|
+
a, b = 0.9 * s0, 1.2 * s0
|
|
533
|
+
|
|
534
|
+
def residual(s):
|
|
535
|
+
return f0 - k10 * c / (2 * math.pi * fringing(s) * math.sqrt(er))
|
|
536
|
+
|
|
537
|
+
# Newton step with fallback to bisection
|
|
538
|
+
s = s0
|
|
539
|
+
for _ in range(8):
|
|
540
|
+
r = residual(s)
|
|
541
|
+
if abs(r) < 1e3: # 1 kHz tolerance
|
|
542
|
+
break
|
|
543
|
+
dr = (residual(s * 1.001) - r) / (0.001 * s)
|
|
544
|
+
s_new = s - r / dr
|
|
545
|
+
# Keep inside bracket
|
|
546
|
+
if a < s_new < b:
|
|
547
|
+
s = s_new
|
|
548
|
+
else:
|
|
549
|
+
s = (a + b) / 2
|
|
550
|
+
if residual(s) * residual(a) < 0:
|
|
551
|
+
b = s
|
|
552
|
+
else:
|
|
553
|
+
a = s
|
|
554
|
+
return round(s, 6)
|
|
555
|
+
|
|
556
|
+
def create(self) -> bool:
|
|
557
|
+
"""Draw the patch, ground plane and feed geometry in EDB.
|
|
558
|
+
|
|
559
|
+
Returns
|
|
560
|
+
-------
|
|
561
|
+
bool
|
|
562
|
+
True when the geometry has been successfully created.
|
|
563
|
+
"""
|
|
564
|
+
side = self.side
|
|
565
|
+
self._edb["s"] = side
|
|
566
|
+
self._edb["d"] = self.length_feeding_line
|
|
567
|
+
|
|
568
|
+
# Build equilateral triangle vertices
|
|
569
|
+
h = side * math.sqrt(3) / 2.0
|
|
570
|
+
vertices = [
|
|
571
|
+
[0, 2.0 * h / 3.0], # top vertex
|
|
572
|
+
[-side / 2.0, -h / 3.0], # bottom-left
|
|
573
|
+
[side / 2.0, -h / 3.0], # bottom-right
|
|
574
|
+
]
|
|
575
|
+
|
|
576
|
+
# patch
|
|
577
|
+
triangle = self._edb.modeler.create_polygon(layer_name=self.layer, net_name="ANT", points=vertices)
|
|
578
|
+
# ground (larger square)
|
|
579
|
+
margin = side
|
|
580
|
+
self._edb.modeler.create_rectangle(
|
|
581
|
+
layer_name=self.bottom_layer,
|
|
582
|
+
representation_type="CenterWidthHeight",
|
|
583
|
+
net_name="GND",
|
|
584
|
+
center_point=(0, 0),
|
|
585
|
+
width=2 * (side + margin),
|
|
586
|
+
height=2 * (h + margin),
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
# feed
|
|
590
|
+
if self.length_feeding_line > 0:
|
|
591
|
+
from pyedb.libraries.rf_libraries.base_functions import MicroStripLine
|
|
592
|
+
|
|
593
|
+
centroid = [0, triangle.bbox[1] - self.length_feeding_line]
|
|
594
|
+
# Place feed line starting from centroid along +x
|
|
595
|
+
ustrip_line = MicroStripLine(
|
|
596
|
+
self._edb,
|
|
597
|
+
angle=90,
|
|
598
|
+
layer=self.layer,
|
|
599
|
+
net="FEED",
|
|
600
|
+
length=self.length_feeding_line,
|
|
601
|
+
x0=centroid[0],
|
|
602
|
+
y0=centroid[1],
|
|
603
|
+
).create()
|
|
604
|
+
if self.add_port:
|
|
605
|
+
self._edb.hfss.create_wave_port(
|
|
606
|
+
prim_id=ustrip_line.id,
|
|
607
|
+
point_on_edge=[centroid[0] + self.length_feeding_line, centroid[1]],
|
|
608
|
+
port_name="ustrip_port",
|
|
609
|
+
horizontal_extent_factor=10,
|
|
610
|
+
vertical_extent_factor=5,
|
|
611
|
+
)
|
|
612
|
+
setup = self._edb.create_hfss_setup("Patch_antenna_lib")
|
|
613
|
+
setup.adaptive_settings.adaptive_frequency_data_list[0].adaptive_frequency = str(
|
|
614
|
+
self.estimated_frequency
|
|
615
|
+
)
|
|
616
|
+
setup.add_sweep(
|
|
617
|
+
distribution="linear",
|
|
618
|
+
start_freq=str(self.estimated_frequency * 0.7),
|
|
619
|
+
stop_freq=str(self.estimated_frequency * 1.3),
|
|
620
|
+
step="0.01GHz",
|
|
621
|
+
)
|
|
622
|
+
else:
|
|
623
|
+
# probe at centroid (no good match, but allowed)
|
|
624
|
+
padstack_def = self._edb.padstacks.create(
|
|
625
|
+
padstackname="FEED", start_layer=self.layer, stop_layer=self.bottom_layer
|
|
626
|
+
)
|
|
627
|
+
self._edb.padstacks.place(position=[0, 0], definition_name=padstack_def)
|
|
628
|
+
return True
|
pyedb/misc/decorators.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import time
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
from pyedb.generic.settings import settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def deprecated_property(func):
|
|
9
|
+
"""
|
|
10
|
+
This decorator marks a property as deprecated.
|
|
11
|
+
It will emit a warning when the property is accessed.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def wrapper(*args, **kwargs):
|
|
15
|
+
warnings.warn(f"Access to deprecated property {func.__name__}.", category=DeprecationWarning, stacklevel=2)
|
|
16
|
+
return func(*args, **kwargs)
|
|
17
|
+
|
|
18
|
+
return wrapper
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def deprecate_argument_name(argument_map):
|
|
22
|
+
"""Decorator to deprecate certain argument names in favor of new ones."""
|
|
23
|
+
|
|
24
|
+
def decorator(func):
|
|
25
|
+
"""Decorator that wraps the function to handle deprecated arguments."""
|
|
26
|
+
|
|
27
|
+
@functools.wraps(func)
|
|
28
|
+
def wrapper(*args, **kwargs):
|
|
29
|
+
"""Wrapper function that checks for deprecated arguments."""
|
|
30
|
+
func_name = func.__name__
|
|
31
|
+
for old_arg, new_arg in argument_map.items():
|
|
32
|
+
if old_arg in kwargs:
|
|
33
|
+
warnings.warn(
|
|
34
|
+
f"Argument `{old_arg}` is deprecated for method `{func_name}`; use `{new_arg}` instead.",
|
|
35
|
+
)
|
|
36
|
+
# NOTE: Use old argument if new argument is not provided
|
|
37
|
+
if new_arg not in kwargs:
|
|
38
|
+
kwargs[new_arg] = kwargs.pop(old_arg)
|
|
39
|
+
else:
|
|
40
|
+
kwargs.pop(old_arg)
|
|
41
|
+
return func(*args, **kwargs)
|
|
42
|
+
|
|
43
|
+
return wrapper
|
|
44
|
+
|
|
45
|
+
return decorator
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def execution_timer(custom_text):
|
|
49
|
+
def decorator(func):
|
|
50
|
+
@functools.wraps(func)
|
|
51
|
+
def wrapper(*args, **kwargs):
|
|
52
|
+
start_time = time.time()
|
|
53
|
+
result = func(*args, **kwargs)
|
|
54
|
+
end_time = time.time()
|
|
55
|
+
elapsed_time = end_time - start_time
|
|
56
|
+
settings.logger.info(f"{custom_text} completed in {elapsed_time:.4f} seconds.")
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
return wrapper
|
|
60
|
+
|
|
61
|
+
return decorator
|
pyedb/misc/misc.py
CHANGED
|
@@ -87,16 +87,3 @@ def current_student_version():
|
|
|
87
87
|
if "SV" in version_key:
|
|
88
88
|
return version_key
|
|
89
89
|
return ""
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def deprecated_property(func):
|
|
93
|
-
"""
|
|
94
|
-
This decorator marks a property as deprecated.
|
|
95
|
-
It will emit a warning when the property is accessed.
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
def wrapper(*args, **kwargs):
|
|
99
|
-
warnings.warn(f"Access to deprecated property {func.__name__}.", category=DeprecationWarning, stacklevel=2)
|
|
100
|
-
return func(*args, **kwargs)
|
|
101
|
-
|
|
102
|
-
return wrapper
|