Horn Antenna with Coaxial Pin Feed

  • A pyramidal horn antenna fed by a coaxial probe (pin) inside the rectangular feed waveguide, terminated by a quarter-wave back-short instead of a waveguide port.

Introduction

This tutorial covers:

  • Setup of a pyramidal horn antenna with a closed rectangular feed waveguide

  • A coaxial pin feed modelled as a lumped port, exciting the TE10 waveguide mode

  • A quarter guided-wavelength back-short instead of a waveguide port or PML termination

  • Calculate the S-Parameter and input impedance

  • Calculate the far-field pattern, directivity and aperture efficiency via NF2FF

Python Script

Get the latest version from git.

Import Libraries

import os, tempfile
import numpy as np
import matplotlib.pyplot as plt

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

Simulation path

Sim_Path = os.path.join(tempfile.gettempdir(), 'Horn_Antenna_PinFeed')

post_proc_only = False

unit = 1e-3     # all lengths in mm

Waveguide and Horn Parameters

a_wg = 20.0     # feed waveguide width  [mm] – TE10 propagates above fc = C0/(2a)
b_wg = 10.0     # feed waveguide height [mm]
wg_t  = 2.0     # metal wall thickness  [mm]

feed_length = 50.0  # length of the closed feed waveguide behind the horn mouth [mm]

horn_length = 50.0  # horn length in propagation direction (z) [mm]
horn_angle  = 20.0  # horn opening half-angle in both x and y [degrees]

Frequency

f_start = 10e9
f_stop  = 20e9
f0      = 15e9      # centre frequency of interest [Hz]

Derived parameters

fc_wg    = C0 / (2 * a_wg * unit)                          # TE10 cutoff [Hz]
lambda_g = C0/f0/unit / np.sqrt(1 - (fc_wg/f0)**2)        # guided wavelength at f0 [mm]

back_short = lambda_g / 4       # quarter-wave back-short [mm]
pin_length = b_wg * 0.55        # probe length [mm]  ← tune for best impedance match
pin_r      = 0.5                # probe cross-section half-width [mm] (≈ SMA inner conductor)
port_h     = 1.0                # lumped-port gap height at pin base [mm]
feed_R     = 50.0               # feed resistance [Ohm]

z_back = -feed_length           # inner face of the back-short wall [mm]
z_pin  = z_back + back_short    # pin position along the waveguide axis [mm]

# Horn aperture half-dimensions (at z = horn_length)
horn_ax = a_wg/2 + np.sin(np.deg2rad(horn_angle)) * horn_length
horn_ay = b_wg/2 + np.sin(np.deg2rad(horn_angle)) * horn_length

# Aperture area for antenna efficiency calculation
A_aperture = (2*horn_ax*unit) * (2*horn_ay*unit)

print(f'TE10 cutoff:            {fc_wg/1e9:.2f} GHz')
print(f'Guided wavelength at f0:{lambda_g:.2f} mm')
print(f'Back-short distance:    {back_short:.2f} mm')
print(f'Pin z-position:         {z_pin:.2f} mm  (from back wall: {back_short:.2f} mm)')
print(f'Pin length:             {pin_length:.2f} mm  (tune for matching)')
print(f'Pin cross-section:      {2*pin_r:.1f} x {2*pin_r:.1f} mm')

FDTD setup

FDTD = openEMS(EndCriteria=1e-4)
FDTD.SetGaussExcite(0.5*(f_start+f_stop), 0.5*(f_stop-f_start))
FDTD.SetBoundaryCond(['PML_8']*6)

Geometry and mesh

CSX = ContinuousStructure()
FDTD.SetCSX(CSX)
mesh = CSX.GetGrid()
mesh.SetDeltaUnit(unit)

max_res   = C0/f_stop/unit/20       # ≈ lambda/20 at highest frequency ≈ 0.75 mm

# Simulation box dimensions: structure edge + lambda/4 air gap + 8-cell PML estimate
lam0      = C0/f0/unit            # free-space wavelength at f0 [mm]
bc_offset = 9                     # PML_8 + 1 cell, matches CreateNF2FFBox placement
margin    = lam0/2 + bc_offset*max_res   # lambda/2 air gap + PML overhead [mm]

x_max     = horn_ax + margin      # x boundary (same margin used for y)
y_max     = horn_ay + margin      # y boundary
z_sim_back  = lam0/4 + bc_offset*max_res  # lambda/4 free space + PML overhead behind back wall
z_sim_front = margin              # in front of horn aperture: lambda/2 + PML

# Fixed mesh lines at key geometry locations
mesh.AddLine('x', [-x_max, -horn_ax, -(a_wg/2 + wg_t), -a_wg/2, -pin_r, pin_r,
                   a_wg/2, a_wg/2 + wg_t, horn_ax, x_max])
