Skip to content

Probes and S-Parameters

rfx provides three probe types for extracting field data, plus several S-parameter extraction methods for different port models.


Records a single field component at one Yee cell over time. The time series is stored in result.time_series.

sim.add_probe(
position=(0.025, 0.025, 0.010), # (x, y, z) in metres
component="ez", # ex / ey / ez / hx / hy / hz
)

After running:

result = sim.run(n_steps=4000)
ts = result.time_series # shape (n_steps, n_probes)
ez_vs_time = ts[:, 0] # first probe, all timesteps

Multiple probes are indexed in the order they were registered.


Records all six field components (Ex, Ey, Ez, Hx, Hy, Hz) at the same position.

sim.add_vector_probe((0.025, 0.025, 0.010))

Result columns 0–5 correspond to Ex, Ey, Ez, Hx, Hy, Hz in that order.


Accumulates the frequency-domain field on a 2-D cross-section during the time loop. More memory-efficient than saving full snapshots and avoids FFT leakage of short time windows.

import jax.numpy as jnp
sim.add_dft_plane_probe(
axis="z",
coordinate=0.010, # z-plane at 10 mm
component="ez",
freqs=jnp.linspace(1e9, 4e9, 50),
name="ez_cross_section",
)

Access the result:

result = sim.run(n_steps=4000)
plane = result.dft_planes["ez_cross_section"]
# plane.data : shape (n_freqs, nx, ny) complex

!!! tip Omit freqs and set n_freqs to let rfx choose a frequency array automatically from freq_max / 10 to freq_max.


When one or more lumped ports are added with add_port(), pass compute_s_params=True:

sim.add_port((0.01, 0.02, 0.01), "ez", impedance=50)
sim.add_port((0.04, 0.02, 0.01), "ez", impedance=50)
result = sim.run(n_steps=3000, compute_s_params=True)
s_matrix = result.s_params # (2, 2, n_freqs) complex
freqs = result.freqs # (n_freqs,) Hz
import numpy as np
s11_db = 20 * np.log10(np.abs(s_matrix[0, 0, :]))
s21_db = 20 * np.log10(np.abs(s_matrix[1, 0, :]))

The S-matrix is normalised to each port’s impedance.

Wire ports use voltage/current DFT integrals accumulated during the time loop. The extraction is performed inside a JIT-compiled scan, making it faster than post-processing FFTs for long simulations.

sim.add_port(
(0.01, 0.02, 0.0),
"ez",
impedance=50,
extent=0.0016, # wire length
)
result = sim.run(n_steps=5000, compute_s_params=True)
s11 = result.s_params[0, 0, :] # complex, shape (n_freqs,)

See Sources & Ports for the waveguide-port setup workflow. After running:

sp = result.waveguide_sparams["port1"]
print(sp.freqs.shape) # (n_freqs,)
print(sp.s11.shape) # (n_freqs,) complex
print(sp.calibration_preset)
print(f"Reference plane: {sp.reference_plane*1e3:.1f} mm")
print(f"Probe plane: {sp.probe_plane*1e3:.1f} mm")

WaveguideSParamResult fields:

FieldTypeDescription
freqsndarrayFrequency array (Hz)
s11complex ndarrayReflection coefficient at port
s21complex ndarrayTransmission to measurement plane
calibration_presetstr"measured" or "source_to_probe"
source_planefloatActual source injection plane (m)
reference_planefloatReported S11 reference plane (m)
probe_planefloatReported S21 measurement plane (m)

result.find_resonances() applies the Harminv MUSIC-like algorithm to extract complex resonant frequencies from the probe ring-down signal. It returns a list of HarminvMode objects sorted by amplitude.

modes = result.find_resonances(
freq_range=(1e9, 5e9), # search band (Hz)
probe_idx=0, # which probe to analyse (default 0)
)
for m in modes:
print(f"f={m.freq/1e9:.4f} GHz Q={m.Q:.0f} amp={m.amplitude:.3e}")

HarminvMode fields:

FieldDescription
freqResonant frequency (Hz)
decayExponential decay rate (1/s)
QQuality factor = pi*f/decay
amplitudeComplex mode amplitude
phasePhase (radians)
modes = result.find_resonances(
freq_range=(1e9, 4e9),
probe_idx=0,
source_decay_time=None, # auto-computed: 2*(3*tau), where tau=1/(f_c*bw*pi)
bandpass=None, # auto: True for CPML (removes DC artifacts), False for PEC
)

!!! note Harminv requires the signal to have decayed to ring-down before the analysis window starts. source_decay_time is subtracted from the beginning of the time series. The default auto-computation is appropriate for GaussianPulse excitation.

You can call Harminv directly on any time series:

from rfx import harminv
import numpy as np
dt = 1e-12 # timestep in seconds
signal = np.loadtxt("probe_data.txt")
modes = harminv(signal, dt, f_min=1e9, f_max=5e9)

from rfx import plot_s_params, plot_time_series, plot_field_slice
plot_s_params(result) # S11/S21 magnitude and phase
plot_time_series(result) # all probe time series on one axes
plot_field_slice(result, "ez", axis="z", index=30) # 2-D Ez cross-section

from rfx import write_touchstone, read_touchstone
# Write 2-port S-params to a .s2p file
write_touchstone("patch_antenna.s2p", result.freqs, result.s_params)
# Read back
freqs, s_matrix = read_touchstone("patch_antenna.s2p")