This page lists what you can record, where it appears after the run, and the result objects of the S-parameter calculators. For worked examples see Probes and S-Parameters, Far-Field and RCS and Visualization and Analysis.
Observables
| Method | Records | Appears in |
|---|---|---|
add_probe(position, component="ez") | one field component at one point, every step | Result.time_series[:, i] |
add_vector_probe(position) | all six components at one point | six more columns of time_series |
add_dft_plane_probe(*, axis, coordinate, component="ez", freqs=None, n_freqs=50, name=None) | one component on a whole plane, at chosen frequencies | Result.dft_planes[name] |
add_flux_monitor(*, axis, coordinate, freqs=None, n_freqs=50, size=None, center=None, name=None, dft_window="rect", dft_window_alpha=0.25) | power through a plane (or a size rectangle of it) | Result.flux_monitors[name] |
add_ntff_box(corner_lo, corner_hi, freqs=None, n_freqs=50) | tangential fields on a closed box, for far fields | Result.ntff_data, Result.ntff_box |
run(snapshot=SnapshotSpec(...)) | field frames during the run | Result.snapshots, Result.snapshot_axes |
Probe columns follow registration order. Frequencies are in Hz. Positions are in metres and snap to the nearest grid location.
import numpy as npfrom rfx import Box, GaussianPulse, Simulation, SnapshotSpec, flux_spectrum
sim = Simulation(freq_max=6e9, domain=(0.04, 0.03, 0.03), dx=1e-3, boundary="cpml", cpml_layers=8)sim.add_material("slab", eps_r=3.0, sigma=0.01)sim.add(Box((0.022, 0.0, 0.0), (0.028, 0.03, 0.03)), material="slab")sim.add_source((0.012, 0.015, 0.015), "ez", waveform=GaussianPulse(f0=3e9, bandwidth=0.8), amplitude_kind="current")
freqs = np.linspace(1e9, 5e9, 9)sim.add_probe((0.016, 0.015, 0.015), "ez") # column 0sim.add_flux_monitor(axis="x", coordinate=0.034, freqs=freqs, name="trans")sim.add_dft_plane_probe(axis="z", coordinate=0.015, component="ez", freqs=freqs, name="mid")
result = sim.run( n_steps=500, snapshot=SnapshotSpec(interval=50, components=("ez",), slice_axis=2, slice_index=20),)
print(result.time_series.shape) # (n_steps, n_probes)power = np.asarray(flux_spectrum(result.flux_monitors["trans"]))print(power.shape) # one value per frequencyaxes = result.snapshot_axes["ez"]print(result.snapshots["ez"].shape, axes.dims, axes.times_s[:2])flux_spectrum(monitor)gives the net power through the plane at each frequency. For transmission, divide by the same monitor in a reference run without the structure;subtract_flux_monitors(sample, ref)removes the incident field at field level, soflux_spectrumof the result is the scattered power.- A snapshot’s
slice_indexcounts grid cells including the absorber pad. Read positions and times fromresult.snapshot_axes[component](coords,times_s,steps) rather than working them out. Frames areintervalsteps apart. - Far fields:
compute_far_field(result.ntff_data, result.ntff_box, result.grid, theta, phi)returns aFarFieldResult;directivity(ff)andradiation_pattern(ff)read it. RCS usescompute_rcs(...). Both are covered in Far-Field and RCS.
Result
run() returns a Result (a named tuple).
Point and vector probe samples use SI field units: ex, ey, ez are in
V/m; hx, hy, hz are in A/m. time_series keeps those raw component
values, without division by the source amplitude or a field maximum.
Field snapshots use the same units. A gallery plot labelled Ez / Eref
is a separately normalized visualization, not the stored probe value.
| Field | Contents |
|---|---|
time_series | probe records, shape (n_steps, n_columns) |
dt | time step in seconds |
settling_db | worst share, over the frequencies the run’s spectral results read, of the value that still rings past the record’s end, in dB, from poles fitted after the source ends. At or below −40 dB passes. nan or None when it could not be judged; read the status. |
settling_witness | status (pass, fail, undetermined, absent; only pass is a pass), reason, and the per-frequency shares behind settling_db |
s_params, freqs | lumped/wire port S-matrix [receiver, driven, freq] and its frequencies (Hz) |
wire_port_sparams | wire-port details |
ringdown | ring-down completion, when run(ringdown=...) was used |
dft_planes | {name: DFTPlaneProbe} |
flux_monitors | {name: FluxMonitor} |
ntff_data, ntff_box | far-field accumulation and its box |
waveguide_ports, waveguide_sparams, waveguide_port_flux | per-port waveguide data from run(); for the full matrix use compute_waveguide_s_matrix() |
snapshots, snapshot_axes | {component: frames} and their coordinates and times |
grid | the grid the run used |
freq_range | the analysis range used by find_resonances() |
state | final field state, for inspection or save_state |
Helpers on Result:
| Method | Returns |
|---|---|
find_resonances(freq_range=None, probe_idx=0, source_decay_time=None, bandpass=None, waveform=None) | list of HarminvMode(freq, decay, Q, amplitude, phase, error) from one probe |
s(m, n), s11(), s21(), s12(), s22() | complex S-parameter vector, ports numbered from 1 |
s_db(m, n) | `20 log10 |
freqs_hz | the S-parameter frequency vector |
assert_finite(*, raise_on_nonfinite=False) | True when the observables hold no NaN/Inf |
plot_time_series(), plot_s_params(), plot_smith() | quick matplotlib plots |
Resonances
find_resonances() runs Harminv on the part of a probe record after the
source has died away. It suits resonators better than S-parameters because it
does not depend on port calibration. Record a long ring-down, and give the
structure some loss, or Q has no finite value to converge to.
cav = Simulation(freq_max=12e9, domain=(0.030, 0.020, 0.010), dx=1e-3, boundary="pec")cav.add_material("lossy_air", eps_r=1.0, sigma=1e-3)cav.add(Box((0.0, 0.0, 0.0), (0.030, 0.020, 0.010)), material="lossy_air")cav.add_source((0.007, 0.006, 0.005), "ez", waveform=GaussianPulse(f0=9e9, bandwidth=0.8), amplitude_kind="current")cav.add_probe((0.021, 0.013, 0.005), "ez")
ring = cav.run(n_steps=3000)for mode in ring.find_resonances(freq_range=(6e9, 11e9)): print(f"{mode.freq / 1e9:.3f} GHz, Q = {mode.Q:.0f}")
c0 = 299_792_458.0print(f"analytic TM110: {c0 / 2 * np.hypot(1 / 0.030, 1 / 0.020) / 1e9:.3f} GHz")For a lower-level call on any signal, use harminv(signal, dt, f_min, f_max, ...).
ForwardResult
forward() returns a smaller named tuple: time_series, s_params and
freqs (lumped/wire S11 vectors when port_s11_freqs= is set),
lumped_port_sparams, wire_port_sparams, dft_planes, ntff_data,
ntff_box, grid, dt and settling_probe_info. Everything in it can carry
a gradient.
S-parameter results
All full matrices are indexed S[receiver_port, driven_port, freq_index].
| Calculator | Result | Main fields |
|---|---|---|
run(compute_s_params=True) | Result | s_params, freqs |
compute_msl_s_matrix() | MSLSMatrixResult | S, freqs, Z0, beta, port_names, reliable, settling_db, reference_impedances, sigma_max_excess, S_raw, passivity_correction |
compute_waveguide_s_matrix() | WaveguideSMatrixResult | s_params, freqs, port_names, port_directions, reference_planes, settling_db, s21_phase_residual_deg_rms |
compute_coaxial_line_reflection() | CoaxialLineReflectionResult | s11, freqs, status, annulus_cells, gamma, recurrence_residual, fit_residual, z0_numerical_ohm, termination |
compute_coaxial_two_port() | CoaxialTwoPortResult | s_params, freqs, port_names, reference_planes, status, settling_db, cond_a, gamma, recurrence_residual, fit_residual |
Microstrip. S is the raw extraction, the same function a gradient
differentiates. A raw S can exceed passivity. sigma_max_excess[k] gives how
far the largest singular value exceeds 1 at bin k, and a warning names the
bins. That is a measurement artifact, usually a record that ended too early or
a mesh that is too coarse. enforce_passivity=True projects S onto the passive
set instead; the unprojected matrix stays in S_raw and the correction in
passivity_correction. Don’t quote projected values where the correction is
large. reliable[p, k] marks low-signal bins: where any entry is False,
drop the whole slice S[:, :, k], because S is solved jointly across drives.
reference_impedances are the power-wave references; Z0 is the fitted line
impedance, reported separately.
Waveguide. S is a ratio of modal voltage waves, each referenced to its own
port’s discrete modal impedance. It equals a power-wave S only when all ports
share one cross-section. s21_phase_residual_deg_rms is the RMS difference
between the S21 phase and −βL for a single-mode two-port. On an empty or
matched guide it measures the port’s discretization error. With a device
between the planes, it includes the device’s own phase.
Coax. Check status == "passed" before using s11. This result does not
go through the shared passivity check.
Checking a result
Some checks run automatically and warn without changing the arrays:
run()and the uniformforward()checktime_seriesand lumped/wires_paramsfor NaN/Inf and for any|S| > 1.1. They do not test the total power in a column.- The microstrip and waveguide calculators check column power and each entry.
- Every calculator warns when its record has not rung down to −40 dB.
Nothing checks DFT planes, flux, NTFF data or the state. Use
np.isfinite(...) on them yourself. A missing warning is not a passivity or
convergence result.
For your own regression checks, validate_port_smatrix(obj, *, check_passivity=True, passivity_tol=1e-9, check_reciprocity=False, ...)
returns a PortValidationReport for any S-matrix result, and
assert_port_smatrix_valid(...) raises instead.
from rfx import validate_port_smatrix
fake = np.zeros((2, 2, 3), dtype=complex)fake[1, 0] = fake[0, 1] = 0.9report = validate_port_smatrix(s_params=fake, freqs=np.array([1e9, 2e9, 3e9]), check_reciprocity=True)print(report.summary())Saving results
write_touchstone(path, s_params, freqs, z0=50.0, ...) and
read_touchstone(path) handle .sNp files. save_snapshots,
save_field_vtk, save_far_field and save_state write other outputs; see
the generated reference.
Limits
- A spectrum from a record that stopped too early is wrong. Check
settling_dbbefore you trust flux, DFT planes, far fields, S-parameters or Q. - Probes, DFT planes and flux monitors have no reference impedance and cannot give S-parameters. Use a port.
find_resonances()on a lossless structure returns an unbounded Q. Add a little loss.save_snapshots(..., dt=result.dt)withoutaxes=is wrong at the default interval, because frames areintervalsteps apart. Passaxes=result.snapshot_axesinstead.
