Patch Antenna Phased Array
Apply complex phase weights to a three-element patch array (helper function
Patch_Antenna_Array) to steer the beam and compute the steered radiation
pattern.
This tutorial covers:
S-parameter-based beamforming using superposition
Phase taper calculation for a target scan angle
Steered far-field pattern vs. broadside comparison
Octave/Matlab Script
Full S-Parameter Simulation Loop
Run three independent openEMS simulations, activating one element at a
time while terminating the other two with 50 Ohm loads. This one-at-a-
time strategy decouples the FDTD runs and yields the complete 3x3
S-parameter and NF2FF dataset without requiring simultaneous multi-port
excitation; port currents and far-field data at f0 are retained for
later superposition.
xpos = [0 -41 41]; % x-center position of the 3 antennas
caps = [0 0 0];
resist = [50 50 50];
spara = [];
color_code = {'k-','r--','m-.'};
for n=1:3
active = [0 0 0];
active(n) = 1; % activate antenna n
Sim_Path = [Sim_Path_Root '_' num2str(n)]; % create an individual path
[port{n} nf2ff{n}] = Patch_Antenna_Array(Sim_Path, ((exist(Sim_Path,'dir')>0) && (force_rerun==0)), show_structure, xpos, caps, resist, active);
port{n} = calcPort( port{n}, Sim_Path, f, 'RefImpedance', 50);
nf2ff{n} = CalcNF2FF(nf2ff{n}, Sim_Path, f0, [-180:2:180]*pi/180, 0);
figure
hold on
grid on
for p=1:3
I(p,n) = interp1(f, port{n}{p}.if.tot,f0);
P_in(p) = 0.5*interp1(f, port{n}{n}.uf.inc,f0)*conj(interp1(f, port{n}{n}.if.inc,f0));
spara(p,n,:) = port{n}{p}.uf.ref./ port{n}{n}.uf.inc;
plot(f, squeeze(20*log10(abs(spara(p,n,:)))),color_code{p},'Linewidth',2);
end
end
Export S-Parameters to Touchstone
Write the full 3x3 S-parameter set to a Touchstone (.s3p) file so it can be imported into circuit simulators such as Qucs. Loading capacitors C2 and C3 can then be attached in the circuit schematic to explore beam-steering without re-running any FDTD simulations. Pre-computed port current ratios from Qucs are included here for cross-validation.
write_touchstone('s',f,spara,[Sim_Path_Root '.s3p']);
% instructions for Qucs:
% load the written touchstone file
% attach C2 and C3 to port 2 and 3
% attach a signal port to port 1
% probe the currents going into port 1 to 3
% example currents for ports 1 to 3 for C2 = 0.2pF and C3=0.2pF
I_qucs(1,1) = 0.00398-0.000465j;
I_qucs(2,1) = 2.92e-5-0.000914j;
I_qucs(3,1) = 2.92e-5-0.000914j;
disp(['I2/I1: Qucs: ' num2str(I_qucs(2)/I_qucs(1)) ' (defined manually)'])
disp(['I3/I1: Qucs: ' num2str(I_qucs(3)/I_qucs(1)) ' (defined manually)'])
Port Current Calculation via Circuit Theory
Convert S-parameters to Z-parameters and analytically solve for port currents when capacitors C2 and C3 reactively load ports 2 and 3, exploiting mutual coupling to steer the beam [1]. This avoids a new FDTD run for every capacitor value and enables rapid design-space exploration; the reference input current at port 1 is set to 1 mA and currents at ports 2 and 3 follow directly from the Z-matrix equations.
z = s2z(spara);
Z2 = 1/(1j*2*pi*f0*C2);
Z3 = 1/(1j*2*pi*f0*C3);
z23(1,1) = interp1(f,squeeze(z(2,2,:)),f0) + Z2;
z23(1,2) = interp1(f,squeeze(z(2,3,:)),f0);
z23(2,1) = interp1(f,squeeze(z(3,2,:)),f0);
z23(2,2) = interp1(f,squeeze(z(3,3,:)),f0) + Z3;
%set input/feeding current of port 1 to 1mA
I_out(1,1) = 1e-3;
% calc current for port 2 and 3
I_out([2 3],1) = z23\[-interp1(f,squeeze(z(2,1,:)),f0);-interp1(f,squeeze(z(3,1,:)),f0)]*I_out(1);
disp(['I2/I1: Matlab: ' num2str(I_out(2)/I_out(1))])
disp(['I3/I1: Matlab: ' num2str(I_out(3)/I_out(1))])
Reference Simulation for Verification
Run a single openEMS simulation with C2 and C3 explicitly modeled as
lumped elements inside the FDTD domain to validate the circuit-theory
current prediction above. Good agreement between the FDTD port currents
and the Z-parameter result confirms that the superposition method is
accurate for this array; set do_reference_simulation = 0 to skip
this step if only the fast circuit approximation is needed.
if (do_reference_simulation)
active = [1 0 0];
caps = [0 C2 C3];
resist = [50 inf inf];
Sim_Path = [Sim_Path_Root '_C2=' num2str(C2*1e12) '_C3=' num2str(C3*1e12)];
[port_ref nf2ff_ref] = Patch_Antenna_Array(Sim_Path, ((exist(Sim_Path,'dir')>0) && (force_rerun==0)), show_structure, xpos, caps, resist, active);
port_ref = calcPort( port_ref, Sim_Path, f, 'RefImpedance', 50);
nf2ff_ref = CalcNF2FF(nf2ff_ref, Sim_Path, f0, [-180:2:180]*pi/180, 0);
% extract currents from reference simulation
for p=1:3
I_ref(p,1) = interp1(f, port_ref{p}.if.tot,f0);
end
disp(['I2/I1: openEMS: ' num2str(I_ref(2)/I_ref(1))])
disp(['I3/I1: openEMS: ' num2str(I_ref(3)/I_ref(1))])
end
Superposition Weighting Coefficients
Compute the complex weights that map the per-element excitation currents
from the three individual simulations to the desired port current
distribution I_out, following the superposition method of [3].
Apply those weights to the individual far-field E-field vectors to
construct the beam-steered array pattern without any additional FDTD run.
% calculate
coeff = I\I_out;
% apply
E_ff_phi = 0*nf2ff{1}.E_phi{1};
E_ff_theta = 0*nf2ff{1}.E_phi{1};
for n=1:3
E_ff_phi = E_ff_phi + coeff(n)*nf2ff{n}.E_phi{1};
E_ff_theta = E_ff_theta + coeff(n)*nf2ff{n}.E_theta{1};
end
Far-Field Pattern Visualization
Plot the normalized far-field pattern reconstructed from the superposition and, if the reference simulation was run, overlay it for direct comparison. Close agreement between the two curves validates the entire workflow from S-parameter extraction through circuit-level beam steering to far-field synthesis.
figure
polar([-180:2:180]'*pi/180,abs(E_ff_phi(:))/max(abs(E_ff_phi(:))));
hold on
if (do_reference_simulation)
polar([-180:2:180]'*pi/180,abs(nf2ff_ref.E_norm{1}(:,1))/max(abs(nf2ff_ref.E_norm{1}(:,1))),'r--');
end
title('normalized far-field pattern','Interpreter', 'none')
legend('calculated','reference')
Array Element Helper Function
The main script delegates the per-element simulation to
Patch_Antenna_Array, which sets up the single-element geometry and mesh,
activates the requested port, and returns the S-parameters and NF2FF result.
Running it once per port gives the full 3×3 S-matrix used for beamforming.
Simulation Parameters
This function builds and optionally runs a microstrip patch antenna array following the reactive-loading beam-steering design of Yusuf & Gong [1]. Each element shares the same patch geometry; the caller supplies xpos to set element spacing and caps/resist/active to configure each port as excited, terminated, or reactively loaded with a capacitor.
physical_constants;
unit = 1e-3; % all length in mm
% patch geometry setup
patch.W = 35; % width
patch.L = 28.3; % length
patch.Ws = 3.8; % width of feeding stub
patch.Gs = 1; % width of feeding gab
patch.l = 6; % length of feeding stub
patch.y0 = 10; % depth of feeding stub into into patch
% patch resonance frequency
f0 = 3e9;
%substrate setup
substrate.name = 'Ro3003';
substrate.epsR = 3;
substrate.kappa = 0.0013 * 2*pi*f0 * EPS0*substrate.epsR;
substrate.thickness = 1.524;
substrate.cells = 4;
substrate.width = patch.W + max(xpos) - min(xpos) + 4*patch.l;
substrate.length = 3*patch.l + patch.L;
% size of the simulation box
AirSpacer = [50 50 30];
edge_res = [-1/3 2/3]*1;
FDTD Parameters and Excitation
A Gaussian pulse centred at f0 = 3 GHz with a 2 GHz corner frequency provides wideband S-parameter coverage in a single simulation run. Open (PML) boundaries on all six faces (BC = 3) absorb outgoing radiation without reflections, which is essential for accurate antenna patterns.
fc = 2e9; % 20 dB corner frequency
FDTD = InitFDTD( 'EndCriteria', 1e-4 );
FDTD = SetGaussExcite( FDTD, f0, fc );
BC = [1 1 1 1 1 1]*3;
FDTD = SetBoundaryCond( FDTD, BC );
CSXCAD Geometry and Mesh Initialization
Initialize the CSXCAD container and empty mesh-line vectors. Mesh lines are accumulated incrementally as each geometry object is added; a later smoothing step converts them into a graded, physically accurate grid.
CSX = InitCSX();
mesh.x = [];
mesh.y = [];
mesh.z = [];
Patch Elements and Feed Ports
Build each radiating patch as two PEC wings separated by a coplanar gap, with a microstrip feed stub inset by y0 into the patch for impedance matching. Ports are placed at the stub base and configured as excited, resistively terminated, or capacitively loaded depending on the arguments.
CSX = AddMetal( CSX, 'patch' ); % create a perfect electric conductor (PEC)
for port_nr=1:numel(xpos)
start = [xpos(port_nr)-patch.W/2 patch.l substrate.thickness];
stop = [xpos(port_nr)-patch.Ws/2-patch.Gs patch.l+patch.L substrate.thickness];
CSX = AddBox(CSX,'patch',10, start, stop);
mesh.x = [mesh.x xpos(port_nr)-patch.W/2-edge_res];
start = [xpos(port_nr)+patch.W/2 patch.l substrate.thickness];
stop = [xpos(port_nr)+patch.Ws/2+patch.Gs patch.l+patch.L substrate.thickness];
CSX = AddBox(CSX,'patch',10, start, stop);
mesh.x = [mesh.x xpos(port_nr)+patch.W/2+edge_res];
mesh.y = [mesh.y patch.l-edge_res patch.l+patch.L+edge_res];
start = [xpos(port_nr)-patch.Ws/2-patch.Gs patch.l+patch.y0 substrate.thickness];
stop = [xpos(port_nr)+patch.Ws/2+patch.Gs patch.l+patch.L substrate.thickness];
CSX = AddBox(CSX,'patch',10, start, stop);
% feed line
start = [xpos(port_nr)-patch.Ws/2 patch.l+patch.y0 substrate.thickness];
stop = [xpos(port_nr)+patch.Ws/2 0 substrate.thickness];
CSX = AddBox(CSX,'patch',10, start, stop);
mesh.x = [mesh.x xpos(port_nr)+linspace(-patch.Ws/2-patch.Gs,-patch.Ws/2,3) xpos(port_nr)+linspace(patch.Ws/2,patch.Ws/2+patch.Gs,3)];
start = [xpos(port_nr)-patch.Ws/2 0 0];
stop = [xpos(port_nr)+patch.Ws/2 0 substrate.thickness];
if (caps(port_nr)>0)
CSX = AddLumpedElement(CSX, ['C_' num2str(port_nr)], 2, 'C', caps(port_nr));
CSX = AddBox(CSX,['C_' num2str(port_nr)],10, start, stop);
[CSX port{port_nr}] = AddLumpedPort(CSX, 5 ,port_nr ,inf, start, stop, [0 0 1], 0);
else
% feed port
[CSX port{port_nr}] = AddLumpedPort(CSX, 5 ,port_nr, resist(port_nr), start, stop, [0 0 1], active(port_nr));
end
end
Dielectric Substrate
Model the Ro3003 laminate as a lossy dielectric slab spanning the full array footprint. The conductivity is derived from the material loss tangent at f0, so substrate losses are accurately captured in S-parameter results.
CSX = AddMaterial( CSX, substrate.name );
CSX = SetMaterialProperty( CSX, substrate.name, 'Epsilon', substrate.epsR, 'Kappa', substrate.kappa );
start = [-substrate.width/2 0 0];
stop = [ substrate.width/2 substrate.length substrate.thickness];
CSX = AddBox( CSX, substrate.name, 0, start, stop );
mesh.x = [mesh.x start(1) stop(1)];
mesh.y = [mesh.y start(2) stop(2)];
% add extra cells to discretize the substrate thickness
mesh.z = [linspace(0,substrate.thickness,substrate.cells+1) mesh.z];
Ground Plane
Add a PEC ground plane coincident with the bottom face of the substrate (z = 0). Together with the patch metallization it forms the resonant microstrip cavity that governs radiation efficiency and bandwidth.
CSX = AddMetal( CSX, 'gnd' ); % create a perfect electric conductor (PEC)
start(3)=0;
stop(3) =0;
CSX = AddBox(CSX,'gnd',10,start,stop);
Mesh Finalization
A first smoothing pass coalesces the accumulated key lines to a 2 mm maximum step; air-spacer lines are then appended around the structure. A second pass enforces a lambda/20 cell-size limit at the highest frequency (f0 + fc) to maintain numerical accuracy across the bandwidth.
% generate a smooth mesh with max. cell size: lambda_min / 20
mesh = SmoothMesh(mesh, 2, 1.3);
mesh.x = [mesh.x min(mesh.x)-AirSpacer(1) max(mesh.x)+AirSpacer(1)];
mesh.y = [mesh.y min(mesh.y)-AirSpacer(2) max(mesh.y)+AirSpacer(2)];
mesh.z = [mesh.z min(mesh.z)-AirSpacer(3) max(mesh.z)+2*AirSpacer(3)];
mesh = SmoothMesh(mesh, c0 / (f0+fc) / unit / 20, 1.3);
Near-Field to Far-Field Box
Place the NF2FF integration surface 3 cells inside each absorbing boundary where the fields are well-converged and evanescent components have decayed. PML layers are added afterwards and the completed mesh is committed to CSXCAD with DefineRectGrid.
start = [mesh.x(4) mesh.y(4) mesh.z(4)];
stop = [mesh.x(end-3) mesh.y(end-3) mesh.z(end-3)];
[CSX nf2ff] = CreateNF2FFBox(CSX, 'nf2ff', start, stop);
mesh = AddPML(mesh,(BC==3)*8);
CSX = DefineRectGrid(CSX, unit, mesh);
Prepare Simulation Folder
Remove any previous results in Sim_Path so the solver starts from a clean state. The CSX filename is fixed to patch_array.xml; the calling script (Patch_Antenna_Phased_Array.m) supplies Sim_Path so that multiple array configurations can run in separate directories in parallel.
Sim_CSX = 'patch_array.xml';
if (postproc_only==0)
CleanupSimPath(Sim_Path);
Write Model to XML
Serialize the FDTD setup and CSXCAD geometry to an XML file that the openEMS engine reads at start-up. This file fully describes the simulation so it can be re-run or inspected without the Octave script.
WriteOpenEMS( [Sim_Path '/' Sim_CSX], FDTD, CSX );
Visualize Structure
Open AppCSXCAD to display the geometry before running the solver. This is optional (controlled by show_structure) but useful for verifying mesh density, port placement, and substrate extent before a long run.
if (show_structure>0)
CSXGeomPlot( [Sim_Path '/' Sim_CSX] );
end
Run openEMS Solver
Launch the FDTD engine in Sim_Path. The engine reads patch_array.xml, steps through time until the end criterion is met, and writes port voltage/current data and the NF2FF field snapshots to the same folder.
RunOpenEMS( Sim_Path, Sim_CSX);
end
Images
Three-element patch array geometry (AppCSXCAD)
Far-field pattern — C2 = 0.2 pF, C3 = 0.2 pF
Far-field pattern — C2 = 0.2 pF, C3 = 1.0 pF
Far-field pattern — C2 = 1.0 pF, C3 = 0.2 pF
Literature
Y. Yusuf and X. Gong, “A low-cost patch antenna phased array with analog beam steering using mutual coupling and reactive loading,” IEEE Antennas Wireless Propag. Lett., vol. 7, pp. 81–84, 2008.
S. Otto, S. Held, A. Rennings, and K. Solbach, “Array and multiport antenna farfield simulation using EMPIRE, MATLAB and ADS,” 39th European Microwave Conf. (EuMC 2009), Rome, Italy, pp. 1547–1550, Sept.–Oct. 2009.
K. Karlsson, J. Carlsson, I. Belov, G. Nilsson, and P.-S. Kildal, “Optimization of antenna diversity gain by combining full-wave and circuit simulations,” 2nd European Conf. on Antennas and Propagation (EuCAP 2007), Nov. 2007, pp. 1–5.