Antenna symmetry: a half-wave dipole as a half model#

The microstrip tutorial cut a model in half with a magnetic symmetry plane through the substrate. Antennas offer the electric counterpart: a center-fed dipole is mirror-symmetric about the plane through its feed, and because the current flows through that plane, the mirror is an electric wall — the same image theory that turned the monopole tutorial’s ground plane into a virtual dipole, now used deliberately to halve the computation.

Three things meet in this model: the symmetry declaration, a lumped feed sitting exactly on the symmetry plane, and the far-field monitor reconstructing the full-sphere radiation pattern from the half that was solved.


The geometry: declared whole, meshed half#

Everything is declared in full-model coordinates — both dipole arms, the full air volume, the feed crossing z = 0. The single declaration "zmin": "SymmetryPEC" states that the plane at z = 0 is an electric mirror; the mesher then simply never meshes the lower half. Deleting the lower arm by hand would change nothing but the room for error.

The feed deserves a closer look. Its endpoints straddle the symmetry plane, its reference impedance is the full dipole’s 73 Ω. Internally the solver keeps the meshed half of the device — half the gap in series with half the impedance — and reports every quantity at full-model scale, exactly as the port impedances of the microstrip tutorial did. Declared watts stay full-model watts.

import matplotlib.pyplot as plt
import numpy as np

import magnelio as mio
from magnelio import geo, monitors, plots, ports

h_arm = 27.3e-3  # arm length incl. half the feed gap (trimmed; ~0.45 lambda total)
gap = 2.0e-3  # feed gap
a_wire = 0.5e-3  # wire radius
pad = 50.0e-3  # clearance antenna -> absorbing boundary

air = mio.Material.air()

model = mio.GeometryModel(
    boundary_conditions={
        "zmin": "SymmetryPEC",  # electric mirror through the feed
        "xmin": "CPML",
        "xmax": "CPML",
        "ymin": "CPML",
        "ymax": "CPML",
        "zmax": "CPML",
    }
)
model.add(
    geo.Brick(
        origin=(-pad, -pad, -(h_arm + pad)),
        size=(2 * pad, 2 * pad, 2 * (h_arm + pad)),
        material=air,
    )
)
model.add(
    geo.ThinWire(
        geo.Curve.polyline([(0.0, 0.0, gap / 2), (0.0, 0.0, h_arm)]),
        radius=a_wire,
        name="arm_up",
    )
)
model.add(
    geo.ThinWire(
        geo.Curve.polyline([(0.0, 0.0, -h_arm), (0.0, 0.0, -gap / 2)]),
        radius=a_wire,
        name="arm_down",
    )
)
model.add_port(
    ports.PortLumped(
        name="feed",
        start=(0.0, 0.0, -gap / 2),
        end=(0.0, 0.0, gap / 2),
        Z0=73.0,
    )
)

fig, ax = plots.plot_cross_section(model, "y", 0.0, title="full model (meshed: z > 0 only)")
full model (meshed: z > 0 only)

Mesh, monitors, run#

The mesh shows the declaration at work: the grid starts at z = 0. The far-field monitor needs no target frequency band of its own — one frequency is enough for a pattern — and no geometry: it places its recording box inside the free-space region and books the symmetry plane automatically.

f_min, f_max = 1.0e9, 4.0e9
f0 = 2.45e9

mesh = mio.Mesh.from_geometry(
    model,
    mio.MeshControl(min_nodes_per_wavelength=20),
    f_max=f_max,
)
print(f"grid: {mesh.Nx} x {mesh.Ny} x {mesh.Nz} cells, z from {mesh.grid.z[0] * 1e3:.0f} mm")

farfield = monitors.MonitorFarField(freqs=[f0], name="farfield")

analysis = mio.AnalysisScatteringTD(
    mesh=mesh,
    f_min=f_min,
    f_max=f_max,
    monitors=(farfield,),
    verbose=False,
)
f_axis = np.linspace(f_min, f_max, 301)
result = analysis.run(f_axis=f_axis, excited=["feed"])
grid: 46 x 46 x 55 cells, z from 0 mm

Full-model electrical results from half a model#

The input impedance derived from S11 lands at the dipole’s textbook values — resonance where the total length is a bit under half a wavelength, feed resistance near 73 Ω. A missing factor of two here would be the monopole reading; the symmetry accounting is what keeps it away.

s11 = result.S("feed", "feed")
zin = 73.0 * (1 + s11) / (1 - s11)

im = zin.imag
i = int(np.nonzero((im[:-1] < 0) & (im[1:] >= 0))[0][0])
f_res = f_axis[i] - im[i] * (f_axis[i + 1] - f_axis[i]) / (im[i + 1] - im[i])
r_res = float(np.interp(f_res, f_axis, zin.real))
print(f"resonance (Im Zin = 0): {f_res / 1e9:.2f} GHz")
print(f"feed resistance there:  {r_res:.1f} Ohm (thin-dipole textbook: ~73)")

fig, ax = plt.subplots(figsize=(7, 4.2))
ax.plot(f_axis / 1e9, zin.real, label="R (real part)")
ax.plot(f_axis / 1e9, zin.imag, label="X (imaginary part)")
ax.axhline(0.0, color="gray", lw=0.8)
ax.axvline(f_res / 1e9, color="gray", lw=0.8, ls="--")
ax.set_xlabel("frequency [GHz]")
ax.set_ylabel(r"$Z_\mathrm{in}$ [$\Omega$]")
ax.set_ylim(-300, 400)
ax.legend()
ax.set_title("dipole input impedance from the half model")
fig.tight_layout()
dipole input impedance from the half model
resonance (Im Zin = 0): 2.45 GHz
feed resistance there:  75.4 Ohm (thin-dipole textbook: ~73)

The full-sphere pattern#

Unlike the monopole’s ground plane, a symmetry plane is bookkeeping, not physics: the mirror half of the world exists, so the pattern covers the whole sphere. The elevation cut shows the dipole donut — and the peak lands at the half-wave dipole’s 2.15 dBi.

pattern = farfield.result(f0)

fig, axes = plt.subplots(1, 2, figsize=(11.0, 4.6), subplot_kw={"projection": "polar"})
pattern.plot_cut(plane="phi", angle=0.0, ax=axes[0], title="elevation cut (E-plane)")
pattern.plot_cut(plane="theta", angle=np.pi / 2, ax=axes[1], title="azimuth cut (H-plane)")
fig.tight_layout()

print(f"peak directivity:   {10 * np.log10(pattern.directivity.max()):.2f} dBi")
print(f"peak realized gain: {10 * np.log10(pattern.realized_gain.max()):.2f} dBi")
print(f"radiated power:     {pattern.P_rad:.3f} W per incident W")
elevation cut (E-plane), azimuth cut (H-plane)
peak directivity:   2.15 dBi
peak realized gain: 2.07 dBi
radiated power:     0.982 W per incident W

The elevation cut carries the shape; the azimuth cut is the circle every wire antenna owes its users. The 3D surface puts both into one picture — the radius is the gain in dB above the floor, so the axial nulls show as pinch points.

fig, ax = pattern.plot_3d(title="dipole radiation surface (dB radius)")
dipole radiation surface (dB radius)

Where to go next#

New in this tutorial: an electric symmetry plane halving an antenna problem, a lumped feed declared across the symmetry plane in full-model coordinates, the full-sphere far field reconstructed from a half model, and pattern cuts plus the 3D radiation surface. The same three ingredients carry over unchanged to patch antennas and arrays — declare the full structure, state its symmetry, and read full-model answers.

Total running time of the script: (0 minutes 27.286 seconds)

Gallery generated by Sphinx-Gallery