R reuter /

QFP58

CC BY-SA 4.0 Native

Vibe engineered circular polarized PCB antenna, inspired by the Triple Feed Patch.

Rendered
128 files · 3.7 MB at 05eecdc Download ZIP
.dockerignore 72 B
.gitignore 170 B
CLAUDE.md 11.0 KB
Dockerfile 3.2 KB
fp-lib-table 152 B
QFP58-feed-RHCP-drc.rpt 553 B
README.md 15.5 KB
run.sh 4.2 KB
uv.lock 189 KB
docs/
preview_LHCP.png 195 KB
preview_RHCP.png 195 KB
docs/svg/
QFP58-feed-LHCP-B_Cu.svg 134 KB
QFP58-feed-LHCP-Edge_Cuts.svg 11.5 KB
QFP58-feed-LHCP-F_Cu.svg 61.9 KB
QFP58-feed-RHCP-B_Cu.svg 131 KB
QFP58-feed-RHCP-Edge_Cuts.svg 11.6 KB
QFP58-feed-RHCP-F_Cu.svg 61.9 KB
QFP58-patch-B_Cu.svg 37.2 KB
QFP58-patch-Edge_Cuts.svg 11.9 KB
QFP58-patch-F_Cu.svg 16.3 KB
gerbers/
QFP58-feed-LHCP-fab.zip 31.5 KB
QFP58-feed-RHCP-fab.zip 31.6 KB
QFP58-patch-d23.06-fab.zip 12.0 KB
QFP58-patch-d23.36-fab.zip 12.0 KB
QFP58-patch-fab.zip 11.7 KB
gerbers/QFP58-feed-LHCP/
QFP58-feed-LHCP-B_Cu.gbl 4.2 KB
QFP58-feed-LHCP-B_Mask.gbs 1.6 KB
QFP58-feed-LHCP-B_Silkscreen.gbo 41.5 KB
QFP58-feed-LHCP-Edge_Cuts.gm1 17.0 KB
QFP58-feed-LHCP-F_Cu.gtl 47.5 KB
QFP58-feed-LHCP-F_Mask.gts 1.2 KB
QFP58-feed-LHCP-F_Silkscreen.gto 1.2 KB
QFP58-feed-LHCP-job.gbrjob 2.4 KB
gerbers/QFP58-feed-RHCP/
QFP58-feed-RHCP-B_Cu.gbl 4.2 KB
QFP58-feed-RHCP-B_Mask.gbs 1.6 KB
QFP58-feed-RHCP-B_Silkscreen.gbo 40.6 KB
QFP58-feed-RHCP-Edge_Cuts.gm1 17.0 KB
QFP58-feed-RHCP-F_Cu.gtl 47.5 KB
QFP58-feed-RHCP-F_Mask.gts 1.2 KB
QFP58-feed-RHCP-F_Silkscreen.gto 1.2 KB
QFP58-feed-RHCP-job.gbrjob 2.4 KB
gerbers/QFP58-patch/
QFP58-patch-B_Cu.gbl 21.0 KB
QFP58-patch-B_Mask.gbs 653 B
QFP58-patch-B_Silkscreen.gbo 663 B
QFP58-patch-Edge_Cuts.gm1 640 B
QFP58-patch-F_Cu.gtl 937 B
QFP58-patch-F_Mask.gts 810 B
QFP58-patch-F_Silkscreen.gto 9.4 KB
QFP58-patch-job.gbrjob 2.4 KB
gerbers/QFP58-patch-d23.06/
QFP58-patch-d23.06-B_Cu.gbl 21.0 KB
QFP58-patch-d23.06-B_Mask.gbs 660 B
QFP58-patch-d23.06-B_Silkscreen.gbo 670 B
QFP58-patch-d23.06-Edge_Cuts.gm1 647 B
QFP58-patch-d23.06-F_Cu.gtl 944 B
QFP58-patch-d23.06-F_Mask.gts 817 B
QFP58-patch-d23.06-F_Silkscreen.gto 10.5 KB
QFP58-patch-d23.06-job.gbrjob 2.4 KB
gerbers/QFP58-patch-d23.36/
QFP58-patch-d23.36-B_Cu.gbl 21.1 KB
QFP58-patch-d23.36-B_Mask.gbs 660 B
QFP58-patch-d23.36-B_Silkscreen.gbo 670 B
QFP58-patch-d23.36-Edge_Cuts.gm1 647 B
QFP58-patch-d23.36-F_Cu.gtl 944 B
QFP58-patch-d23.36-F_Mask.gts 817 B
QFP58-patch-d23.36-F_Silkscreen.gto 10.1 KB
QFP58-patch-d23.36-job.gbrjob 2.4 KB
kicad/QFP58-feed-LHCP/
fp-lib-table 149 B
QFP58-feed-LHCP-drc.rpt 553 B
kicad/QFP58-patch/
fp-lib-table 149 B
QFP58-patch-drc.rpt 576 B
kicad/QFP58-patch-d23.06/
fp-lib-table 149 B
QFP58-patch-d23.06-drc.rpt 528 B
kicad/QFP58-patch-d23.36/
fp-lib-table 149 B
QFP58-patch-d23.36-drc.rpt 528 B
kicad/QFP58.pretty/
CouplingDisk_3.6mm.kicad_mod 1.2 KB
FeedPin_1.1mm.kicad_mod 1.0 KB
MountingHole_2.2mm_M2.kicad_mod 1.0 KB
R_0603_Wilkinson.kicad_mod 1.2 KB
SMA_EdgeLaunch_1.0mm_generic.kicad_mod 1.7 KB
sim/full/
full_cuts_5.65GHz.png 86.6 KB
full_cuts_5.80GHz.png 87.0 KB
full_cuts_5.95GHz.png 88.6 KB
full_farfield_5.65GHz.npz 147 KB
full_farfield_5.80GHz.npz 147 KB
full_farfield_5.95GHz.npz 147 KB
full_pattern_5.65GHz.png 79.2 KB
full_pattern_5.80GHz.png 80.5 KB
full_pattern_5.95GHz.png 83.6 KB
full_pattern3d_5.65GHz.png 154 KB
full_pattern3d_5.80GHz.png 156 KB
full_pattern3d_5.95GHz.png 155 KB
full_sparams.png 30.3 KB
summary.json 1.5 KB
sim/network/
network_sparams.png 45.0 KB
sim/patch/
patch_ideal_feed_pattern_5.65GHz.png 69.2 KB
patch_ideal_feed_pattern_5.80GHz.png 69.5 KB
patch_ideal_feed_pattern_5.95GHz.png 70.5 KB
patch_sparams.png 48.0 KB
summary.json 1.4 KB
src/
generate.py 1.9 KB
qfp_geom.py 11.6 KB
qfp_sim.py 27.4 KB
src/qfp_layers.py
#!/usr/bin/env python3
"""Derive per-layer copper/outline geometry from qfp_geom primitives; write KiCad-importable SVGs + preview."""
import os, sys, math
import numpy as np
from shapely.geometry import Point, LineString, Polygon, box
from shapely.ops import unary_union, split
from shapely import affinity
from qfp_geom import P, build, arc_points, u


