This page shows how to excite a model. A source injects a field and has no impedance. A port also terminates the line in a known impedance, so rfx can compute S-parameters from it. Core Concepts explains the difference.
Each port family has its own S-parameter calculator. Only that calculator gives S-parameters for it:
| Structure | Port | Calculator |
|---|---|---|
| discrete feed, component terminals | add_port(...) (lumped; with extent= a wire port) | run(compute_s_params=True), or forward(port_s11_freqs=...) for gradients |
| microstrip line | add_msl_port(...) | compute_msl_s_matrix() |
| rectangular waveguide | add_waveguide_port(...) | compute_waveguide_s_matrix() |
| coaxial line | add_coaxial_port(...) | compute_coaxial_line_reflection(), compute_coaxial_two_port() |
Port families cannot be mixed in one S-parameter calculation. If you are not
sure which calculator a model needs, sim.s_matrix_lane() tells you without
running anything, and sim.compute_s_matrix(**kwargs) calls it for you. The
one exception is a lumped/wire-only model, which compute_s_matrix refuses:
call run(compute_s_params=True) for those.
Waveforms
Point sources and lumped/wire ports take a waveform object. Waveguide and TFSF
sources take f0 and bandwidth directly.
import jax.numpy as jnpfrom rfx import CustomWaveform, CWSource, GaussianPulse, ModulatedGaussian
pulse = GaussianPulse(f0=2.4e9, bandwidth=0.5) # broadband, no carrierburst = ModulatedGaussian(f0=3e9, bandwidth=0.6) # carrier with a Gaussian envelopecw = CWSource(f0=5.8e9, ramp_steps=50) # sinusoid, tapered on over 50 cycles
def chirp(t): rate = (4e9 - 1e9) / 10e-9 return jnp.sin(2 * jnp.pi * (1e9 * t + 0.5 * rate * t**2))
sweep = CustomWaveform(func=chirp) # any JAX-traceable f(t)| Waveform | Spectrum | Use it for |
|---|---|---|
GaussianPulse | differentiated Gaussian; broad, extends well below f0 | the default; broadband S-parameters |
ModulatedGaussian | Gaussian centred on f0, width f0·bandwidth | a band away from DC; resonance searches |
CWSource | one frequency | steady-state field pictures |
CustomWaveform | whatever func gives | chirps, measured drive signals |
bandwidth is fractional. When you omit waveform, rfx uses
GaussianPulse(f0=freq_max/2, bandwidth=0.8).
A pulse that turns on abruptly leaves a small net charge behind. It usually
does not matter. If you stop a run on until_decay and a static remnant keeps
it from stopping, raise GaussianPulse(cutoff=4.5) or use a
ModulatedGaussian with bandwidth <= 0.4.
Point sources
add_source is a soft source at one grid edge. It does not load the
structure, so a cavity’s Q is unchanged. Use it for resonances, field
patterns and far fields.
from rfx import Box, Simulation
sim = Simulation(freq_max=6e9, domain=(0.05, 0.05, 0.02), dx=2.5e-3, boundary="cpml", cpml_layers=6)sim.add_material("substrate", eps_r=3.2, sigma=0.01)sim.add(Box((0.010, 0.010, 0.005), (0.040, 0.040, 0.015)), material="substrate")
sim.add_source((0.025, 0.025, 0.010), "ez", waveform=GaussianPulse(f0=3e9), amplitude_kind="current")Always pass amplitude_kind:
"current": the amplitude is a current moment in A·m. The injected power does not change when you refine the mesh. Use this by default."field": the amplitude is added to E each step, in V/m.
Omitting it or passing None selects "current" and warns once per simulation.
The default is boundary- and mesh-independent on Yee paths; ADI requires
explicit "field". Witness: tests/unit/sources/test_source_amplitude_kind.py.
Polarized sources
add_polarized_source drives two components together:
sim.add_polarized_source((0.025, 0.025, 0.010), polarization="slant45", amplitude_kind="current")sim.add_polarized_source((0.025, 0.025, 0.010), polarization=(1.0, -0.5), waveform=GaussianPulse(f0=3e9), amplitude_kind="current")"ex", "ey", "ez", "slant45" and real (Ex, Ey) tuples give linear
polarization. "circular", "rhcp", "lhcp" and complex tuples are accepted,
but their time-domain phasing has not been verified. Do not rely on them for
axial ratio or handedness.
Plane waves (TFSF)
add_tfsf_source launches a plane wave inside a total-field region, for
scattering and RCS. It travels along x ("+x" or "-x") with "ez" or "ey"
polarization, and needs a CPML boundary.
scatter = Simulation(freq_max=8e9, domain=(0.06, 0.06, 0.06), dx=3e-3, boundary="cpml", cpml_layers=6)scatter.add_tfsf_source(f0=5e9, bandwidth=0.4, polarization="ez", direction="+x")For oblique incidence, set angle_deg and choose method:
method="bloch"(default): a periodic unit cell, for infinite arrays and surfaces. Its fields are complex, and frequency monitors do not account for that.method="methodB": an open domain around a compact scatterer. It needspolarization="ez",direction="+x"and a uniform grid.
TFSF cannot share a simulation with any port. It is not a port and gives no S-parameters. For RCS, see Far-Field and RCS.
Lumped and wire ports
add_port is a feed with a resistive load equal to impedance. It is the
port to use for a discrete feed, a probe feed, or a component’s terminals.
- Without
extent, it is a lumped port: one cell. - With
extent, it is a wire port that spansextentmetres along its component axis, frompositiontoposition + extent. Use it to bridge a gap between two conductors, such as a probe feed through a substrate. radius=declares the wire port’s probe-pin radius, within the local thin-probe mesh restrictions. If omitted, the effective pin radius follows the mesh and its series inductance changes under refinement.
sim = Simulation(freq_max=6e9, domain=(0.05, 0.05, 0.02), dx=2.5e-3, boundary="cpml", cpml_layers=6)sim.add_port((0.01, 0.025, 0.010), "ez", impedance=50.0, waveform=GaussianPulse(f0=3e9))sim.add_port((0.04, 0.025, 0.010), "ez", impedance=50.0, waveform=GaussianPulse(f0=3e9))
result = sim.run(n_steps=300, compute_s_params=True, s_param_freqs=jnp.linspace(1e9, 5e9, 21))s11 = result.s_params[0, 0, :] # S[receiver, driven, freq], complexs21 = result.s_params[1, 0, :]With several ports, rfx drives each in turn and assembles the full matrix.
All ports in one calculation must be lumped, or all wire. Without
s_param_freqs, rfx uses 50 points from freq_max/10 to freq_max. The run
above is far too short to settle; it only shows the calls.
Probes and S-Parameters covers plotting and
Touchstone export.
Getting accurate S11
- Resolve the wavelength. Use about 60 cells per wavelength at the
highest frequency you care about. At 60 cells per wavelength, one-port |S11|
lands within 0.54 dB of the closed form on resistive loads, and exactly on a
short or open. At 30 cells per wavelength, magnitudes are within 1.1 dB,
and the line’s phase-crossing frequencies sit 1.7–2.9 % from the closed
form. These figures come from the lumped/wire chain battery, which measures
forward(port_s11_freqs=...)on a one-cell parallel-plate line. - Know the reference plane. A one-cell port reads its voltage at the port node and its current half a cell toward the line.
- Lumped and wire ports give identical S11 on the same cells.
Completing a short record
A resonant structure can take far longer to ring down than you want to
simulate. For wire ports, run(..., ringdown=RingdownSpec()) fits the
decaying tail of the record and adds the unrecorded part of each spectrum in
closed form:
import numpy as npfrom rfx.ringdown import RingdownSpec
MM = 1e-3box = Simulation(freq_max=20e9, domain=(12 * MM, 11 * MM, 5 * MM), dx=1 * MM, boundary="pec")box.add_material("fill", eps_r=2.2, sigma=2 * np.pi * 12.5e9 * 8.854e-12 * 2.2 / 300)box.add(Box((0, 0, 0), (12 * MM, 11 * MM, 5 * MM)), material="fill")box.add_port((2 * MM, 2 * MM, 0.0), "ez", impedance=50.0, extent=1 * MM, waveform=GaussianPulse(f0=13e9, bandwidth=0.8, cutoff=4.5))
short = box.run(n_steps=1500, compute_s_params=True, s_param_freqs=np.linspace(8e9, 18e9, 101), ringdown=RingdownSpec())print(short.ringdown.report.ok) # did the completion pass its checks?s11_completed = short.ringdown.s_params[0, 0, :]short.s_params is still the plain, truncated result. The completed matrix is
short.ringdown.s_params, and short.ringdown.report lists the fitted poles
and the checks. report.ok is decided by comparing completions from two
different windows of the record. These checks catch a bad fit. They are not an
error bound.
ringdown= works with wire ports on a fixed-length record: one port on a
uniform mesh, or several on a graded mesh. The source must be off in the
second half of the record. Lumped, microstrip, waveguide and coaxial ports,
TFSF, until_decay and devices= are refused.
forward(..., ringdown=RingdownSpec()) puts the same completion inside the
differentiable program, so jax.grad sees the ringing too; it is also refused
with distributed=True.
Ending a line at a port
When a trace runs to the edge of the domain, rfx extends it into the absorber,
as if the line continued forever. If the line is meant to end at your port,
name its conductor with terminates=:
line = Simulation(freq_max=6e9, domain=(0.04, 0.02, 0.01), dx=1e-3, boundary="cpml", cpml_layers=6)line.add_thin_conductor(Box((0.0, 0.0, 0.001), (0.04, 0.02, 0.001))) # groundstrip = Box((0.0, 0.009, 0.004), (0.04, 0.011, 0.004))line.add_thin_conductor(strip)line.add_port((0.035, 0.010, 0.001), "ez", extent=3e-3, waveform=GaussianPulse(f0=3e9), terminates=strip)Preflight warns (port_conductor_continues) when a port’s conductor runs into
the absorber and no port terminates it. A ground plane should continue; name
only the line the port ends. Microstrip ports do this automatically for the
trace under them.
Lumped circuit elements
add_lumped_rlc puts a resistor, inductor, capacitor or RLC combination on
one grid edge. It is not a port: it has no reference impedance. To measure
the reflection it presents, drive the line with add_port(...) and use the
RLC element as the load.
sim.add_lumped_rlc((0.03, 0.02, 0.01), "ez", R=50.0, C=0.2e-12)topology is "series" (default) or "parallel". On the uniform,
single-device forward() path, rlc_values_override={0: {"R": R}} makes a
component value differentiable. The key is the element’s registration order.
Microstrip ports
add_msl_port spans the full width under the trace and extracts the line’s
wave amplitudes from a row of probe planes downstream of the feed. Use
compute_msl_s_matrix(), not run(compute_s_params=True):
# Run from the repository root to use the tutorial's edge-aware board.from examples.tutorials.ports_and_sparams_101 import build_microstrip_ports
board = build_microstrip_ports()msl = board.compute_msl_s_matrix(freqs=np.linspace(2e9, 6e9, 13), num_periods=20)print(np.abs(msl.S[1, 0])) # |S21| across the bandposition is (feed plane, trace centre, substrate bottom). direction may
be "+x", "-x", "+y" or "-y"; the substrate normal is always z. S is
returned as extracted. A warning names any bins where it is not passive;
enforce_passivity=True projects it instead. msl.reliable flags bins with
too little signal to trust.
The builder in examples/tutorials/ports_and_sparams_101.py uses a
44 × 34 mm domain, edge-aware x/y profiles, and a uniform z profile of 16 cells of 0.25 mm. It
builds and preflights this board without solving it in the tutorial; the
calculation above runs it. The tutorial’s live generic-port S11 examples
use S11_STEPS = 600 each. This board is coarse for speed, so check the fitted
Z0 against the closed form.
S11 and S21 use the measured V and I with the analytic Z0, not the fitted
Z0. Refine before you quote Z0.
Getting an accurate microstrip result:
- The substrate sets the cell size. On a thin substrate the substrate thickness binds long before the wavelength does. Preflight warns below 4 cells through the substrate. Start there, then refine and check that your answer stops moving.
- End the line at its ports, not in the absorber. CPML is a much poorer termination for a two-conductor line than for a plane wave. Put a port at each end of the line.
- Give the probes room. The automatic probe offset and spacing are chosen
in cells from the mesh under the port, and recounted at run time on graded
or automatic meshes. On a short feed, rfx warns if there is no clean place
for them. Check
msl.probe_clearance, and lengthen the feed if it is insufficient.
Waveguide ports
add_waveguide_port launches a TE or TM mode of a rectangular aperture and
decomposes the fields into that mode. Its aperture defines the guide. A port
that spans the whole cross-section uses the transverse domain faces as the
guide walls, so declare them PEC and put the absorber on the port axis only;
on a uniform mesh boundary="cpml" on every face is refused.
guide = Simulation(freq_max=11e9, domain=(0.10, 0.023, 0.010), dx=1e-3, boundary={"x": "cpml", "y": "pec", "z": "pec"}, cpml_layers=8)guide.add_waveguide_port(0.01, mode=(1, 0), mode_type="TE", direction="+x", f0=10e9, bandwidth=0.5, name="port1")guide.add_waveguide_port(0.09, mode=(1, 0), mode_type="TE", direction="-x", f0=10e9, bandwidth=0.5, name="port2")print(guide.s_matrix_lane())Compute the matrix with guide.compute_waveguide_s_matrix(normalize=...).
Waveguide Ports covers normalization,
calibration planes and junctions.
Coaxial ports
The coaxial calculators build their own line: a coax along z with a matched
feed, a TEM source, and a row of probe planes. You register one
add_coaxial_port for the cross-section (its x/y centre, radii and
waveform) and call a calculator:
compute_coaxial_line_reflection(termination=...): one port. The line ends in"short"or a"matched"load;dut_impedance=Rmakes the load a resistor.compute_coaxial_two_port(): a through line with a port at each end.
coax = Simulation(freq_max=40e9, domain=(0.008, 0.008, 0.040), boundary="cpml", cpml_layers=8)coax.add_coaxial_port((0.004, 0.004, 0.020), face="top", pin_radius=0.635e-3, outer_radius=2.055e-3, waveform=GaussianPulse(f0=8e9, bandwidth=1.2))
refl = coax.compute_coaxial_line_reflection( termination="short", n_steps=3000, freqs=np.linspace(4e9, 12e9, 5))print(refl.status, f"{refl.annulus_cells:.1f} annulus cells")print(np.abs(refl.s11))The line’s conductors are realized PEC. Their wall thickness is 1 mm, or the room to the nearest absorber less one cell if that is smaller; a line with less than a cell of room raises.
What the calculators need:
- A uniform, float32, 3-D Yee grid with CPML on all six faces. Exactly one
coaxial port with
face="top", and nothing else in the model: no geometry, sheets, probes, monitors or other ports. The calculator refuses extras rather than ignoring them. - The port’s z position,
pin_lengthandimpedancedo not place the line or set its loads. Usefeed_impedance=anddut_impedance=.
Getting an accurate result:
- Use at least 4 cells across the annulus (between pin and outer
conductor). Raise
freq_maxto shrink the cell.statusreports"under_resolved"below about 3.5. The example above uses 3.8 for speed. - Keep the probe span physical. Probe planes are placed in cells
(
probe_start_cells,probe_spacing_cells), so refining the mesh shortens the length the phase fit sees. Scale the spacing up when you refine. - Leave room around the line. Both coax calculators absorb on all three axes, so an open end radiates instead of ringing in a closed can. A line whose outer wall comes within one cell of a lateral absorber is refused.
Limits
- Sources give no S-parameters.
add_source, polarized sources and TFSF have no impedance. Use a port and its calculator. - One port family per calculation. Mixed lumped + microstrip and coax-to-microstrip calculators exist but are experimental; measure each line with its own calculator instead.
- Circular polarization from
add_polarized_sourceis unverified. - Lumped/wire ports need about 60 cells per wavelength for S11 within
about 0.5 dB.
forward(port_s11_freqs=...)is uniform and single-device only. Wire ports on a graded mesh are experimental. - Microstrip
mode="eigenmode"is unsupported. Graded-mesh microstrip has no external comparison yet. ADI, subgridding and TFSF are refused. - Coax lanes cover one geometry family, and only azimuthally symmetric discontinuities.
- TFSF on a graded axis: preflight warns, because the scattered-field region picks up a spurious field (about 0.7 of the total-field peak with no scatterer). Normal incidence along ±x needs a uniform x axis.
- Detailed scope for every port family is in the S-parameter support matrix.