mesh.AddLine('y', [-y_max, -horn_ay, -(b_wg/2 + wg_t), -b_wg/2,
                   -b_wg/2+port_h, -b_wg/2+pin_length,
                   b_wg/2, b_wg/2 + wg_t, horn_ay, y_max])
mesh.AddLine('z', [z_back - z_sim_back, z_back - lam0/4, z_back - wg_t, z_back,
                   z_pin-pin_r, z_pin+pin_r,
                   0, horn_length, horn_length + z_sim_front])

Create horn antenna geometry

horn = CSX.AddMetal('horn')

Feed waveguide walls (closed rectangular tube from z_back to z=0)

# left wall  (at x = -a_wg/2)
horn.AddBox(priority=10, start=[-a_wg/2 - wg_t, -b_wg/2, z_back], stop=[-a_wg/2, b_wg/2, 0])
# right wall (at x = +a_wg/2)
horn.AddBox(priority=10, start=[ a_wg/2, -b_wg/2, z_back], stop=[ a_wg/2 + wg_t, b_wg/2, 0])
# top wall   (at y = +b_wg/2)
horn.AddBox(priority=10, start=[-a_wg/2 - wg_t,  b_wg/2,        z_back], stop=[ a_wg/2 + wg_t,  b_wg/2 + wg_t, 0])
# bottom wall (at y = -b_wg/2)
horn.AddBox(priority=10, start=[-a_wg/2 - wg_t, -b_wg/2 - wg_t, z_back], stop=[ a_wg/2 + wg_t, -b_wg/2,        0])

Back-short wall (metal lid closing the waveguide)

horn.AddBox(priority=10,
            start=[-a_wg/2 - wg_t, -b_wg/2 - wg_t, z_back - wg_t],
            stop= [ a_wg/2 + wg_t,  b_wg/2 + wg_t,  z_back       ])

Flared horn walls (four trapezoidal plates, one per side)

#
# LinPoly coordinate convention (from CSXCAD C++ cyclic index formula):
#   norm_dir='y': points[0] = z-coords, points[1] = x-coords
#   norm_dir='x': points[0] = y-coords, points[1] = z-coords
#
# Each plate starts in the plane of its normal direction at y/x = 0,
# is extruded by wg_t (centred on the elevation), then transformed with
# a rotation (to create the flare angle) and a translation (to the waveguide edge).

# Shared z-coordinates for the top/bottom horn-wall polygon
z_tb = np.array([0, horn_length, horn_length, 0])
# Corresponding x-coordinates (trapezoid widening along z)
x_tb = np.array([ a_wg/2, a_wg/2 + np.sin(np.deg2rad(horn_angle))*horn_length,
                 -a_wg/2 - np.sin(np.deg2rad(horn_angle))*horn_length, -a_wg/2])

# Bottom horn wall: rotate +horn_angle around x → far end tilts to −y
p = horn.AddLinPoly(points=[z_tb, x_tb], norm_dir='y',
                    elevation=-wg_t/2, length=wg_t, priority=10)
p.AddTransform('RotateAxis', 'x',  horn_angle)
p.AddTransform('Translate', [0, -b_wg/2 - wg_t/2, 0])

# Top horn wall: rotate −horn_angle around x → far end tilts to +y
p = horn.AddLinPoly(points=[z_tb, x_tb], norm_dir='y',
                    elevation=-wg_t/2, length=wg_t, priority=10)
p.AddTransform('RotateAxis', 'x', -horn_angle)
p.AddTransform('Translate', [0,  b_wg/2 + wg_t/2, 0])

# Shared y-coordinates for the left/right horn-wall polygon
# (spans full waveguide height including wall thickness, flares outward in y)
y_lr = np.array([ b_wg/2 + wg_t, b_wg/2 + wg_t + np.sin(np.deg2rad(horn_angle))*horn_length,
                 -b_wg/2 - wg_t - np.sin(np.deg2rad(horn_angle))*horn_length, -b_wg/2 - wg_t])
z_lr = np.array([0, horn_length, horn_length, 0])

# Left horn wall: rotate −horn_angle around y → far end tilts to −x
p = horn.AddLinPoly(points=[y_lr, z_lr], norm_dir='x',
                    elevation=-wg_t/2, length=wg_t, priority=10)
p.AddTransform('RotateAxis', 'y', -horn_angle)
p.AddTransform('Translate', [-a_wg/2 - wg_t/2, 0, 0])

# Right horn wall: rotate +horn_angle around y → far end tilts to +x
p = horn.AddLinPoly(points=[y_lr, z_lr], norm_dir='x',
                    elevation=-wg_t/2, length=wg_t, priority=10)
p.AddTransform('RotateAxis', 'y',  horn_angle)
p.AddTransform('Translate', [ a_wg/2 + wg_t/2, 0, 0])

Coaxial pin feed

