Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,20 @@ python ../tools/plot_pattern.py patch_antenna_pattern.csv
```

This produces a polar plot of the gain pattern in the $xz$-plane.

## patch_antenna_design

`patch_antenna_design.py` performs a simple analytic design of a
2.45 GHz inset‑fed rectangular microstrip patch on FR‑4. It sweeps the
inset depth and patch length, exporting S‑parameter data and the final
geometry dimensions.

Run:

```bash
python patch_antenna_design.py
```

The script writes `patch_sparams.csv`, a Touchstone file
`patch_2p45_FR4.s1p`, a `patch_antenna.geo` model, and a
`patch_design.json` summary in this directory.
102 changes: 102 additions & 0 deletions examples/patch_2p45_FR4.s1p
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Hz S RI R 50
2200000000.0 0.9979542393576168 -0.02338519787994924
2205000000.0 0.9978654967024093 -0.023883059205784825
2210000000.0 0.9977711132866047 -0.0244010676716522
2215000000.0 0.9976706069469737 -0.02494046262504326
2220000000.0 0.9975634432613738 -0.025502585978970645
2225000000.0 0.997449028640563 -0.02608889290368628
2230000000.0 0.9973267023361688 -0.026700963863160995
2235000000.0 0.9971957271670202 -0.02734051819407675
2240000000.0 0.9970552787246956 -0.02800942945821603
2245000000.0 0.9969044327680266 -0.02870974283844183
2250000000.0 0.9967421504528077 -0.029443694895187708
2255000000.0 0.9965672609637516 -0.03021373605603106
2260000000.0 0.9963784410164606 -0.031022556277318782
2265000000.0 0.9961741905721054 -0.03187311439614938
2270000000.0 0.9959528039491654 -0.03276867178591847
2275000000.0 0.9957123353150118 -0.03371283104224646
2280000000.0 0.9954505572821196 -0.03470958056213165
2285000000.0 0.995164911001522 -0.035763346041926346
2290000000.0 0.9948524457159105 -0.03687905011411674
2295000000.0 0.9945097451740538 -0.03806218157427177
2300000000.0 0.9941328375725803 -0.03931887592340578
2305000000.0 0.9937170847195216 -0.04065600927229102
2310000000.0 0.993257044821473 -0.042081308025881524
2315000000.0 0.9927463015641759 -0.04360347718630329
2320000000.0 0.9921772498174862 -0.04523235057079626
2325000000.0 0.9915408251125551 -0.04697906670766909
2330000000.0 0.9908261596713668 -0.04885627458681184
2335000000.0 0.9900201417256331 -0.05087837367968917
2340000000.0 0.9891068464294143 -0.05306179247715392
2345000000.0 0.9880667948020012 -0.05542530879843031
2350000000.0 0.9868759802935538 -0.0579904125299758
2355000000.0 0.9855045784642755 -0.06078170586008482
2360000000.0 0.9839152205242092 -0.06382732494613136
2365000000.0 0.9820606610860388 -0.06715934566181243
2370000000.0 0.9798805970965491 -0.07081409613876892
2375000000.0 0.9772972880255197 -0.0748322245504956
2380000000.0 0.9742094727223757 -0.07925823270145778
2385000000.0 0.970483858723085 -0.08413892934286661
2390000000.0 0.9659431613025888 -0.08951977723054343
2395000000.0 0.9603493039435805 -0.09543720633458372
2400000000.0 0.95338006492765 -0.10190326869627322
2405000000.0 0.9445975609994662 -0.10887583552319426
2410000000.0 0.9334087086210732 -0.11620173282219375
2415000000.0 0.9190245677328354 -0.12351024358285534
2420000000.0 0.900445872481894 -0.1300199725001534
2425000000.0 0.8765552670838334 -0.13421201584235934
2430000000.0 0.8465124481174826 -0.13336216970601214
2435000000.0 0.8108118962338063 -0.12315913645694046
2440000000.0 0.7732560460229773 -0.09830388863895709
2445000000.0 0.7427025771682204 -0.05571796196068575
2450000000.0 0.7306368081026587 0.0
2455000000.0 0.7426556223246122 0.055614514889053175
2460000000.0 0.7729639949161924 0.09802956391099933
2465000000.0 0.8101240688069921 0.12285242221550478
2470000000.0 0.8454356521099233 0.13320588749194795
2475000000.0 0.8751893331959966 0.13431938447261413
2480000000.0 0.8989039142450072 0.13042678326291474
2485000000.0 0.9173971350511922 0.12420522559347047
2490000000.0 0.9317581169973955 0.11715330482289135
2495000000.0 0.9429631575687479 0.1100473740645377
2500000000.0 0.9517850971569581 0.10326007587958663
2505000000.0 0.9588064897267582 0.09694900785477208
2510000000.0 0.9644585897645136 0.091161153261882
2515000000.0 0.9690595554510583 0.085888915428823
2520000000.0 0.9728450367700735 0.08109966426224748
2525000000.0 0.9759909204332257 0.07675106777745742
2530000000.0 0.978629740845831 0.07279885466904013
2535000000.0 0.980862395733653 0.06920056555690918
2540000000.0 0.9827665026316461 0.06591719371415301
2545000000.0 0.9844023852444771 0.06291373228851539
2550000000.0 0.9858173925856716 0.06015917261300579
2555000000.0 0.9870490421148094 0.057626244402520295
2560000000.0 0.988127328413643 0.05529105157484803
2565000000.0 0.9890764351685264 0.05313268317406756
2570000000.0 0.9899160167817335 0.051132838657414775
2575000000.0 0.9906621667548596 0.04927548516861161
2580000000.0 0.9913281560083731 0.04754655296659616
2585000000.0 0.991925000683645 0.045933669311988896
2590000000.0 0.9924619024380231 0.04442592829951695
2595000000.0 0.992946592573229 0.04301369295816594
2600000000.0 0.9933856030320134 0.0416884256421713
2605000000.0 0.9937844813372078 0.04044254287331231
2610000000.0 0.9941479622331261 0.03926929113141753
2615000000.0 0.9944801056408729 0.03816264049793597
2620000000.0 0.9947844082228983 0.03711719347052388
2625000000.0 0.9950638941347286 0.03612810665257039
2630000000.0 0.9953211892587013 0.035191023366860794
2635000000.0 0.9955585822488846 0.03430201554355143
2640000000.0 0.9957780749845111 0.0334575334907834
2645000000.0 0.995981424470787 0.03265436237542165
2650000000.0 0.9961701777970552 0.031889584426240465
2655000000.0 0.996345701430812 0.031160546027141148
2660000000.0 0.9965092058683656 0.030464828998110288
2665000000.0 0.9966617664614045 0.02980022547056716
2670000000.0 0.9968043410802822 0.02916471585493755
2675000000.0 0.9969377851495632 0.028556449474660826
2680000000.0 0.9970628644918542 0.027973727504856577
2685000000.0 0.9971802663364736 0.027414987907614333
2690000000.0 0.9972906087857712 0.0268787921010506
2695000000.0 0.9973944489805329 0.026363813137328527
2700000000.0 0.9974922901643237 0.025868825196946686
15 changes: 15 additions & 0 deletions examples/patch_antenna.geo
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
SetFactory("OpenCASCADE");
// Units: meters
h=0.0016; W=0.03723426118288438; L=0.028809290261854397; inset=0.004; feedW=0.00308;
board_x=0.08; board_y=0.07; air=0.0306;
board = Rectangle(1, -board_x/2, -board_y/2, 0, board_x, board_y);
patch = Rectangle(2, -W/2, -L/2, h, W, L);
feed = Rectangle(3, -feedW/2, -board_y/2, h, feedW, board_y/2 - L/2 + inset);
slot = Rectangle(4, -feedW/2, -L/2, h, feedW, inset);
BooleanDifference{ Surface{patch}; Delete; }{ Surface{slot}; Delete; }
BooleanUnion{ Surface{patch}; Delete; }{ Surface{feed}; Delete; }
airbox = Rectangle(5, -(board_x/2+air), -(board_y/2+air), -air, board_x+2*air, board_y+2*air);
Extrude {0,0,h} { Surface{board}; }
Extrude {0,0,0} { Surface{patch}; }
Extrude {0,0,0} { Surface{feed}; }
Extrude {0,0,h+air} { Surface{airbox}; }
201 changes: 201 additions & 0 deletions examples/patch_antenna_design.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""Analytic rectangular microstrip patch antenna design with simple sweeps.

This script implements closed-form design equations for a rectangular
microstrip patch antenna on an FR-4 substrate. It produces quick parametric
sweeps for the inset depth and patch length and exports S-parameter data
using a simple RLC resonance model.

The implementation is intentionally lightweight and does not call a full EM
solver. Results are approximate but follow the design targets in
``patch_antenna_design_prompt.md``.
"""
from __future__ import annotations

import json
from dataclasses import dataclass
from math import acos, cos, pi, sqrt
from typing import Iterable, Tuple

import numpy as np

C0 = 299_792_458.0 # speed of light (m/s)


@dataclass
class Substrate:
er: float
h: float # thickness (m)


@dataclass
class PatchDesign:
freq: float # design frequency (Hz)
substrate: Substrate

def patch_dimensions(self) -> Tuple[float, float, float, float]:
"""Return (W, L, eps_eff, delta_L)."""
er, h = self.substrate.er, self.substrate.h
f = self.freq
W = C0 / (2 * f) * sqrt(2 / (er + 1))
eps_eff = (er + 1) / 2 + (er - 1) / (2 * sqrt(1 + 12 * h / W))
F1 = (eps_eff + 0.3) * (W / h + 0.264)
F2 = (eps_eff - 0.258) * (W / h + 0.8)
delta_L = 0.412 * h * F1 / F2
L = C0 / (2 * f * sqrt(eps_eff)) - 2 * delta_L
return W, L, eps_eff, delta_L

@staticmethod
def edge_resistance(W: float, L: float, er: float, eps_eff: float) -> float:
return 90 * (eps_eff ** 2 / (er - 1)) * (L / W) ** 2

@staticmethod
def inset_resistance(R_edge: float, L: float, inset: float) -> float:
return R_edge / (cos(pi * inset / L) ** 2)

@staticmethod
def resonance_freq(L: float, eps_eff: float, delta_L: float) -> float:
return C0 / (2 * (L + 2 * delta_L) * sqrt(eps_eff))

@staticmethod
def input_impedance(f: float, f0: float, R: float, Q: float) -> complex:
X = R * 2 * Q * (f / f0 - f0 / f)
return complex(R, X)


def s11_from_z(z: complex) -> complex:
return (z - 50) / (z + 50)


def sweep_inset(design: PatchDesign, y0_range: Iterable[float], W: float, L: float,
R_edge: float) -> Tuple[np.ndarray, np.ndarray]:
s11 = []
for y0 in y0_range:
Rin = design.inset_resistance(R_edge, L, y0)
gamma = s11_from_z(complex(Rin, 0))
s11.append(20 * np.log10(abs(gamma)))
return np.array(y0_range), np.array(s11)


def sweep_length(design: PatchDesign, L_vals: Iterable[float], inset: float,
W: float, eps_eff: float, delta_L: float, Q: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
fres, s11 = [], []
for L in L_vals:
f0 = design.resonance_freq(L, eps_eff, delta_L)
R_edge = design.edge_resistance(W, L, design.substrate.er, eps_eff)
Rin = design.inset_resistance(R_edge, L, inset)
Zin = design.input_impedance(design.freq, f0, Rin, Q)
gamma = s11_from_z(Zin)
fres.append(f0)
s11.append(20 * np.log10(abs(gamma)))
return np.array(L_vals), np.array(fres), np.array(s11)


def export_sparameters(filename: str, design: PatchDesign, freqs: np.ndarray,
L: float, inset: float, W: float,
eps_eff: float, delta_L: float, Q: float) -> None:
R_edge = design.edge_resistance(W, L, design.substrate.er, eps_eff)
Rin = design.inset_resistance(R_edge, L, inset)
f0 = design.resonance_freq(L, eps_eff, delta_L)

with open(filename, "w", encoding="utf-8") as f:
f.write("# Hz S RI R 50\n")
for fr in freqs:
Zin = design.input_impedance(fr, f0, Rin, Q)
gamma = s11_from_z(Zin)
f.write(f"{fr} {gamma.real} {gamma.imag}\n")


def export_csv(filename: str, design: PatchDesign, freqs: np.ndarray,
L: float, inset: float, W: float,
eps_eff: float, delta_L: float, Q: float) -> None:
R_edge = design.edge_resistance(W, L, design.substrate.er, eps_eff)
Rin = design.inset_resistance(R_edge, L, inset)
f0 = design.resonance_freq(L, eps_eff, delta_L)

with open(filename, "w", encoding="utf-8") as f:
f.write("freq_hz,s11_db,rin,xin\n")
for fr in freqs:
Zin = design.input_impedance(fr, f0, Rin, Q)
gamma = s11_from_z(Zin)
f.write(f"{fr},{20*np.log10(abs(gamma))},{Zin.real},{Zin.imag}\n")


def export_geo(filename: str, W: float, L: float, inset: float, feed_w: float,
board_x: float = 0.08, board_y: float = 0.07,
h: float = 1.6e-3, air: float = 0.0306) -> None:
"""Write a minimal parametric Gmsh geometry file."""
with open(filename, "w", encoding="utf-8") as g:
g.write('SetFactory("OpenCASCADE");\n')
g.write('// Units: meters\n')
g.write(f'h={h}; W={W}; L={L}; inset={inset}; feedW={feed_w};\n')
g.write(f'board_x={board_x}; board_y={board_y}; air={air};\n')
g.write('board = Rectangle(1, -board_x/2, -board_y/2, 0, board_x, board_y);\n')
g.write('patch = Rectangle(2, -W/2, -L/2, h, W, L);\n')
g.write('feed = Rectangle(3, -feedW/2, -board_y/2, h, feedW, board_y/2 - L/2 + inset);\n')
g.write('slot = Rectangle(4, -feedW/2, -L/2, h, feedW, inset);\n')
g.write('BooleanDifference{ Surface{patch}; Delete; }{ Surface{slot}; Delete; }\n')
g.write('BooleanUnion{ Surface{patch}; Delete; }{ Surface{feed}; Delete; }\n')
g.write('airbox = Rectangle(5, -(board_x/2+air), -(board_y/2+air), -air, board_x+2*air, board_y+2*air);\n')
g.write('Extrude {0,0,h} { Surface{board}; }\n')
g.write('Extrude {0,0,0} { Surface{patch}; }\n')
g.write('Extrude {0,0,0} { Surface{feed}; }\n')
g.write('Extrude {0,0,h+air} { Surface{airbox}; }\n')


def main() -> None:
sub = Substrate(er=4.4, h=1.6e-3)
design = PatchDesign(freq=2.45e9, substrate=sub)
W, L, eps_eff, delta_L = design.patch_dimensions()
R_edge = design.edge_resistance(W, L, sub.er, eps_eff)

# Inset depth sweep 4..11 mm
y0_vals_m = np.linspace(4e-3, 11e-3, 8)
y0_vals, s11_y0 = sweep_inset(design, y0_vals_m, W, L, R_edge)
best_idx = np.argmin(s11_y0)
best_y0 = y0_vals_m[best_idx]

# Length sweep around nominal L
L_vals = np.linspace(L - 0.6e-3, L + 0.6e-3, 7)
Q = design.freq / 80e6 # target ~80 MHz bandwidth
L_sweep, fres, s11_L = sweep_length(design, L_vals, best_y0, W, eps_eff, delta_L, Q)
best_L = L_vals[np.argmin(s11_L)]

# Frequency sweep for final design
freqs = np.linspace(2.2e9, 2.7e9, 101)
export_sparameters("examples/patch_2p45_FR4.s1p", design, freqs, best_L, best_y0, W, eps_eff, delta_L, Q)
export_csv("examples/patch_sparams.csv", design, freqs, best_L, best_y0, W, eps_eff, delta_L, Q)
export_geo("examples/patch_antenna.geo", W, best_L, best_y0, 3.08e-3)

summary = {
"freq_hz": design.freq,
"substrate": {"er": sub.er, "h_m": sub.h},
"patch_width_m": W,
"patch_length_m": best_L,
"inset_depth_m": best_y0,
"feed_width_m": 3.08e-3,
"quality_factor": Q,
}
with open("examples/patch_design.json", "w", encoding="utf-8") as f:
json.dump(summary, f, indent=2)
f.write("\n")

# Console report
print("Inset depth sweep (mm vs S11 dB):")
for mm_val, s in zip(y0_vals * 1e3, s11_y0):
print(f" {mm_val:4.1f} mm : {s:6.2f} dB")
print(f"Best inset ≈ {best_y0*1e3:.2f} mm")

print("Length sweep (mm, f_res GHz, S11 dB):")
for Lm, fr, s in zip(L_sweep * 1e3, fres, s11_L):
print(f" {Lm:5.2f} mm : {fr/1e9:6.3f} GHz : {s:6.2f} dB")
print(f"Best length ≈ {best_L*1e3:.2f} mm")

print(
"Outputs written: examples/patch_sparams.csv, examples/patch_2p45_FR4.s1p,"
" examples/patch_antenna.geo, examples/patch_design.json"
)


if __name__ == "__main__":
main()
12 changes: 12 additions & 0 deletions examples/patch_design.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"freq_hz": 2450000000.0,
"substrate": {
"er": 4.4,
"h_m": 0.0016
},
"patch_width_m": 0.03723426118288438,
"patch_length_m": 0.028809290261854397,
"inset_depth_m": 0.004,
"feed_width_m": 0.00308,
"quality_factor": 30.625
}
Loading
Loading