# Sources and Ports

Channel: release; package: 1.8.0; source: 7d1e97331656dd74ad0f5471653ac0c5effdff70

Page: https://remilab.ai/rfx/versions/v1.8.0/guide/sources-ports/
Source: https://github.com/bk-squared/rfx/blob/7d1e97331656dd74ad0f5471653ac0c5effdff70/docs/public/guide/sources-ports.mdx

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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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.

```python
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:

```python
# 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.

```python
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`:

```python
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](https://remilab.ai/rfx/versions/v1.8.0/guide/probes-sparams/#s-parameter-extraction) 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:

```python
# 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.

```python
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:

```python
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:

```python
# 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.

```python
# 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(...)`:

```python
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:

```python
# 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](https://remilab.ai/rfx/versions/v1.8.0/guide/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"`.

```python
# 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](https://remilab.ai/rfx/versions/v1.8.0/api/sources-ports/) |
| `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](https://remilab.ai/rfx/versions/v1.8.0/api/sources-ports/) before a long run.
