rfx distinguishes between sources (field injection without impedance loading) and ports (sources with a matched load for S-parameter extraction).
Setup used by the source and lumped-port snippets below:
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")Waveforms
add_source() and lumped/wire add_port() accept waveform objects.
Waveguide and TFSF sources use their own excitation arguments. The verified
add_polarized_source() scope below is linear and uses
GaussianPulse-compatible f0, bandwidth, and amplitude attributes.
GaussianPulse
A differentiated Gaussian pulse (no explicit carrier) — the default excitation
when add_source/add_port build a waveform for you:
from rfx import GaussianPulse
pulse = GaussianPulse( f0=2.4e9, # center frequency (Hz) bandwidth=0.5, # fractional bandwidth (class default 0.5) amplitude=1.0, # peak amplitude)f0 sets the center frequency and bandwidth the fractional spectral width;
a larger bandwidth gives broader (more wideband) excitation, a smaller one a
narrower band. The class default is bandwidth=0.5, but when rfx auto-builds
the pulse for a source or port it uses 0.8. The differentiated Gaussian
can carry a small numerical DC residual. Compare the low-frequency spectrum
and resulting fields when DC sensitivity matters.
ModulatedGaussian
A Gaussian-envelope modulated carrier (Meep-style). It usually reduces
low-frequency/DC content relative to the differentiated Gaussian, but a finite
sampled pulse does not generally have an exactly zero integral. Its spectrum is
centered near f0 with fractional width bandwidth, which makes it useful for
resonance-focused add_source() runs:
from rfx import ModulatedGaussian
waveform = ModulatedGaussian(f0=3e9, bandwidth=0.6)When waveform is omitted, add_source/add_port default to GaussianPulse
(not ModulatedGaussian); pass ModulatedGaussian(...) explicitly when its
carrier-centered spectrum is preferable, then check the sampled spectrum if a
low-frequency component would affect the result.
CWSource
Continuous-wave sinusoid, useful for steady-state field visualization:
from rfx import CWSource
cw = CWSource( f0=5.8e9, # frequency (Hz) ramp_steps=200, # cosine-taper onset over 200 source cycles (default 50; 0 = instant on))CustomWaveform
Arbitrary time-domain waveform via a JAX-compatible callable:
from rfx import CustomWaveformimport jax.numpy as jnp
def chirp(t): f_start, f_end = 1e9, 4e9 rate = (f_end - f_start) / 10e-9 return jnp.sin(2 * jnp.pi * (f_start * t + 0.5 * rate * t**2))
sim.add_source((0.02, 0.02, 0.01), "ez", waveform=CustomWaveform(func=chirp), amplitude_kind="current")Point source (add_source)
A soft current source injected at a single Yee cell. No resistive load — cavity Q is not affected.
sim.add_source( position=(0.025, 0.025, 0.010), # (x, y, z) in meters component="ez", # "ex", "ey", or "ez" waveform=GaussianPulse(f0=3e9), amplitude_kind="current", # or "field"; see below)Use add_source for resonance characterization (Harminv) where port loading would suppress the ring-down signal.
What the waveform amplitude means is controlled by amplitude_kind
(issue #571). With amplitude_kind='current' the amplitude is a current
I(t) in amperes, injected as E += Cb·I/dV on every path and boundary
(Cb = (dt/ε)/(1 + σ·dt/(2ε)) at the source cell, dV its cell volume), so
the injected power does not change when the mesh is refined — Meep’s
convention. With amplitude_kind='field' the amplitude is a raw E-field
increment per step, E += w(t), again the same on every path and boundary.
Omitting it
keeps a deprecated per-path legacy meaning (raw add on uniform+PEC,
Cb-normalized add on uniform+CPML/UPML, current on non-uniform meshes)
and emits one DeprecationWarning per simulation naming which one applies.
amplitude_kind becomes required in 1.9, and the default becomes
'current' in 2.0.
To migrate an open-boundary uniform script exactly, multiply the waveform
amplitude by the cell volume dV and pass amplitude_kind='current'.
Polarized source (add_polarized_source)
Injects one or two Cartesian source components. The documented scope is linear:
# Linearsim.add_polarized_source((0.025, 0.025, 0.01), polarization="ez", amplitude_kind="current")
# 45-degree slant linearsim.add_polarized_source((0.025, 0.025, 0.01), polarization="slant45", amplitude_kind="current")
# Arbitrary real linear combinationsim.add_polarized_source( (0.025, 0.025, 0.01), polarization=(1.0, -0.5), # real (Ex, Ey), normalized internally waveform=GaussianPulse(f0=3e9), amplitude_kind="current",)Verified shortcuts are "ex", "ey", "ez", and "slant45".
"circular"/"rhcp", "lhcp", and complex Jones tuples are accepted by
the API, but the generated components’ time-domain quadrature has not been
independently verified. Do not infer circular/elliptical polarization, axial
ratio, or polarization sense from those inputs.
Lumped port (add_port)
A single-cell port with a resistive load equal to impedance. Enables S-parameter extraction referenced to that impedance. It is validated on the uniform-Yee lumped-port examples; scope any broader result to that evidence.
sim.add_port( position=(0.01, 0.025, 0.010), component="ez", impedance=50.0, # ohms waveform=GaussianPulse(f0=5e9),)Run with compute_s_params=True to extract the S-matrix. When no frequency
grid is supplied, rfx uses 50 points from freq_max/10 to freq_max:
result = sim.run(n_steps=2000, compute_s_params=True)s11 = result.s_params[0, 0, :] # s_params shape (n_ports, n_ports, n_freqs), complexSee Probes and S-parameters for the full extraction, plotting, and Touchstone-export path.
!!! tip
Multiple add_port() calls create a multi-port structure. rfx drives one
port at a time and assembles the full N×N S-matrix automatically — you do
not need to set excite=False yourself. The set must be homogeneous
(all lumped or all wire); a mixed lumped+wire set is not supported.
Wire port (add_port with extent)
A wire port spans multiple Yee cells along the port axis, connecting conductor to conductor across a gap. It is a common model for probe- and coaxial-style feeds:
# A wire port cannot share a Simulation with the lumped port above, so this# section starts a fresh one.sim = Simulation( freq_max=6e9, domain=(0.06, 0.06, 0.012), dx=1e-3, boundary="cpml", cpml_layers=6,)
# Port spans 1.6 mm in z, from substrate bottom to patch surfacesim.add_port( position=(0.029, 0.030, 0.0), # start of wire component="ez", impedance=50.0, extent=0.0016, # wire length in meters waveform=GaussianPulse(f0=2.4e9),)The extent parameter turns the lumped port into a WirePort. S-parameters are extracted from voltage/current integrals over the wire span. The public examples exercise this for probe-fed resonance workflows; treat absolute calibration outside that regime as unvalidated.
!!! warning
extent must be aligned with the component direction. For component="ez", extent
spans in the z-direction from position[2] to position[2] + extent.
Lumped RLC element (add_lumped_rlc)
add_lumped_rlc(...) places a discrete R, L, C, or RLC element into the FDTD
update at one Yee cell. It is useful for modeling a local load or circuit
element, but it is not an impedance-referenced port by itself. To measure the
reflection that load presents to a reference line, excite the structure with
add_port(..., impedance=Z0) and place the RLC element as the load.
sim.add_lumped_rlc( position=(0.03, 0.02, 0.01), component="ez", R=50.0, C=0.2e-12,)On the uniform single-device differentiable path, registered RLC elements now
affect forward(...), and scalar component values can be overridden by tracers:
import jax.numpy as jnp
R = 75.0 # the overridden resistance; pass a tracer here under jax.grad
result = sim.forward( port_s11_freqs=jnp.array([5e9]), rlc_values_override={0: {"R": R}},)The 0 key is the registration order among add_lumped_rlc(...) calls.
Non-uniform and distributed forward(...) lanes reject this override.
Microstrip-line port (add_msl_port)
add_msl_port(...) is the specialized full-strip microstrip-line port. It is
not routed through run(compute_s_params=True). Use the dedicated calculator:
# A microstrip port needs its own board, and cannot share a Simulation with# the lumped/wire ports above. Ground, substrate, and trace are explicit.sim = Simulation( freq_max=6e9, domain=(0.020, 0.010, 0.004), dx=0.25e-3, boundary="cpml", cpml_layers=3,)sim.add_material("substrate", eps_r=3.2, sigma=0.01)sim.add(Box((0.75e-3, 0.75e-3, 0.75e-3), (19.25e-3, 9.25e-3, 1.25e-3)), material="pec")sim.add(Box((0.75e-3, 0.75e-3, 1.25e-3), (19.25e-3, 9.25e-3, 2.25e-3)), material="substrate")sim.add(Box((0.75e-3, 4.50e-3, 2.25e-3), (19.25e-3, 5.50e-3, 2.75e-3)), material="pec")
sim.add_msl_port( position=(0.004, 0.005, 1.25e-3), width=1.0e-3, height=1.0e-3, direction="+x", impedance=50.0, eps_r_sub=3.2, name="in",)sim.add_msl_port( position=(0.016, 0.005, 1.25e-3), width=1.0e-3, height=1.0e-3, direction="-x", impedance=50.0, eps_r_sub=3.2, name="out",)
msl = sim.compute_msl_s_matrix(n_freqs=101, num_periods=40)S = msl.S # (n_ports, n_ports, n_freqs) complexZ0 = msl.Z0 # (n_ports, n_freqs) complexThe calculator drives each port in turn and de-embeds β and Z0 downstream of the
feed plane with an N-probe (default n_probes=5) least-squares
wave-decomposition extractor. It is a uniform-mesh path; unsupported
mesh/source/port combinations are rejected rather than silently returning
None. num_periods defaults to 40 because MSL transients drain slowly. The
thru-line and notch examples define the tested uniform-Yee configurations.
Feed direction
direction accepts "+x", "-x", "+y" and "-y". The board lies in the
xy-plane, so the feed may run along either in-board axis — useful when the
geometry comes from a CAD import and you do not get to choose its orientation.
position is a physical (x, y, z) point: the coordinate on the propagation
axis is the feed plane, the coordinate on the other in-board axis is the trace
centre, and z is the substrate bottom. width spans whichever in-board axis
is not the propagation axis.
"+z" / "-z" raise ValueError. The substrate normal is fixed to z by the
position + scalar height contract, so a z-propagating microstrip would need
its substrate normal along x or y — which that contract cannot express, and
which the mode solve, the "ez" source component, the modal voltage and the
trace-conductor scan all assume. Rotate the board instead.
mode="eigenmode" remains "+x" / "-x" only.
Waveguide port (add_waveguide_port)
Modal waveguide excitation for rectangular waveguide S-matrix extraction.
Injects a specific TE/TM mode and decomposes the reflected/transmitted fields into the same modal basis. Requires boundary="cpml" (with cpml_layers > 0) and mode="3d"; it cannot be combined with lumped ports or a TFSF source. The public examples cover rectangular guides — validate branch or T-junction geometries against a reference before reporting.
# A waveguide port cannot share a Simulation with lumped ports, so this# section starts a fresh guide.sim = Simulation( freq_max=11e9, domain=(0.10, 0.023, 0.010), dx=1e-3, boundary="cpml", cpml_layers=8,)
sim.add_waveguide_port( x_position=0.01, # physical x-coordinate of port face y_range=(0.0, 0.023), # aperture y-extent z_range=(0.0, 0.010), # aperture z-extent mode=(1, 0), # TE10 mode mode_type="TE", direction="+x", # propagation direction f0=10e9, bandwidth=0.5, name="port1",)For a full multi-port S-matrix, use compute_waveguide_s_matrix(...):
sim.add_waveguide_port( x_position=0.09, y_range=(0.0, 0.023), z_range=(0.0, 0.010), direction="-x", f0=10e9, bandwidth=0.5, name="port2",)
wg = sim.compute_waveguide_s_matrix(num_periods=30, normalize=True)S = wg.s_params # (n_ports, n_ports, n_freqs) complexnormalize=True cancels one-way Yee dispersion in the transmission
(off-diagonal) terms and is the right choice for S21. It does not correct
the round-trip dispersion in reflection. On a uniform grid, S11 studies may use
normalize=False (default) or normalize="flux"; non-uniform waveguide runs
reject normalize=False and require normalize="flux". If a passive
normalize=False result rises above the documented single-run over-unity
range, rfx emits a soft advisory rather than silently treating it as ordinary
evidence. Use the guide-recommended flux normalization or a reference check
before reporting that number.
A plain run(...) also populates result.waveguide_sparams — per-port,
plane-calibrated s11/s21 from that single excitation. This is a diagnostic
of one driven configuration, not the full driven-in-turn matrix that
compute_waveguide_s_matrix assembles:
# run() supports one waveguide port, so drive a single-port guide here.sim = Simulation( freq_max=11e9, domain=(0.10, 0.023, 0.010), dx=1e-3, boundary="cpml", cpml_layers=8,)sim.add_waveguide_port( x_position=0.01, y_range=(0.0, 0.023), z_range=(0.0, 0.010), f0=10e9, bandwidth=0.5, name="port1",)
result = sim.run(n_steps=3000)sp = result.waveguide_sparams["port1"]# sp.freqs, sp.s11, sp.s21See Waveguide ports for mode selection, calibration planes, and validated gates.
TFSF plane-wave source (add_tfsf_source)
Total-field/scattered-field plane wave for scattering and RCS simulations.
Requires boundary="cpml" (with cpml_layers > 0) and a 3D or 2D-TE/TM mode.
Scope is deliberately narrow: propagation along x only (direction "+x"/"-x")
and polarization "ez" or "ey".
# TFSF cannot share a Simulation with lumped or waveguide ports.sim = Simulation( freq_max=8e9, domain=(0.06, 0.06, 0.06), dx=3e-3, boundary="cpml", cpml_layers=6,)
sim.add_tfsf_source( f0=5e9, bandwidth=0.4, polarization="ez", direction="+x", # propagation direction (+x or -x) angle_deg=0.0, # oblique incidence angle (|angle_deg| < 90, degrees from normal) method="bloch", # oblique path: "bloch" (periodic unit cell) or # "methodB" (open-domain compact scatterer))For oblique incidence (angle_deg != 0) the method argument selects the
transverse boundary-value problem: "bloch" is the laterally periodic
(unit-cell / infinite-array) path, "methodB" is the open-domain 2.5-D path
for a compact isolated scatterer (polarization="ez", uniform grid, and
direction="+x" only — "-x" raises NotImplementedError under
"methodB" at run() time, since the Simulation API defers TFSF
construction to the run; mirror the geometry instead).
!!! warning
add_tfsf_source cannot be combined with lumped ports or waveguide ports.
Summary table
| Method | Use case | S-parameter extraction |
|---|---|---|
add_source() | Resonance, field visualization | none (soft source, no port) |
add_polarized_source() | Linear antenna excitation; circular/elliptical outputs are not validated | none (soft source, no port) |
add_port(extent=None) | Lumped-port S-params | run(compute_s_params=True); validated on uniform-Yee examples |
add_port(extent=...) | Wire / probe-feed S-params | run(compute_s_params=True); probe-feed examples, calibration caveated |
add_lumped_rlc() | Lumped circuit/load element | no direct S-parameter calculator; can be a differentiable scalar load via forward(rlc_values_override=...) in a uniform single-device calculation |
add_msl_port() | Full-strip microstrip S-matrix | compute_msl_s_matrix(); Laplace/quasi-TEM accepts +x, -x, +y, and -y; the x-only eigenmode code path remains outside the public support envelope |
add_waveguide_port() | Modal waveguide S-matrix | compute_waveguide_s_matrix(); rectangular guide, cpml + 3D |
add_coaxial_port() | One-port coaxial line reflection | compute_coaxial_line_reflection(); float32 precision; nonperiodic 3D second-order uniform Yee grid; CPML on all six boundary faces with positive thickness on both z faces; cpml_axes="z"; exactly one face="top" port; see the API constraints |
add_tfsf_source() | RCS / scattering plane wave | none (not a port) |
compute_coaxial_line_reflection() constructs its own line, TEM source, DFT
planes, and termination. Do not add separate geometry, thin conductors, lumped
RLC elements, probes, field monitors, NTFF boxes, or add_coaxial_*
termination helpers to that simulation; the method rejects them. The port’s z
position, pin_length, and impedance do not set the internally derived line
layout or loads. Use feed_impedance, and use dut_impedance only with
termination="matched". At least three probe planes are required, and every
requested plane must fit before the source. See the
complete calculator rules before a long run.
