simpleEMS 0.1.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.
simpleEMS/__init__.py ADDED
@@ -0,0 +1,89 @@
1
+ # simpleEMS
2
+ # Copyright (C) 2026 Mohamoud Ali
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Affero General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU Affero General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Affero General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ """
18
+ simpleEMS — A Python library built on openEMS for simplified antenna
19
+ and RF structure design, simulation, optimisation, and visualisation.
20
+ """
21
+
22
+ import importlib
23
+
24
+
25
+ _NAMES_BY_MODULE = {
26
+ "microstrip_line": ["MicrostripLine", "MicrostripLineParams"],
27
+ "patch_antenna": [
28
+ "InsetFedPatchAntenna",
29
+ "InsetFedPatchParams",
30
+ "ProbeFedPatchAntenna",
31
+ "ProbeFedPatchParams",
32
+ ],
33
+ "quarterwave_stub_filter": [
34
+ "QuarterWaveFilterParams",
35
+ "BandStopQuarterWaveFilter",
36
+ "BandPassQuarterWaveFilter",
37
+ ],
38
+ "standalone_model": ["simulate_model"],
39
+ "sim_tools": [
40
+ "DumpType",
41
+ "SimTools",
42
+ "optimize_s11",
43
+ "optimize_s21",
44
+ "optimize_s_params",
45
+ "param_sweep",
46
+ "setup_simulation",
47
+ ],
48
+ }
49
+
50
+ _NAME_TO_MODULE = {}
51
+
52
+ for _module_name, _names in _NAMES_BY_MODULE.items():
53
+ for _name in _names:
54
+ _NAME_TO_MODULE[_name] = _module_name
55
+
56
+
57
+ def __getattr__(name: str) -> object:
58
+ module_name = _NAME_TO_MODULE.get(name)
59
+ if module_name is not None:
60
+ module = importlib.import_module(f".{module_name}", __package__)
61
+ value = getattr(module, name)
62
+ globals()[name] = value
63
+ return value
64
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
65
+
66
+
67
+ __all__ = [
68
+ "InsetFedPatchAntenna",
69
+ "ProbeFedPatchAntenna",
70
+ "MicrostripLine",
71
+ "InsetFedPatchParams",
72
+ "ProbeFedPatchParams",
73
+ "MicrostripLineParams",
74
+ "QuarterWaveFilterParams",
75
+ "BandStopQuarterWaveFilter",
76
+ "BandPassQuarterWaveFilter",
77
+ "DumpType",
78
+ "SimTools",
79
+ "setup_simulation",
80
+ "optimize_s11",
81
+ "optimize_s21",
82
+ "optimize_s_params",
83
+ "param_sweep",
84
+ "simulate_model",
85
+ ]
86
+
87
+ __version__ = "0.1.0"
88
+ __author__ = "Mohamoud Ali"
89
+ __license__ = "AGPL-3.0-or-later"
simpleEMS/calc.py ADDED
@@ -0,0 +1,397 @@
1
+ # simpleEMS
2
+ # Copyright (C) 2026 Mohamoud Ali
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Affero General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU Affero General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Affero General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ """
18
+ Calculation utilities for microstrip patch antenna design.
19
+
20
+ Provides functions for computing patch dimensions, microstrip line
21
+ width from target impedance, phase shift lengths, conductance, and
22
+ inset depths. All formulas follow standard references (Balanis,
23
+ Hammerstad-Jensen).
24
+ """
25
+
26
+ from typing import NamedTuple
27
+
28
+ import numpy as np
29
+ from openEMS.physical_constants import C0
30
+ from scipy.integrate import quad
31
+
32
+ from .sim_tools import m_to_mm, mm_to_m
33
+
34
+
35
+ def calculate_electrical_length_mm(
36
+ elec_length_deg_rad: float,
37
+ eps_eff: float,
38
+ frequency: float,
39
+ radians: bool = False,
40
+ ) -> float:
41
+ """
42
+ Compute transmission line length (mm) required to obtain
43
+ a specified phase shift at a given frequency.
44
+
45
+ Parameters
46
+ ----------
47
+ elec_length_deg_rad : float
48
+ Phase shift in degrees (default) or radians.
49
+ eps_eff : float
50
+ Effective dielectric constant.
51
+ frequency : float
52
+ Frequency in Hz.
53
+ radians : bool
54
+ If True, elec_length_deg_rad is already in radians.
55
+
56
+ Returns
57
+ -------
58
+ float
59
+ Required length in mm.
60
+ """
61
+ theta = elec_length_deg_rad if radians else np.deg2rad(elec_length_deg_rad)
62
+
63
+ # Guided phase constant (rad/m)
64
+ beta = 2 * np.pi * frequency * np.sqrt(eps_eff) / C0
65
+
66
+ # Length in meters
67
+ length_m = theta / beta
68
+
69
+ return length_m * 1e3
70
+
71
+
72
+ def microstrip_width_from_impedance(
73
+ charac_imp: float,
74
+ subs_height: float,
75
+ copper_thickness: float,
76
+ subst_eps_r: float,
77
+ freq_hz: float,
78
+ tolerance: float = 0.01,
79
+ ) -> tuple[float, float]:
80
+ """
81
+ Calculates microstrip width for a target impedance including dispersion
82
+ and thickness using Hammerstad-Jensen equations.
83
+
84
+ Parameters
85
+ ----------
86
+
87
+ charac_imp: float
88
+ The required characteristic impedance of the microstrip line
89
+ subs_height: float
90
+ The height of the substrate
91
+ copper_thickness: float
92
+ The thickness of the copper trace
93
+ subst_eps_r: float
94
+ Dielectric constant of the substrate material.
95
+ freq_hz: float
96
+ Frequency in Hz
97
+ tolerance: float
98
+ Maximum deviation allowed for calculated characteristic impedance
99
+
100
+ Returns
101
+ -------
102
+ width: float
103
+ Width of trace for given characteristic impedance in mm.
104
+ er_eff: float
105
+ Effective dielectric constant at the given frequency.
106
+
107
+ Notes
108
+ -----
109
+ For the formulas used, refer to:
110
+ `<https://qucs.github.io/docs/technical/technical.pdf>`_.
111
+ `<https://ieeexplore.ieee.org/document/1124303>`_.
112
+ """
113
+
114
+ def get_Z_at_freq(
115
+ w: float,
116
+ h: float,
117
+ t: float,
118
+ er: float,
119
+ f_hz: float,
120
+ ) -> tuple[float, float]:
121
+ """Calculate microstrip characteristic impedance at a given
122
+ frequency using Hammerstad-Jensen equations."""
123
+ # Constants
124
+ eta0 = 376.730313668
125
+ mue0 = 4 * np.pi * 1e-7
126
+ h_m = mm_to_m(h)
127
+
128
+ if t == 0:
129
+ t = 1e-6
130
+
131
+ # 1. Thickness Correction
132
+ u = w / h
133
+ tau = t / h
134
+ # coth(x) = 1/tanh(x)
135
+ coth_term = 1.0 / np.tanh(np.sqrt(6.517 * u))
136
+ delta_u1 = (tau / np.pi) * np.log(1 + (4 * np.e) / (tau * (coth_term**2)))
137
+ delta_ur = 0.5 * delta_u1 * (1 + 1.0 / np.cosh(np.sqrt(er - 1)))
138
+
139
+ # Effective width ratio to be used in all static formulas
140
+ ur = u + delta_ur
141
+
142
+ # 2. Static Impedance (Air-filled Z01)
143
+ f_ur = 6 + (2 * np.pi - 6) * np.exp(-((30.666 / ur) ** 0.7528))
144
+ Z01 = (eta0 / (2 * np.pi)) * np.log((f_ur / ur) + np.sqrt(1 + (2 / ur) ** 2))
145
+
146
+ # 3. Static Effective Permittivity
147
+ a_u = (
148
+ 1
149
+ + (1 / 49) * np.log((ur**4 + (ur / 52) ** 2) / (ur**4 + 0.432))
150
+ + (1 / 18.7) * np.log(1 + (ur / 18.1) ** 3)
151
+ )
152
+ b_er = 0.564 * ((er - 0.9) / (er + 3)) ** 0.053
153
+ er_eff_0 = (er + 1) / 2 + ((er - 1) / 2) * (1 + 10 / ur) ** (-a_u * b_er)
154
+
155
+ # 4. Static Impedance
156
+ Z0 = Z01 / np.sqrt(er_eff_0)
157
+
158
+ # 5. Frequency Dispersion
159
+ if f_hz <= 0:
160
+ return Z0
161
+
162
+ # Transition frequency (fp) in Hz
163
+ fp = Z0 / (2 * mue0 * h_m)
164
+
165
+ # Dispersion factor G
166
+ G = (np.pi**2 / 12) * ((er - 1) / er_eff_0) * np.sqrt(2 * np.pi * Z0 / eta0)
167
+
168
+ # Frequency-dependent effective permittivity
169
+ er_eff_f = er - (er - er_eff_0) / (1 + G * (f_hz / fp) ** 2)
170
+
171
+ # Frequency-dependent impedance
172
+ Z_f = Z0 * ((er_eff_f - 1) / (er_eff_0 - 1)) * np.sqrt(er_eff_0 / er_eff_f)
173
+
174
+ return Z_f, er_eff_f
175
+
176
+ # --- Convergence check ---
177
+ Z_thinnest, _ = get_Z_at_freq(
178
+ 0.001, subs_height, copper_thickness, subst_eps_r, freq_hz
179
+ )
180
+ Z_widest, _ = get_Z_at_freq(
181
+ 100, subs_height, copper_thickness, subst_eps_r, freq_hz
182
+ )
183
+ Z_min = min(Z_thinnest, Z_widest)
184
+ Z_max = max(Z_thinnest, Z_widest)
185
+ if not (Z_min <= charac_imp <= Z_max):
186
+ raise ValueError(
187
+ f"Target impedance {charac_imp} Ω not realizable on this substrate"
188
+ f" (range [{Z_min:.1f}, {Z_max:.1f}] Ω)"
189
+ )
190
+
191
+ # --- Binary Search for Width ---
192
+ low, high = 0.001, 100
193
+ mid_w = 0.0
194
+ for _ in range(100):
195
+ mid_w = (low + high) / 2
196
+ Z_curr, _ = get_Z_at_freq(
197
+ mid_w, subs_height, copper_thickness, subst_eps_r, freq_hz
198
+ )
199
+
200
+ if abs(Z_curr - charac_imp) < tolerance:
201
+ break
202
+ if Z_curr > charac_imp:
203
+ low = mid_w
204
+ else:
205
+ high = mid_w
206
+ _, er_eff = get_Z_at_freq(
207
+ mid_w, subs_height, copper_thickness, subst_eps_r, freq_hz
208
+ )
209
+
210
+ return mid_w, er_eff
211
+
212
+
213
+ def conductance_G1(patch_width: float, frequency: float) -> float:
214
+ """
215
+ Computes the conductance of patch antenna.
216
+
217
+ Parameters
218
+ ----------
219
+
220
+ patch_width: float
221
+ width of patch antenna in meters
222
+ frequency: float
223
+ frequency in Hz
224
+
225
+ Returns
226
+ -------
227
+ conductance_G1: float
228
+ conductance of patch antenna
229
+
230
+ Notes
231
+ -----
232
+ Refer to Balanis (3rd ed.) 14-12 formula for radiation conductance of one slot.
233
+ """
234
+
235
+ lambda0 = C0 / frequency
236
+ k0 = 2 * np.pi / lambda0
237
+
238
+ def integrand(theta: float) -> float:
239
+ """Integrand for conductance calculation using the cavity model."""
240
+ ct = np.cos(theta)
241
+ if abs(ct) < 1e-5:
242
+ return 0.0
243
+ term = np.sin((k0 * patch_width / 2) * ct) / ct
244
+ return (term**2) * (np.sin(theta) ** 3)
245
+
246
+ integral, _ = quad(integrand, 0, np.pi, limit=1000)
247
+ return integral / (120 * np.pi**2)
248
+
249
+
250
+ def inset_depth(
251
+ charac_imp: float,
252
+ patch_width: float,
253
+ patch_length: float,
254
+ frequency: float,
255
+ ) -> tuple[float, float]:
256
+ """
257
+ Compute the inset feed depth of an inset fed patch antenna
258
+ and probe position of probe fed patch antenna.
259
+
260
+ Parameters
261
+ ----------
262
+ charac_imp: float
263
+ characteristic impedance of the antenna.
264
+ patch_width: float
265
+ patch antenna width in meters.
266
+ patch_length: float
267
+ patch antenna length in meters.
268
+ frequency: float
269
+ frequency in Hz.
270
+
271
+ Returns
272
+ -------
273
+ inset_length: float
274
+ inset length in meters
275
+ probe_pos: float
276
+ probe position in meters. note that probe position is in Y direction and X is 0.
277
+
278
+ Notes
279
+ -----
280
+ Refer to Balanis formula 14-20a
281
+
282
+ """
283
+
284
+ G1 = conductance_G1(patch_width, frequency)
285
+ R_edge = 1.0 / (2.0 * G1)
286
+
287
+ if charac_imp > R_edge:
288
+ raise ValueError("characteristic impedance must be <= edge resistance")
289
+
290
+ inset_length = (patch_length / np.pi) * np.arccos(np.sqrt(charac_imp / R_edge))
291
+ probe_pos = inset_length
292
+ return inset_length, probe_pos
293
+
294
+
295
+ class PatchDims(NamedTuple):
296
+ """
297
+ Named tuple holding all rectangular patch antenna dimensions.
298
+
299
+ Attributes
300
+ ----------
301
+ patch_width_mm : float
302
+ Width of the patch in the x-direction in mm.
303
+ patch_length_mm : float
304
+ Length of the patch in the y-direction in mm.
305
+ inset_length_mm : float
306
+ Inset depth of the patch in mm.
307
+ inset_width_mm : float
308
+ Inset gap width of the patch in mm.
309
+ probe_pos_mm : float
310
+ Feed position for probe-fed patch in the y-direction in mm.
311
+ """
312
+
313
+ patch_width_mm: float
314
+ patch_length_mm: float
315
+ inset_length_mm: float
316
+ inset_width_mm: float
317
+ probe_pos_mm: float
318
+
319
+
320
+ def patch_dims(
321
+ frequency_hz: float,
322
+ subst_eps_r: float,
323
+ subst_height: float,
324
+ charac_imp: float,
325
+ copper_thickness: float,
326
+ ) -> PatchDims:
327
+ """
328
+ Calculate rectangular microstrip patch dimensions.
329
+
330
+ Parameters
331
+ ----------
332
+ frequency_hz: float
333
+ resonant frequency of antenna in Hz.
334
+ subst_eps_r: float
335
+ Dielectric constant of the substrate.
336
+ subst_height: float
337
+ Thickness of the substrate.
338
+ charac_imp: float
339
+ characteristic impedance of the feed line.
340
+ copper_thickness: float
341
+ Thickness of the copper trace.
342
+
343
+ Returns
344
+ -------
345
+ PatchDims
346
+ a named tuple with the following attributes.
347
+ patch_width_mm: float
348
+ Width of the patch antenna in direction of x in mm.
349
+ patch_length_mm: float
350
+ Length of the patch antenna in direction of y in mm
351
+ inset_length_mm: float
352
+ Inset length of the patch in direction of y in mm
353
+ inset_width_mm: float
354
+ Inset width of the patch in the direction of x in mm
355
+ probe_pos_mm: float
356
+ The feed position of probe fed patch antenna in direction of y in mm
357
+ """
358
+
359
+ patch_width = C0 / (2 * frequency_hz) * np.sqrt(2 / (subst_eps_r + 1))
360
+ patch_width_mm = m_to_mm(patch_width)
361
+
362
+ eff_eps_r = (subst_eps_r + 1) / 2 + (subst_eps_r - 1) / 2 * (
363
+ 1 / np.sqrt(1 + 12 * subst_height / patch_width)
364
+ )
365
+
366
+ delta_length = (
367
+ 0.412
368
+ * subst_height
369
+ * ((eff_eps_r + 0.3) * (patch_width / subst_height + 0.264))
370
+ / ((eff_eps_r - 0.258) * (patch_width / subst_height + 0.8))
371
+ )
372
+
373
+ patch_length = (C0 / (2 * frequency_hz * np.sqrt(eff_eps_r))) - 2 * delta_length
374
+ patch_length_mm = m_to_mm(patch_length)
375
+
376
+ inset_length, probe_pos = inset_depth(
377
+ charac_imp, patch_width, patch_length, frequency_hz
378
+ )
379
+
380
+ inset_length_mm = m_to_mm(inset_length)
381
+ probe_pos_mm = m_to_mm(probe_pos)
382
+
383
+ inset_width_mm, _ = microstrip_width_from_impedance(
384
+ charac_imp,
385
+ m_to_mm(subst_height),
386
+ copper_thickness,
387
+ subst_eps_r,
388
+ frequency_hz,
389
+ )
390
+
391
+ return PatchDims(
392
+ patch_width_mm,
393
+ patch_length_mm,
394
+ inset_length_mm,
395
+ inset_width_mm,
396
+ probe_pos_mm,
397
+ )