Design a 1.6GHz CP patch antenna for GNSS

How to use openEMS. Discussion on examples, tutorials etc

Moderator: thorsten

Post Reply
Posts: 1
Joined: Sun 11 Sep 2022, 11:11

Design a 1.6GHz CP patch antenna for GNSS

Post by pepijndevos-alt » Tue 13 Sep 2022, 16:19

I am trying to design a patch antenna for receiving GNSS signals, but am facing several challenges.

I am trying to replicate this work to see if I get sane results before I start my own design:
I'm not sure what the policy around here is wrt paywalls and sci-hub and posting figures from copyrighted material.

Anyway, the paper gives the dimensions for a circular patch antenna with two circular slots to obtain circular polarization at two bands.
They then give radiation patterns for RHCP and LHCP, showing that LHCP is much stronger.

What I have done is I took the example at ... and changed the geometry to match that of the paper. I then tried to extend the plotting to plot CP as well.

Code: Select all

# -*- coding: utf-8 -*-
Created on Fri Dec 18 20:56:53 2015

@author: thorsten

### Import Libraries
import os, tempfile, math
from pylab import *

from CSXCAD  import ContinuousStructure
from openEMS import openEMS
from openEMS.physical_constants import *

### General parameter setup
Sim_Path = os.path.join(tempfile.gettempdir(), 'Simp_Patch')

post_proc_only = False

# patch width (resonant length) in x-direction
patch_radius  = 15.8 #
r1 = 4.7
d1 = 7.9
r2 = 3.3
d2 = 6.1

#substrate setup
substrate_epsR   = 4.4
substrate_kappa  = 1e-3 * 2*pi*2.45e9 * EPS0*substrate_epsR
substrate_width  = 60
substrate_length = 60
substrate_thickness = 1.524
substrate_cells = 4

#setup feeding
feed_pos = -8 #feeding position in x-direction
feed_R = 50     #feed resistance

# size of the simulation box
SimBox = np.array([200, 200, 150])

# setup FDTD parameter & excitation function
f0 = 2e9 # center frequency
fc = 1e9 # 20 dB corner frequency

### FDTD setup
## * Limit the simulation to 30k timesteps
## * Define a reduced end criteria of -40dB
FDTD = openEMS(NrTS=30000, EndCriteria=1e-4)
FDTD.SetGaussExcite( f0, fc )
FDTD.SetBoundaryCond( ['MUR', 'MUR', 'MUR', 'MUR', 'MUR', 'MUR'] )

CSX = ContinuousStructure()
mesh = CSX.GetGrid()
mesh_res = C0/(f0+fc)/1e-3/20

### Generate properties, primitives and mesh-grid
#initialize the mesh with the "air-box" dimensions
mesh.AddLine('x', [-SimBox[0]/2, SimBox[0]/2])
mesh.AddLine('y', [-SimBox[1]/2, SimBox[1]/2]          )
mesh.AddLine('z', [-SimBox[2]/3, SimBox[2]*2/3]        )

air = CSX.AddMaterial('Air', epsilon=1, mue=1)
# create patch
patch = CSX.AddMetal( 'patch' ) # create a perfect electric conductor (PEC)
#start = [-patch_width/2, -patch_length/2, substrate_thickness]
#stop  = [ patch_width/2 , patch_length/2, substrate_thickness]
start = [0, 0, substrate_thickness]
stop  = [0, 0, substrate_thickness+0.0348]
#patch.AddBox(priority=10, start=start, stop=stop) # add a box-primitive to the metal property 'patch'
patch.AddCylinder(priority=10, start=start, stop=stop, radius=patch_radius)

# add slots
phi = math.radians(315)
start = [d1*math.cos(phi), d1*math.sin(phi), substrate_thickness-0.001]
stop  = [d1*math.cos(phi), d1*math.sin(phi), substrate_thickness+0.0358]
air.AddCylinder(priority=11, start=start, stop=stop, radius=r1)

phi = math.radians(135)
start = [d2*math.cos(phi), d2*math.sin(phi), substrate_thickness-0.001]
stop  = [d2*math.cos(phi), d2*math.sin(phi), substrate_thickness+0.0358]
air.AddCylinder(priority=11, start=start, stop=stop, radius=r2)

FDTD.AddEdges2Grid(dirs='xy', properties=patch, metal_edge_res=mesh_res/2)

# create substrate
substrate = CSX.AddMaterial( 'substrate', epsilon=substrate_epsR, kappa=substrate_kappa)
start = [-substrate_width/2, -substrate_length/2, 0]
stop  = [ substrate_width/2,  substrate_length/2, substrate_thickness]
substrate.AddBox( priority=0, start=start, stop=stop )

# add extra cells to discretize the substrate thickness
mesh.AddLine('z', linspace(0,substrate_thickness,substrate_cells+1))

# create ground (same size as substrate)
gnd = CSX.AddMetal( 'gnd' ) # create a perfect electric conductor (PEC)
stop[2] =0
gnd.AddBox(start, stop, priority=10)

