A coaxial line: solids, booleans, and a coax port#

The first tutorial needed no geometry at all — one air brick, and every conductor was a boundary face. This one builds a real three-dimensional structure: a short piece of RG-58 class coaxial cable, modelled from solid primitives and a boolean operation. Along the way it introduces the second way of declaring a port — by naming the analytical family of its cross-section — and the habit of reading a port report that carries both a numerical and a reference impedance.


The problem#

A coaxial line guides a TEM wave between an inner conductor of radius \(r_i\) and a shield of radius \(r_o\), filled with a dielectric of permittivity \(\varepsilon_r\). Its line impedance has a closed form,

\[Z_0 = \frac{\eta_0}{2 \pi \sqrt{\varepsilon_r}} \, \ln \frac{r_o}{r_i},\]

which makes it the classic first “real” validation case. We use the dimensions of an RG-58 class cable — solid polyethylene, \(r_i = 0.405\) mm, \(r_o = 1.475\) mm, \(\varepsilon_r = 2.25\) — giving \(Z_0 \approx 51.7\) Ohm. The first higher-order coax mode (TE11) appears near 34 GHz; simulating up to 6 GHz keeps us safely in the single-mode TEM band, so the S-parameters stay plain S11 and S21.

import math

import matplotlib.pyplot as plt
import numpy as np

import magnelio as mio
from magnelio import geo, plots, ports
from magnelio.constants import *

r_i = 0.405e-3  # inner conductor radius [m]
r_o = 1.475e-3  # shield (dielectric outer) radius [m]
eps_r = 2.25  # solid polyethylene
L = 8e-3  # line length [m]
f_max = 6e9  # upper band edge [Hz]

z_formula = ETA0 / (2 * math.pi * math.sqrt(eps_r)) * math.log(r_o / r_i)
print(f"target impedance: {z_formula:.3f} Ohm")
target impedance: 51.665 Ohm

Building the geometry#

Two solids and one boolean are enough:

  • the dielectric is a Cylinder of radius r_o with the inner conductor’s Cylinder carved out of it — Difference produces the annulus;

  • the inner conductor is the small cylinder itself, added as PEC.

The shield is not drawn at all. The model’s background material — what fills every cell no shape claims — is set to PEC, and the domain boundary closes as PEC by default. Everything outside the dielectric, including the box corners around the circle, is therefore solid conductor: the shield comes for free, and its inner surface is exactly the dielectric boundary at r_o.

pec = mio.Material.pec()
polyethylene = mio.Material.from_isotropic(name="polyethylene", epsilon=eps_r)

dielectric = geo.Cylinder(
    origin=(0.0, 0.0, 0.0), radius=r_o, height=L, axis="z", material=polyethylene
)
inner = geo.Cylinder(origin=(0.0, 0.0, 0.0), radius=r_i, height=L, axis="z", material=pec)

model = mio.GeometryModel(background=pec)
model.add(geo.Difference(dielectric, inner))  # the dielectric annulus
model.add(inner)  # the inner conductor
GeometryModel(2 shapes, background=PEC)

Looking at the model in three dimensions#

The cross-sections used throughout these pages are flat by necessity — they render into a static document. In a Jupyter notebook, model.plot() opens the model as a rotatable, zoomable 3D view instead, which is the faster way to check that a boolean did what you meant. Every tutorial can be downloaded as a notebook (the link at the bottom of the page), so this is one call away at any point:

model.plot()
The coaxial model as an interactive 3D view in a notebook

The coax port#

In the first tutorial, PortWaveguide simply said “solve whatever modes this cross-section has”. When the cross-section belongs to a family with a closed-form solution, a PortAnalytical says so explicitly. The port still solves its mode on the actual grid — but the report will carry the analytical value alongside, as a built-in cross-check.

model.add_port(
    ports.PortAnalytical(
        name="port1",
        plane="zmin",
        family="coax",
        inner_radius=r_i,
        outer_radius=r_o,
        epsilon_r=eps_r,
    )
)
model.add_port(
    ports.PortAnalytical(
        name="port2",
        plane="zmax",
        family="coax",
        inner_radius=r_i,
        outer_radius=r_o,
        epsilon_r=eps_r,
    )
)
GeometryModel(2 shapes, background=PEC)

Mesh#

At 6 GHz the wavelength is huge compared to the cable, so the mesh size is dictated entirely by the geometry: the grid has to resolve the annulus. A 0.12 mm cap gives about nine cells across the dielectric gap.

mesh = mio.Mesh.from_geometry(model, mio.MeshControl(max_cell_size=0.12e-3), f_max=f_max)
print(f"grid: {mesh.Nx} x {mesh.Ny} x {mesh.Nz} cells")