# The feed is split into two parts that together model a coaxial probe:
#
#   ┌──────┐  ← pin tip      (y = −b_wg/2 + pin_length)
#   │ PEC  │  ← metal pin body (CSX metal box, cross-section 2*pin_r × 2*pin_r)
#   │ pin  │
#   └──────┘  ← port top     (y = −b_wg/2 + port_h)
#   [lumped]  ← short port gap modelling the coaxial connector transition
#  ══════════ ← bottom wall   (y = −b_wg/2)

# Short lumped port (the coaxial connector gap, same cross-section as pin body)
start = [-pin_r, -b_wg/2,          z_pin - pin_r]
stop  = [ pin_r, -b_wg/2 + port_h, z_pin + pin_r]
port = FDTD.AddLumpedPort(1, feed_R, start, stop, 'y', 1.0, priority=5)

# Metal pin body (PEC post from port top to pin tip, realistic cross-section)
pin_metal = CSX.AddMetal('pin')
pin_metal.AddBox(priority=10,
                 start=[-pin_r, -b_wg/2 + port_h,  z_pin - pin_r],
                 stop= [ pin_r, -b_wg/2 + pin_length, z_pin + pin_r])

Smooth mesh and create NF2FF recording box

mesh.SmoothMeshLines('all', max_res, 1.4)
nf2ff = FDTD.CreateNF2FFBox()

Optional: write XML and launch AppCSXCAD for geometry inspection

if 1:
    CSX_file = os.path.join(Sim_Path, 'horn_pinfeed.xml')
    if not os.path.exists(Sim_Path):
        os.mkdir(Sim_Path)
    CSX.Write2XML(CSX_file)
    from CSXCAD import AppCSXCAD_BIN
    os.system(AppCSXCAD_BIN + ' "{}"'.format(CSX_file))

Run the simulation

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

Post-processing

freq = np.linspace(f_start, f_stop, 401)
port.CalcPort(Sim_Path, freq)

s11    = port.uf_ref / port.uf_inc
s11_dB = 20.0 * np.log10(np.abs(s11))
Zin    = port.uf_tot / port.if_tot

Reflection coefficient S11

fig, ax = plt.subplots(num='S11', tight_layout=True)
ax.plot(freq/1e9, s11_dB, 'k-', linewidth=2)
ax.axhline(-10, color='gray', linestyle='--', linewidth=1)
ax.grid()
ax.set_xmargin(0)
ax.set_ylim([-40, 5])
ax.set_xlabel('Frequency (GHz)')
ax.set_ylabel('S11 (dB)')
ax.set_title('Reflection Coefficient S11')

Input impedance

fig, ax = plt.subplots(num='Zin', tight_layout=True)
ax.plot(freq/1e9, np.real(Zin), 'k-',  linewidth=2, label=r'$\Re\{Z_{in}\}$')
ax.plot(freq/1e9, np.imag(Zin), 'r--', linewidth=2, label=r'$\Im\{Z_{in}\}$')
ax.grid()
ax.set_xmargin(0)
ax.set_xlabel('Frequency (GHz)')
ax.set_ylabel('Impedance (Ohm)')
ax.set_title('Input Impedance')
ax.legend()

Far-field radiation pattern at f0

theta = np.arange(-180.0, 180.0, 2.0)
phi   = [0.0, 90.0]
print(f'Calculating far field at f0 = {f0/1e9:.1f} GHz ...')
nf2ff_res = nf2ff.CalcNF2FF(Sim_Path, f0, theta, phi)

Dmax_dBi = 10.0 * np.log10(nf2ff_res.Dmax[0])
G_a = 4 * np.pi * A_aperture / (C0/f0)**2      # ideal aperture gain (uniform illumination)
e_a = nf2ff_res.Dmax[0] / G_a                  # aperture efficiency

print(f'Directivity:          Dmax = {Dmax_dBi:.1f} dBi')
print(f'Aperture efficiency:  e_a  = {e_a*100:.1f} %')

E_norm = (20.0*np.log10(nf2ff_res.E_norm[0] / np.max(nf2ff_res.E_norm[0]))
          + Dmax_dBi)

fig, ax = plt.subplots(num='Pattern', tight_layout=True)
ax.plot(theta, E_norm[:, 0], 'k-',  linewidth=2, label='E-plane  (phi=0°)')
ax.plot(theta, E_norm[:, 1], 'r--', linewidth=2, label='H-plane  (phi=90°)')
ax.grid()
ax.set_xmargin(0)
ax.set_xlabel('Theta (deg)')
ax.set_ylabel('Directivity (dBi)')
ax.set_title(f'Far-Field Pattern at {f0/1e9:.1f} GHz')
ax.legend()

plt.show()

Images

3D view of the horn antenna

3D view of the Horn Antenna with coaxial pin feed (AppCSXCAD)

S-Parameter

S-Parameter of the horn antenna

Farfield pattern

Farfield pattern on an E- and H-plane