Lumped ports and RF networks

Feed antennas and circuits with lumped or terminal ports, get S-parameters and input impedance, and add RLC loads, Touchstone networks and circuits.

This guide covers RF problems where the feed is a voltage across a small gap: antennas, PCB traces, connectors and packages. You place a LumpedPort (or a TerminalPort between two conductors), run a port sweep, and read port voltages, currents, impedances and the S-matrix. For waveguide modes, see Mode ports and S-parameters. Every class here is described in the Ports API.

Place a lumped port#

A LumpedPort needs a straight voltage path between two terminal points and a small surface around which Maxwell integrates the magnetic field to get the current.

import numpy as np
import witwin.maxwell as mw
 
dl = 0.005        # 5 mm cells
arm = 0.07        # each dipole arm, m
gap = 0.01        # feed gap, m
 
scene = mw.Scene(
    domain=mw.Domain(bounds=((-0.15, 0.15), (-0.15, 0.15), (-0.2, 0.2))),
    grid=mw.GridSpec.uniform(dl),
    boundary=mw.BoundarySpec.pml(num_layers=10),
    device="cuda",
)
for name, zc in (("arm_top", gap / 2 + arm / 2), ("arm_bottom", -gap / 2 - arm / 2)):
    scene.add_structure(mw.Structure(name=name, geometry=mw.Box(position=(0.0, 0.0, zc), size=(dl, dl, arm)),
                                     material=mw.Material.pec()))
scene.add_port(
    mw.LumpedPort(
        "feed",
        positive=(0.0, 0.0, gap / 2),
        negative=(0.0, 0.0, -gap / 2),
        voltage_path=mw.AxisPath("z"),
        current_surface=mw.Box(position=(0.0, 0.0, dl / 2), size=(3 * dl, 3 * dl, 0.0)),
        reference_impedance=50.0,
    )
)

Two placement rules are checked when the scene compiles:

  • Terminals on grid nodes. positive and negative must lie on Yee node coordinates and differ only along voltage_path.axis. With a 5 mm grid, z = ±0.0025 fails and z = ±0.005 works.
  • Current surface on a half-grid plane. The flat current_surface box (one zero size) must sit halfway between node planes along its normal, here z = dl / 2. Make it a little larger than the conductor cross section.

TerminalPort("feed", TerminalRef("signal"), TerminalRef("ground"), AxisPath("z"), ...) does the same between two named PEC structures, and resolves the terminal points from their geometry.

Sweep the port and read the results#

PortSweep() drives each port in turn with a Gaussian pulse that covers the simulation frequencies, terminates the other ports with their reference impedance, and assembles the S-matrix:

freqs = tuple(float(f) for f in np.linspace(0.6e9, 1.4e9, 9))
result = mw.Simulation.fdtd(scene, frequencies=freqs, excitations=mw.PortSweep()).run()
 
port = result.port("feed")                       # PortData
z_in = port.z_in.reshape(-1)                     # complex input impedance per frequency, ohm
s11 = result.network.s[:, 0, 0]                  # complex S11 per frequency
s11_db = 20 * np.log10(np.abs(s11.detach().cpu().numpy()))

This scene runs in about 20 seconds on a recent GPU. PortData also gives voltage, current, the power waves a and b, accepted_power, delivered_power, reflection_coefficient, return_loss_db and vswr. Frequencies must be strictly increasing, and ports in a sweep must not carry a termination.

Note

Maxwell uses the exp(-i omega t) phasor convention. The imaginary part of an impedance therefore has the opposite sign from the exp(+j omega t) convention used in most circuit tools: an inductive load shows a negative imaginary z_in. Conjugate before comparing with such tools.

Tip

A gap feed adds its own reactance, so the input impedance of a coarse strip antenna differs from thin-wire theory. Refine the grid, keep the gap at one or two cells, or model wire antennas with ThinWire and a WirePortBinding (see the Thin wires API).

Drive a port with a chosen signal#

To excite one port with your own amplitude, generator impedance or waveform, use PortExcitation:

result = mw.Simulation.fdtd(
    scene,
    frequencies=[1.0e9],
    excitations=mw.PortExcitation("feed", amplitude=1.0, source_time=mw.CW(frequency=1.0e9)),
).run()
print(result.port("feed").accepted_power)

Add lumped loads#

Terminate a port with a passive load, or place a standalone component in the field:

load_port = mw.LumpedPort(
    "load",
    positive=(0.05, 0.0, 0.005), negative=(0.05, 0.0, -0.005),
    voltage_path=mw.AxisPath("z"),
    current_surface=mw.Box(position=(0.05, 0.0, 0.0025), size=(0.015, 0.015, 0.0)),
    termination=mw.SeriesRLC(r=50.0, l=2e-9),
)

SeriesRLC and ParallelRLC take r (ohm), l (henry) and c (farad); missing or zero values are absent. Resistor(name, positive, negative, resistance), Capacitor and Inductor, added with scene.add_lumped_element(...), put a single component between two terminal points. R, L and C values may be tensors with requires_grad=True for optimization.

Embed measured networks and circuits#

  • Touchstone data. TouchstoneNetwork(name, connections={...}, path="filter.s2p"), added with scene.add_network(...), fits a rational model to the file and steps it together with the fields, connected to named scene ports. Read it back with result.embedded_network(name).
  • Linear circuits. A Circuit (built in Python or with parse_spice(netlist_text)) is co-simulated with the fields through modified nodal analysis and bound to scene ports with Circuit.bind_port(...). Read node voltages and branch currents with result.circuit(name).

See the Networks API and the Circuits API.

Post-process the network#

result.network is a NetworkData. Convert it with to_z() and to_y(), change the reference impedance with renormalize(z0), terminate or cascade ports, check passivity with validate_physicality(), and write a Touchstone file with to_touchstone("antenna.s1p"). result.antenna(...) computes gain, realized gain and efficiency from a port and a ClosedSurfaceMonitor (see the Antennas and arrays API).