def prim_shape(p):
    pts = [tuple(p['p0']), tuple(p['p1'])] if p['kind'] == 'seg' else [tuple(q) for q in arc_points(p)]
    return LineString(pts).buffer(p['w']/2, cap_style=1, join_style=1, quad_segs=16)

def rect(c, along, across, ang):
    r = box(-along/2, -across/2, along/2, across/2)
    return affinity.translate(affinity.rotate(r, ang, use_radians=True, origin=(0, 0)), *c)

def disk(c, dia): return Point(*c).buffer(dia/2, quad_segs=64)

def outline_feed(f):
    circ = Point(0, 0).buffer(P['board_d']/2, quad_segs=180)
    uu, tt, e = f['sma']['u'], f['sma']['t'], f['sma']['edge']
    half = Polygon([tuple(e + 100*tt), tuple(e - 100*tt), tuple(e - 100*tt - 100*uu), tuple(e + 100*tt - 100*uu)])
    return circ.intersection(half)

def feed_layers(f):
    d = P['res_pitch']/2
    s = f['sma']
    ang_u = math.atan2(s['u'][1], s['u'][0])
    res_pads = [rect(c + k*d*u(a), P['res_pad'][0], P['res_pad'][1], a) for _, c, a, _ in f['res'] for k in (-1, 1)]
    sig = rect(s['sig_c'], P['sma_sig_len'], f['prims'][-1]['w'], ang_u)
    gnd = [rect(g, P['sma_gnd_len'], P['sma_gnd_w'], ang_u) for g in s['gnd_c']]
    pinpads = [disk(p, P['pin_pad']) for p in f['pins']]
    viapads = [disk(v, P['via_pad']) for v in f['vias']]
    net = unary_union([prim_shape(p) for p in f['prims']] + res_pads + [sig] + pinpads)
    outline = outline_feed(f)
    plane = outline.buffer(-P['edge_pullback'])
    for p in f['pins']: plane = plane.difference(disk(p, P['gnd_antipad']))
    for p in f['mh']: plane = plane.difference(disk(p, P['mh_clear']))
    B = unary_union([net] + gnd + viapads)
    F = unary_union([plane] + gnd + pinpads)
    drills = [(tuple(p), P['pin_drill'], True) for p in f['pins']] + [(v, P['via_drill'], True) for v in f['vias']] \
           + [(tuple(p), P['mh_drill'], False) for p in f['mh']]
    return dict(B_Cu=B, F_Cu=F, Edge_Cuts=outline, drills=drills, net=net)

