Mode ports and S-parameters

Terminate waveguides with WavePort or ModePort, run a port sweep, and read, renormalize and export the S-matrix.

This guide shows how to measure the S-parameters of a waveguide device: put a modal port at each waveguide end, sweep the ports, and read result.network. For a voltage-gap feed on a PCB or an antenna, use lumped ports instead. The complete runnable example is the rectangular waveguide tutorial.

WavePort or ModePort#

PortWhat it doesUse it for
WavePortA network port: launches and measures named waveguide modes, with a reference plane and impedance definitions. Driven by PortExcitation or PortSweep.S-parameters of waveguide devices
ModePortA ModeSource plus a ModeMonitor on one aperture. Returns modal amplitudes through result.monitor(name).Mode launch and modal overlap, monitor-only taps

Warning

Modal ports and the underlying mode solver are experimental. Check a new port setup against a known case (a straight guide should give |S21| close to 1 and |S11| close to 0) before trusting device results.

Place a WavePort#

A WavePort lies on an axis-aligned plane. Exactly one size component is zero; that axis is the propagation axis.

port_in = mw.WavePort(
    "input",
    position=(-0.3, 0.0, 0.0),
    size=(0.0, 0.6, 0.3),              # covers the guide cross section in y and z
    direction="+",                      # the port launches toward +x
    reference_plane=-0.3,               # S-parameter phase reference along x, m
    modes=(mw.WaveModeSpec("te", polarization="Ez"),),
)
scene.add_port(port_in)
  • direction points into the device: "+" on the low end, "-" on the high end.
  • modes lists the modes to solve and track. WaveModeSpec(family, mode_index=0, polarization=...) selects a family (such as "te" or "tem") and the dominant polarization. Each mode becomes a network channel named "<port>::<mode>", for example "input::TE0".
  • The aperture must cover the whole guided field. For closed metal guides, size it to the inner cross section.

Waveguide walls must run through the PML to the edge of the padded grid, or the open wall ends reflect. With num_layers PML cells of size dl outside a domain that spans ±L along the guide, make the walls at least 2 * (L + num_layers * dl) long.

Run a port sweep#

PortSweep() drives each port in turn and assembles the S-matrix. For WavePort sweeps, Maxwell runs calibrated CW simulations per frequency, so leave source_time unset:

import numpy as np
 
freqs = tuple(float(f) for f in np.linspace(300e6, 550e6, 6))
result = mw.Simulation.fdtd(
    scene,
    frequencies=freqs,
    excitations=mw.PortSweep(),
    run_time=mw.TimeConfig.auto(steady_cycles=8, transient_cycles=16),
).run()

To drive one port with a chosen amplitude instead, pass excitations=mw.PortExcitation("input").

Read the S-matrix#

result.network is a NetworkData:

net = result.network
S = net.s                     # complex tensor, shape (num_frequencies, N, N)
print(net.port_names)         # ('left::TE0', 'right::TE0')
s21_db = 20 * np.log10(np.abs(S[:, 1, 0].cpu().numpy()))

Maxwell uses the exp(-i omega t) phasor convention, so a matched line of length L gives arg(S21) = +beta * L (modulo 2π).

NetworkData also provides:

MethodResult
to_z(), to_y()Impedance and admittance matrices
renormalize(z0)S-matrix for another reference impedance
shift_reference_planes(...)De-embed or move the reference planes
return_loss_db, vswr, reflection_coefficient (properties)Per-port reflection figures, shape (num_frequencies, N)
terminate(port, impedance=...), cascade(other, port_map=...)Load a port, or connect two networks
validate_physicality()Passivity and reciprocity report
to_touchstone(path), save(path)Write a Touchstone .sNp file or a snapshot

See the Networks API for arguments.

A WavePort references each mode to its own wave impedance, which changes with frequency. A Touchstone file holds one reference impedance per port, so call net.renormalize(50.0).to_touchstone("device.s2p"); net.to_touchstone(...) alone raises ValueError.

A ModePort without source_time is a monitor-only tap. result.monitor(name) then resolves to the forward and backward modal amplitudes on that plane:

tap = mw.ModePort("tap", position=(0.2, 0.0, 0.0), size=(0.0, 0.6, 0.3),
                  polarization="Ez", frequencies=freqs)
scene.add_port(tap)

witwin.maxwell.postprocess.compute_mode_overlap and compute_s_parameters turn mode data into overlaps and S-parameters (see the Postprocess API).