REMI Lab
OPEN KNOWLEDGE / TOOLS

Sources and Ports

이 페이지의 내용

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 CustomWaveform
import 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:

# Linear
sim.add_polarized_source((0.025, 0.025, 0.01), polarization="ez",
amplitude_kind="current")
# 45-degree slant linear
sim.add_polarized_source((0.025, 0.025, 0.01), polarization="slant45",
amplitude_kind="current")
# Arbitrary real linear combination
sim.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), complex

See 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 surface
sim.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) complex
Z0 = msl.Z0 # (n_ports, n_freqs) complex

The 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) complex

normalize=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.s21

See 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

MethodUse caseS-parameter extraction
add_source()Resonance, field visualizationnone (soft source, no port)
add_polarized_source()Linear antenna excitation; circular/elliptical outputs are not validatednone (soft source, no port)
add_port(extent=None)Lumped-port S-paramsrun(compute_s_params=True); validated on uniform-Yee examples
add_port(extent=...)Wire / probe-feed S-paramsrun(compute_s_params=True); probe-feed examples, calibration caveated
add_lumped_rlc()Lumped circuit/load elementno 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-matrixcompute_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-matrixcompute_waveguide_s_matrix(); rectangular guide, cpml + 3D
add_coaxial_port()One-port coaxial line reflectioncompute_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 wavenone (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.

Back to top ↑