def patch_layers():
    pins = [P['rp']*u(k*math.pi/2) for k in range(4)]
    patch = disk((0, 0), P['patch_d'])
    for p in pins: patch = patch.difference(disk(p, P['patch_antipad']))
    rings = [disk(p, P['patch_pin_ring']) for p in pins]
    B = unary_union([patch] + rings)
    F = unary_union([disk(p, P['disk_d']) for p in pins])
    outline = Point(0, 0).buffer(P['patch_d']/2 + P['patch_margin'], quad_segs=180)
    return dict(B_Cu=B, F_Cu=F, Edge_Cuts=outline, drills=[(tuple(p), P['pin_drill'], True) for p in pins])


# ----------------------------------------------------------------------------- SVG (hole-free pieces)
def polys_of(g):
    if g.is_empty: return []
    if isinstance(g, Polygon): return [g]
    return [p for p in getattr(g, 'geoms', []) if isinstance(p, Polygon)]

def fracture(g):
    out, todo = [], polys_of(g)
    while todo:
        p = todo.pop()
        if not p.interiors:
            out.append(p); continue
        cy = Polygon(p.interiors[0]).centroid.y + 1e-4
        x0, _, x1, _ = p.bounds
        todo += [q for q in split(p, LineString([(x0 - 1, cy), (x1 + 1, cy)])).geoms if q.area > 1e-6]
    return out

def svg(path, geom, frame, outline=False, color='#000000'):
    x0, y0, W, H = frame
    pth = lambda p: 'M' + ' L'.join(f'{x:.4f},{-y:.4f}' for x, y in p.exterior.coords) + ' Z'
    if outline:
        body = [f'<path d="{pth(p)}" fill="none" stroke="{color}" stroke-width="0.1"/>' for p in polys_of(geom)]
    else:
        body = [f'<path d="{pth(p)}" fill="{color}" stroke="none"/>' for p in fracture(geom)]
    open(path, 'w').write(f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}mm" height="{H}mm" '
                          f'viewBox="{x0} {y0} {W} {H}">\n' + '\n'.join(body) + '\n</svg>\n')


