REMI Lab
OPEN KNOWLEDGE / TOOLS

Results and Observables

이 페이지의 내용

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

MethodRecordsAppears in
add_probe(position, component="ez")one field component at one point, every stepResult.time_series[:, i]
add_vector_probe(position)all six components at one pointsix 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 frequenciesResult.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 fieldsResult.ntff_data, Result.ntff_box
run(snapshot=SnapshotSpec(...))field frames during the runResult.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 np
from 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 0
sim.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 frequency
axes = 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, so flux_spectrum of the result is the scattered power.
  • A snapshot’s slice_index counts grid cells including the absorber pad. Read positions and times from result.snapshot_axes[component] (coords, times_s, steps) rather than working them out. Frames are interval steps apart.
  • Far fields: compute_far_field(result.ntff_data, result.ntff_box, result.grid, theta, phi) returns a FarFieldResult; directivity(ff) and radiation_pattern(ff) read it. RCS uses compute_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.

FieldContents
time_seriesprobe records, shape (n_steps, n_columns)
dttime step in seconds
settling_dbworst 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_witnessstatus (pass, fail, undetermined, absent; only pass is a pass), reason, and the per-frequency shares behind settling_db
s_params, freqslumped/wire port S-matrix [receiver, driven, freq] and its frequencies (Hz)
wire_port_sparamswire-port details
ringdownring-down completion, when run(ringdown=...) was used
dft_planes{name: DFTPlaneProbe}
flux_monitors{name: FluxMonitor}
ntff_data, ntff_boxfar-field accumulation and its box
waveguide_ports, waveguide_sparams, waveguide_port_fluxper-port waveguide data from run(); for the full matrix use compute_waveguide_s_matrix()
snapshots, snapshot_axes{component: frames} and their coordinates and times
gridthe grid the run used
freq_rangethe analysis range used by find_resonances()
statefinal field state, for inspection or save_state

Helpers on Result:

MethodReturns
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_hzthe 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.0
print(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].

CalculatorResultMain fields
run(compute_s_params=True)Results_params, freqs
compute_msl_s_matrix()MSLSMatrixResultS, freqs, Z0, beta, port_names, reliable, settling_db, reference_impedances, sigma_max_excess, S_raw, passivity_correction
compute_waveguide_s_matrix()WaveguideSMatrixResults_params, freqs, port_names, port_directions, reference_planes, settling_db, s21_phase_residual_deg_rms
compute_coaxial_line_reflection()CoaxialLineReflectionResults11, freqs, status, annulus_cells, gamma, recurrence_residual, fit_residual, z0_numerical_ohm, termination
compute_coaxial_two_port()CoaxialTwoPortResults_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 uniform forward() check time_series and lumped/wire s_params for 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.9
report = 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_db before 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) without axes= is wrong at the default interval, because frames are interval steps apart. Pass axes=result.snapshot_axes instead.
Back to top ↑