FDTD.AddEdges2Grid(dirs='xy', properties=gnd)

# apply the excitation & resist as a current source
start = [feed_pos, 0, 0]
stop  = [feed_pos, 0, substrate_thickness]
port = FDTD.AddLumpedPort(1, feed_R, start, stop, 'z', 1.0, priority=5, edges2grid='xy')

mesh.SmoothMeshLines('all', mesh_res, 1.4)

# Add the nf2ff recording box
nf2ff = FDTD.CreateNF2FFBox()

### Run the simulation
if 1:  # debugging only
    CSX_file = os.path.join(Sim_Path, 'simp_patch.xml')
    if not os.path.exists(Sim_Path):
    os.system(r'AppCSXCAD "{}"'.format(CSX_file))

if not post_proc_only:
    FDTD.Run(Sim_Path, verbose=3, cleanup=True)

### Post-processing and plotting
f = np.linspace(max(1e9,f0-fc),f0+fc,401)
port.CalcPort(Sim_Path, f)
s11 = port.uf_ref/port.uf_inc
s11_dB = 20.0*np.log10(np.abs(s11))
plot(f/1e9, s11_dB, 'k-', linewidth=2, label='$S_{11}$')
ylabel('S-Parameter (dB)')
xlabel('Frequency (GHz)')

idx = np.where((s11_dB<-10) & (s11_dB==np.min(s11_dB)))[0]
if not len(idx)==1:
    print('No resonance frequency found for far-field calulation')
    f_res = f[idx[0]]
    theta = np.arange(-180.0, 180.0, 2.0)
    phi   = [0., 90.]
    nf2ff_res = nf2ff.CalcNF2FF(Sim_Path, f_res, theta, phi, center=[0,0,1e-3])
    # breakpoint()

    E_norm = 20.0*np.log10(nf2ff_res.E_norm[0]/np.max(nf2ff_res.E_norm[0])) + nf2ff_res.Dmax[0]
    E_cprh = 20.0*np.log10(nf2ff_res.E_cprh[0]/np.max(nf2ff_res.E_cprh[0])) + nf2ff_res.Dmax[0]
    E_cplh = 20.0*np.log10(nf2ff_res.E_cplh[0]/np.max(nf2ff_res.E_cplh[0])) + nf2ff_res.Dmax[0]
    plot(theta, np.squeeze(E_norm[:,0]), '-', linewidth=2, label='xz-plane norm')
    plot(theta, np.squeeze(E_norm[:,1]), '--', linewidth=2, label='yz-plane norm')
    E_cprh = 20.0*np.log10(nf2ff_res.E_cprh[0]/np.max(nf2ff_res.E_cprh[0])) + nf2ff_res.Dmax[0]
    plot(theta, np.abs(np.squeeze(E_cprh[:,0])), '-', linewidth=2, label='xz-plane cprh')
    plot(theta, np.abs(np.squeeze(E_cprh[:,1])), '--', linewidth=2, label='yz-plane cprh')
    E_cplh = 20.0*np.log10(nf2ff_res.E_cplh[0]/np.max(nf2ff_res.E_cplh[0])) + nf2ff_res.Dmax[0]
    plot(theta, np.abs(np.squeeze(E_cplh[:,0])), '-', linewidth=2, label='xz-plane cplh')
    plot(theta, np.abs(np.squeeze(E_cplh[:,1])), '--', linewidth=2, label='yz-plane cplh')
    ylabel('Directivity (dBi)')
    xlabel('Theta (deg)')
    title('Frequency: {} GHz'.format(f_res/1e9))

Zin = port.uf_tot/port.if_tot
plot(f/1e9, np.real(Zin), 'k-', linewidth=2, label='$\Re\{Z_{in}\}$')
plot(f/1e9, np.imag(Zin), 'r--', linewidth=2, label='$\Im\{Z_{in}\}$')
ylabel('Zin (Ohm)')
xlabel('Frequency (GHz)')

Running the above code you can verify that the geometry matches what the paper describes, but the radiation pattern looks completely different.

I don't really want to build this particular antenna, but I'd like to have some confidence in the setup.

One thing I'm concerned about is the z-fighting in the viewer between the "air" cylinders and the metal ones. If the air does not properly cut the ideal conductor it is of course going to behave as though it's just an infinitely thin metal sheet.

It's also somewhat weird that the coordinate system is different from the paper but worst case that would cause the polarization to flip.

And I'm just not 100% sure how to interpret the cprh and cplh vectors, maybe I'm just plotting them wrong.

[edit] I tried going back to basics a bit more and just adjusting the simple patch antenna example to 1.6GHz and according to my math that comes out to about 45mm but this gives completely weird results. It doesn't seem like I can upload pictures, but here is a tweet with pictures of my ongoing struggle: ... 1197547521 it seems like maybe a problem with meshing and the air box but it's unclear what.

Post Reply