# Two cuts: across the cable, and along it.  The longitudinal cut
# shows that the grid is uniform along the line — nothing varies in
# z, so nothing there needs resolving.
fig, axes = plt.subplots(1, 2, figsize=(11, 4.2))
plots.plot_cross_section(
    model, "z", L / 2, mesh=mesh, ax=axes[0], title="Coax cross-section with mesh"
)
plots.plot_cross_section(
    model, "x", 0.0, mesh=mesh, ax=axes[1], flip=True, title="Longitudinal cut (x = 0)"
)
fig.tight_layout()
Coax cross-section with mesh, Longitudinal cut (x = 0)
grid: 25 x 25 x 67 cells

The circle is rendered on a rectangular grid as a staircase, with partially filled cells treated by conformal material matrices — keep this picture in mind when reading the port report below.

Numerical mode vs. analytical reference#

analysis = mio.AnalysisScatteringTD(mesh=mesh, f_max=f_max, verbose=False)

report = analysis.solve_ports()["port1"]
print(report)

z_num = report.z_line_num
z_ref = report.z_line_ref
print(f"z_line numerical : {z_num:7.2f} Ohm  ({100 * (z_num / z_ref - 1):+.1f} %)")
print(f"z_line reference : {z_ref:7.2f} Ohm")
Port 'port1' — 1 mode(s)
  z_line = 49.67 Ω (numerical), 51.67 Ω (reference, Δ -3.87 %)
  [0] TEM_lap00    TEM f_c = 0.0000 GHz  z_line = 49.67 Ω
z_line numerical :   49.67 Ohm  (-3.9 %)
z_line reference :   51.67 Ohm

The two impedances answer different questions. The reference is the closed-form value of the ideal circular line — the design target. The numerical value is the impedance of the staircased cross-section the grid actually represents; it differs by a few percent here and approaches the reference as the mesh is refined (not monotonically — the staircase reshapes at every resolution). The gap between the two is an honest measure of how well the mesh captures this cross-section.

The transverse mode profile shows the expected radial TEM field, strongest at the inner conductor. Passing the geometry model overlays the port-plane cross-section — the grey disc is the inner conductor, the tinted annulus the dielectric:

fig, ax = report.modes[0].plot(field="E", title="Coax TEM mode, transverse E", geometry=model)
Coax TEM mode, transverse E

Run and S-parameters#

result = analysis.run(excited=[("port1", 0)])

f = result.f_axis
s11_db = result.db("port1", "port1")
s21_db = result.db("port2", "port1")

fig, ax = plt.subplots()
ax.plot(f / 1e9, s11_db, label="|S11|")
ax.plot(f / 1e9, s21_db, label="|S21|")
ax.set_xlabel("frequency [GHz]")
ax.set_ylabel("magnitude [dB]")
ax.set_title("RG-58 class coax, 8 mm")
ax.grid(True)
ax.legend()

print(f"max |S11| in band: {s11_db.max():6.1f} dB")
RG-58 class coax, 8 mm
max |S11| in band: -110.5 dB

As for every uniform matched line, |S21| sits at 0 dB and |S11| at the numerical floor — around -110 dB here. Note what that floor means: the port’s discrete mode matches the discrete line so well that essentially nothing reflects, even though the staircased impedance differs from the ideal one by a few percent. Port matching is between port and grid; the reference impedance is between grid and reality.

The transmission phase must follow the dielectric-loaded electrical length \(\beta L\) with \(\beta = 2 \pi f \sqrt{\varepsilon_r} / c_0\):

s21 = result.S("port2", "port1")
phase_sim = np.unwrap(np.angle(s21))
phase_ref = -2 * np.pi * f * math.sqrt(eps_r) / C0 * L

fig, ax = plt.subplots()
ax.plot(f / 1e9, np.degrees(phase_sim), label="arg S21 (simulated)")
ax.plot(f / 1e9, np.degrees(phase_ref), "--", label=r"$-\beta L$ (analytic)")
ax.set_xlabel("frequency [GHz]")
ax.set_ylabel("phase [deg]")
ax.set_title("Transmission phase vs. analytic")
ax.grid(True)
ax.legend()

print(f"max phase deviation: {np.degrees(np.abs(phase_sim - phase_ref)).max():.3f} deg")
Transmission phase vs. analytic
max phase deviation: 0.099 deg

Where to go next#

New in this tutorial: solids and booleans (Cylinder, Difference), the PEC background as an implicit outer conductor, an analytical port family (PortAnalytical), and the numerical-vs-reference reading of a port report. The next tutorial excites both ports of this line and assembles the full S-matrix — including what to check when a device has more ports than modes of interest.

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

Gallery generated by Sphinx-Gallery