pyedb 0.53.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 (119) 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 +20 -9
  13. pyedb/configuration/cfg_s_parameter_models.py +67 -172
  14. pyedb/configuration/cfg_setup.py +102 -295
  15. pyedb/configuration/configuration.py +66 -6
  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 +64 -3
  20. pyedb/dotnet/database/cell/layout_obj.py +3 -3
  21. pyedb/dotnet/database/cell/primitive/path.py +6 -8
  22. pyedb/dotnet/database/cell/primitive/primitive.py +10 -31
  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 +26 -28
  28. pyedb/dotnet/database/cell/voltage_regulator.py +0 -21
  29. pyedb/dotnet/database/components.py +99 -91
  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 +27 -218
  34. pyedb/dotnet/database/dotnet/primitive.py +16 -16
  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 +35 -35
  38. pyedb/dotnet/database/edb_data/padstacks_data.py +65 -90
  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 +8 -4
  42. pyedb/dotnet/database/geometry/point_data.py +14 -10
  43. pyedb/dotnet/database/geometry/polygon_data.py +3 -5
  44. pyedb/dotnet/database/hfss.py +50 -52
  45. pyedb/dotnet/database/layout_validation.py +14 -11
  46. pyedb/dotnet/database/materials.py +10 -11
  47. pyedb/dotnet/database/modeler.py +104 -101
  48. pyedb/dotnet/database/nets.py +20 -23
  49. pyedb/dotnet/database/padstack.py +156 -84
  50. pyedb/dotnet/database/sim_setup_data/data/settings.py +24 -0
  51. pyedb/dotnet/database/sim_setup_data/io/siwave.py +26 -1
  52. pyedb/dotnet/database/siwave.py +47 -47
  53. pyedb/dotnet/database/stackup.py +152 -87
  54. pyedb/dotnet/database/utilities/heatsink.py +4 -4
  55. pyedb/dotnet/database/utilities/obj_base.py +3 -3
  56. pyedb/dotnet/database/utilities/simulation_setup.py +2 -2
  57. pyedb/dotnet/database/utilities/value.py +116 -0
  58. pyedb/dotnet/edb.py +248 -170
  59. pyedb/edb_logger.py +12 -27
  60. pyedb/extensions/via_design_backend.py +6 -3
  61. pyedb/generic/design_types.py +68 -21
  62. pyedb/generic/general_methods.py +0 -120
  63. pyedb/generic/process.py +44 -108
  64. pyedb/generic/settings.py +75 -19
  65. pyedb/grpc/__init__.py +0 -0
  66. pyedb/grpc/database/components.py +55 -17
  67. pyedb/grpc/database/control_file.py +5 -5
  68. pyedb/grpc/database/definition/materials.py +24 -31
  69. pyedb/grpc/database/definition/package_def.py +18 -18
  70. pyedb/grpc/database/definition/padstack_def.py +104 -51
  71. pyedb/grpc/database/geometry/arc_data.py +7 -5
  72. pyedb/grpc/database/geometry/point_3d_data.py +8 -7
  73. pyedb/grpc/database/geometry/polygon_data.py +4 -3
  74. pyedb/grpc/database/hierarchy/component.py +43 -38
  75. pyedb/grpc/database/hierarchy/pin_pair_model.py +15 -14
  76. pyedb/grpc/database/hierarchy/pingroup.py +9 -9
  77. pyedb/grpc/database/layers/stackup_layer.py +45 -44
  78. pyedb/grpc/database/layout/layout.py +17 -13
  79. pyedb/grpc/database/layout/voltage_regulator.py +7 -7
  80. pyedb/grpc/database/layout_validation.py +16 -15
  81. pyedb/grpc/database/modeler.py +60 -58
  82. pyedb/grpc/database/net/net.py +15 -14
  83. pyedb/grpc/database/nets.py +112 -31
  84. pyedb/grpc/database/padstacks.py +303 -190
  85. pyedb/grpc/database/ports/ports.py +5 -6
  86. pyedb/grpc/database/primitive/bondwire.py +8 -7
  87. pyedb/grpc/database/primitive/circle.py +4 -4
  88. pyedb/grpc/database/primitive/padstack_instance.py +191 -23
  89. pyedb/grpc/database/primitive/path.py +7 -7
  90. pyedb/grpc/database/primitive/polygon.py +3 -3
  91. pyedb/grpc/database/primitive/primitive.py +13 -17
  92. pyedb/grpc/database/primitive/rectangle.py +13 -13
  93. pyedb/grpc/database/simulation_setup/hfss_general_settings.py +1 -1
  94. pyedb/grpc/database/simulation_setup/hfss_simulation_setup.py +10 -0
  95. pyedb/grpc/database/simulation_setup/siwave_simulation_setup.py +17 -1
  96. pyedb/grpc/database/siwave.py +31 -25
  97. pyedb/grpc/database/source_excitations.py +335 -233
  98. pyedb/grpc/database/stackup.py +165 -148
  99. pyedb/grpc/database/terminal/bundle_terminal.py +18 -8
  100. pyedb/grpc/database/terminal/edge_terminal.py +10 -0
  101. pyedb/grpc/database/terminal/padstack_instance_terminal.py +16 -5
  102. pyedb/grpc/database/terminal/pingroup_terminal.py +12 -11
  103. pyedb/grpc/database/terminal/point_terminal.py +4 -3
  104. pyedb/grpc/database/terminal/terminal.py +9 -9
  105. pyedb/grpc/database/utility/value.py +109 -0
  106. pyedb/grpc/database/utility/xml_control_file.py +5 -5
  107. pyedb/grpc/edb.py +130 -63
  108. pyedb/grpc/edb_init.py +3 -10
  109. pyedb/grpc/rpc_session.py +10 -10
  110. pyedb/libraries/common.py +366 -0
  111. pyedb/libraries/rf_libraries/base_functions.py +1358 -0
  112. pyedb/libraries/rf_libraries/planar_antennas.py +628 -0
  113. pyedb/misc/decorators.py +61 -0
  114. pyedb/misc/misc.py +0 -13
  115. pyedb/siwave.py +2 -2
  116. {pyedb-0.53.0.dist-info → pyedb-0.55.0.dist-info}/METADATA +2 -3
  117. {pyedb-0.53.0.dist-info → pyedb-0.55.0.dist-info}/RECORD +119 -112
  118. {pyedb-0.53.0.dist-info → pyedb-0.55.0.dist-info}/WHEEL +0 -0
  119. {pyedb-0.53.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
@@ -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