# ----------------------------------------------------------------------------- preview
def preview(path, hand='RHCP'):
    import matplotlib; matplotlib.use('Agg')
    import matplotlib.pyplot as plt
    from matplotlib.patches import Polygon as MP, Circle
    f, info = build(hand)
    FL, PL = feed_layers(f), patch_layers()
    def draw(ax, g, **kw):
        for p in fracture(g): ax.add_patch(MP(list(p.exterior.coords), closed=True, **kw))
    fig, axs = plt.subplots(1, 2, figsize=(15, 7.5), gridspec_kw=dict(width_ratios=[1.35, 1]))
    ax = axs[0]
    draw(ax, FL['F_Cu'], fc='#dcdcdc', ec='none')
    draw(ax, FL['B_Cu'], fc='#b87333', ec='none')
    for (x, y), dd, pl in FL['drills']: ax.add_patch(Circle((x, y), dd/2, fc='k' if pl else 'w', ec='k', lw=0.5))
    for p in polys_of(FL['Edge_Cuts']):
        xs, ys = zip(*p.exterior.coords); ax.plot(xs, ys, 'k', lw=0.8)
    for ref, c, a, v in f['res']:
        ax.annotate(f'{ref} {v}Ω', c, xytext=(c[0]*1.18, c[1]*1.18), fontsize=8, color='#a00000',
                    ha='center', arrowprops=dict(arrowstyle='-', color='#a00000', lw=0.6))
    ph = {0: '0°', 1: '−90°', 2: '−180°', 3: '−270°'}
    for k, p in enumerate(f['pins']):
        ax.text(*(p*0.42), ph[k], fontsize=7, ha='center', va='center', color='#0040a0')
    ax.text(*(f['sma']['edge']*0.93 + f['sma']['t']*5), 'J1 SMA', fontsize=8, ha='center')
    ax.set_aspect('equal'); ax.set_xlim(-31, 31); ax.set_ylim(-31, 31); ax.grid(alpha=.25)
    ax.set_title(f'Feed board {hand} — top view: B.Cu network (copper) over F.Cu ground (grey)', fontsize=10)
    ax = axs[1]
    draw(ax, PL['B_Cu'], fc='#b87333', ec='none', alpha=.55)
    draw(ax, PL['F_Cu'], fc='#2060c0', ec='none', alpha=.8)
    for (x, y), dd, _ in PL['drills']: ax.add_patch(Circle((x, y), dd/2, fc='k'))
    for p in polys_of(PL['Edge_Cuts']):
        xs, ys = zip(*p.exterior.coords); ax.plot(xs, ys, 'k', lw=0.8)
    ax.set_aspect('equal'); ax.set_xlim(-13, 13); ax.set_ylim(-13, 13); ax.grid(alpha=.25)
    ax.set_title('Patch board — top view: B.Cu patch (faces ground), F.Cu coupling disks (blue)', fontsize=10)
    plt.tight_layout(); plt.savefig(path, dpi=120); plt.close(fig)


def write_svgs(outdir):
    os.makedirs(outdir, exist_ok=True)
    for hand in ('RHCP', 'LHCP'):
        f, _ = build(hand)
        L = feed_layers(f)
        b = f'QFP58-feed-{hand}'
        fr = (-32, -32, 64, 64)
        svg(f'{outdir}/{b}-B_Cu.svg', L['B_Cu'], fr)
        svg(f'{outdir}/{b}-F_Cu.svg', L['F_Cu'], fr)
        svg(f'{outdir}/{b}-Edge_Cuts.svg', L['Edge_Cuts'], fr, outline=True)
    L = patch_layers()
    fr = (-14, -14, 28, 28)
    svg(f'{outdir}/QFP58-patch-B_Cu.svg', L['B_Cu'], fr)
    svg(f'{outdir}/QFP58-patch-F_Cu.svg', L['F_Cu'], fr)
    svg(f'{outdir}/QFP58-patch-Edge_Cuts.svg', L['Edge_Cuts'], fr, outline=True)


if __name__ == '__main__':
    hand = sys.argv[1] if len(sys.argv) > 1 else 'RHCP'
    preview(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'docs', f'preview_{hand}.png'), hand)