# Sources and Ports

Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723

Page: https://remilab.ai/rfx/api/sources-ports/
Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/api/sources-ports.mdx

This page lists the excitation calls, the port families and the S-parameter
calculator for each family. 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. Worked examples are in the
[Sources and Ports guide](https://remilab.ai/rfx/model/ports/) and
[Probes and S-Parameters](https://remilab.ai/rfx/measure/s-parameters/).

## Waveforms

`add_source()`, `add_polarized_source()`, `add_port()` and `add_msl_port()`
take a `waveform=` object. Waveguide, TFSF and Floquet sources take their own
pulse arguments instead.

| Waveform | Signature | Use |
|---|---|---|
| `GaussianPulse` | `GaussianPulse(f0, bandwidth=0.5, amplitude=1.0, cutoff=3.0)` | default wideband pulse (differentiated Gaussian, almost no DC) |
| `ModulatedGaussian` | `ModulatedGaussian(f0, bandwidth=0.5, amplitude=1.0, cutoff=5.0)` | band-pass pulse centred on `f0` |
| `CWSource` | `CWSource(f0, amplitude=1.0, ramp_steps=50)` | single frequency with a ramped start, for steady-state fields |
| `CustomWaveform` | `CustomWaveform(func)` | your own `func(t) -> amplitude`, with `t` in seconds |

`bandwidth` is a fraction of `f0`. If you omit `waveform`, rfx uses a
`GaussianPulse` at `freq_max / 2`.

## Sources

```python
import numpy as np
from rfx import GaussianPulse, Simulation

sim = Simulation(freq_max=6e9, domain=(0.03, 0.03, 0.03), dx=1e-3,
                 boundary="cpml", cpml_layers=8)

sim.add_source((0.010, 0.015, 0.015), "ez",
               waveform=GaussianPulse(f0=3e9, bandwidth=0.8),
               amplitude_kind="current")
sim.add_polarized_source((0.010, 0.020, 0.015), polarization="slant45",
                         amplitude_kind="current")
```

| Method | Signature | Notes |
|---|---|---|
| `add_source` | `add_source(position, component="ez", *, waveform=None, amplitude_kind=None)` | soft point source in one cell; no load, so it suits resonance and Harminv work |
| `add_polarized_source` | `add_polarized_source(position, *, polarization="ez", waveform=None, amplitude_kind=None)` | linear polarization: `"ex"`, `"ey"`, `"ez"`, `"slant45"` or a real `(Ex, Ey)` tuple |
| `add_tfsf_source` | `add_tfsf_source(*, f0=None, bandwidth=0.5, amplitude=1.0, margin=3, polarization="ez", direction="+x", angle_deg=0.0, waveform="differentiated_gaussian", method="bloch")` | plane wave travelling along x, for scattering; 3-D or 2-D (`mode="2d_tmz"`/`"2d_tez"`), with CPML |
| `add_floquet_port` | see the [generated reference](https://remilab.ai/rfx/api/generated/) | experimental; no documented S-parameter API |

**`amplitude_kind`.** The default is `"current"`: the waveform amplitude is
a current moment in A·m, injected as `E += Cb * I / dV` on Yee paths.
`"field"` adds the waveform directly to E each step. The signature sentinel
`None` also resolves to `"current"` and warns once per simulation; pass the
kind explicitly to silence that warning. ADI requires explicit `"field"`.
Witness: `tests/unit/sources/test_source_amplitude_kind.py`.

`add_polarized_source` also accepts `"rhcp"`, `"lhcp"` and complex tuples, but
circular polarization from this source has not been verified. Don't use it to
report axial ratio or sense.

For the plane wave on a graded mesh, only normal incidence along `+x` or `-x`
is valid, and only when x itself is uniform. A graded x axis still runs, with
a preflight warning. Far-field and RCS use is covered in
[Far-Field and RCS](https://remilab.ai/rfx/measure/far-field-and-rcs/).

## Ports

| Method | Port family | Calculator |
|---|---|---|
| `add_port(...)` | lumped (one cell) or wire (`extent=`) | `run(compute_s_params=True)`, or `forward(port_s11_freqs=...)` for S11 gradients |
| `add_msl_port(...)` | microstrip line | `compute_msl_s_matrix()` |
| `add_waveguide_port(...)` | rectangular waveguide | `compute_waveguide_s_matrix()` |
| `add_coaxial_port(...)` | coaxial line | `compute_coaxial_line_reflection()` (one-port), `compute_coaxial_two_port()` |

### Lumped and wire ports

```text
add_port(position, component="ez", *, impedance=50.0, waveform=None,
         extent=None, radius=None, excite=True, direction=None,
         reference_plane_cells=None, terminates=None) -> Simulation
```

- `impedance`: the port's reference impedance in ohms.
- `extent`: omit for a one-cell lumped port. Set it (in metres) to make a wire
  port spanning `extent` from `position` along the component axis.
- `radius`: the physical probe-pin radius for a wire port, subject to the
  local thin-probe mesh restrictions. With `radius=None`, the effective pin
  radius follows the mesh, so refining changes its series inductance.
- `excite=False`: a matched load only, no source. Use it for ports that only
  receive.
- `direction`: outward normal of the port (`"+x"`, `"-x"`, `"+y"`, `"-y"`),
  used to split V and I into incoming and outgoing waves. Detected from the
  nearest face if omitted.
- `terminates`: the conductor(s) this port ends, given as the shape or its
  index in the geometry list. A line should end at its ports, not in the
  absorber.
- `reference_plane_cells=N`: opt-in reference planes for a wire port's
  off-diagonal terms. See the [generated reference](https://remilab.ai/rfx/api/generated/).

```python
lp = Simulation(freq_max=6e9, domain=(0.03, 0.03, 0.03), dx=1e-3,
                boundary="cpml", cpml_layers=8)
lp.add_port((0.015, 0.015, 0.015), "ez", impedance=50.0,
            waveform=GaussianPulse(f0=3e9, bandwidth=0.8))
lp.add_lumped_rlc((0.015, 0.015, 0.016), "ez", R=50.0, C=1e-12,
                  topology="series")

print(lp.s_matrix_lane())                                   # run(compute_s_params=True)
print(lp.preflight_sparameters(calculator="run").ok)
result = lp.run(n_steps=600, compute_s_params=True,
                s_param_freqs=np.linspace(1e9, 5e9, 9))
print(result.s_params.shape)                                # (n_ports, n_ports, n_freqs)
```

`result.s_params[i, j, k]` is the response at port `i` with port `j` driven, at
frequency `result.freqs[k]`. `result.s11()` and `result.s_db(2, 1)` are
shortcuts. The mesh here is coarse for speed. For a real one-port S11, use
about 60 cells per wavelength at the highest frequency of interest.

### Lumped R, L, C elements

```text
add_lumped_rlc(position, component="ez", *, R=0.0, L=0.0, C=0.0,
               topology="series") -> Simulation
```

Puts an R, L and/or C in one cell, updated with the fields. `topology` is
`"series"` or `"parallel"`. Set unused values to 0. A series element with only
one non-zero value is folded into the cell's material, so give at least two
to get the series circuit.

`add_lumped_rlc` is **not** a port. It embeds a circuit element in the FDTD
update, but it is not a reflection-referenced port: it fixes no reference
impedance, so it gives no Z0-referenced S11. Read on its own, a one-cell
element reports only its near-field self-interaction. To measure what a load
does to a line, drive with `add_port(..., impedance=Z0)` and place the
`add_lumped_rlc` element **a cell or more away from the port cell**, as in the
example above. An element on the port cell sits in parallel with the source,
and S11 does not see it.

For gradients with respect to R, L or C, pass
`forward(rlc_values_override={index: {"R": R, "L": L, "C": C}})` on the
uniform single-device path. That changes the element; it still does not make
it a port.

### Microstrip ports

```text
add_msl_port(position, *, width, height, direction="+x", impedance=50.0,
             waveform=None, excite=True, n_probe_offset=None,
             n_probe_spacing=None, n_probes=5, name=None, mode="laplace",
             eps_r_sub=None, terminates=None) -> Simulation
```

The port covers the full trace cross-section and fits the line's β and Z0 from
a short probe array downstream of the feed.

- `position = (feed, centre, z_substrate_bottom)`: the coordinate along
  `direction` is the feed plane; the other in-plane coordinate is the trace
  centre.
- `direction`: `"+x"`, `"-x"`, `"+y"` or `"-y"`. The substrate normal is always
  z.
- `mode`: `"laplace"` (default, quasi-TEM). `"eigenmode"` is x-only and
  outside the documented scope.
- `n_probe_offset`, `n_probe_spacing`: in cells. When left at `None` on a
  graded runway or auto mesh, rfx recounts them in the cells under the port at
  run time.

```python
from rfx import Box

h, w = 0.3e-3, 0.6e-3                     # 300 um substrate, 600 um trace
msl = Simulation(freq_max=10e9, domain=(0.020, 0.008, 0.003), dx=100e-6,
                 boundary="cpml", cpml_layers=8)
msl.add_material("ro4350b", eps_r=3.66)
msl.add(Box((0.0, 0.0, 0.0), (0.020, 0.008, h)), material="ro4350b")
msl.add_thin_conductor(Box((0.0, 0.0, 0.0), (0.020, 0.008, 0.0)))            # ground
msl.add_thin_conductor(Box((0.0, 0.004 - w / 2, h), (0.020, 0.004 + w / 2, h)))
msl.add_msl_port((0.003, 0.004, 0.0), width=w, height=h, direction="+x")
msl.add_msl_port((0.017, 0.004, 0.0), width=w, height=h, direction="-x",
                 excite=False)
print(msl.s_matrix_lane())                # compute_msl_s_matrix
```

A cell of 100 µm puts 3 cells across the 300 µm substrate, which is coarse
for speed: preflight warns below 4. For microstrip, the substrate thickness
sets the cell size, not the wavelength.

### Waveguide ports

```text
add_waveguide_port(x_position, *, x_range=None, y_range=None, z_range=None,
                   mode=(1, 0), mode_type="TE", direction="+x", freqs=None,
                   n_freqs=50, f0=None, bandwidth=0.5, amplitude=1.0,
                   probe_offset=10, ref_offset=3, calibration_preset=None,
                   reference_plane=None, probe_plane=None, name=None,
                   n_modes=1, waveform="modulated_gaussian",
                   mode_profile="discrete") -> Simulation
```

`x_position` is the port plane's coordinate along the axis named by
`direction` (for `direction="-y"` it is a y coordinate). The two `*_range`
arguments across that axis give the guide aperture. An aperture that spans
the whole cross-section needs PEC on the transverse faces and an absorber on
the port axis only (`boundary={"x": "cpml", "y": "pec", "z": "pec"}` for an
x-directed port); on a uniform mesh `boundary="cpml"` on every face is refused. Waveguide ports
need `mode="3d"`, and cannot share a simulation with lumped ports.

```python
wg = Simulation(freq_max=12e9, domain=(0.10, 0.023, 0.010), dx=1e-3,
                boundary={"x": "cpml", "y": "pec", "z": "pec"}, cpml_layers=8)
band = np.linspace(8.2e9, 12.4e9, 11)
wg.add_waveguide_port(0.01, y_range=(0.0, 0.023), z_range=(0.0, 0.010),
                      direction="+x", freqs=band, name="p1")
wg.add_waveguide_port(0.09, y_range=(0.0, 0.023), z_range=(0.0, 0.010),
                      direction="-x", freqs=band, name="p2")
print(wg.s_matrix_lane())                 # compute_waveguide_s_matrix
```

The [Waveguide Ports guide](https://remilab.ai/rfx/guide/waveguide-ports/) covers calibration
planes and `normalize`.

### Coaxial ports

```text
add_coaxial_port(position, face="top", *, pin_length=0.005,
                 pin_radius=0.000635, outer_radius=0.002055, impedance=50.0,
                 waveform=None, terminates=None) -> Simulation
```

The coaxial calculators build their own line, TEM source, probe planes and
termination. The port supplies only the x/y centre, `face`, the two radii and
the waveform. See the complete example below.

## S-parameter calculators

| Calculator | Signature (key arguments) | Returns |
|---|---|---|
| lumped/wire | `run(..., compute_s_params=True, s_param_freqs=None, s_param_n_steps=None)` | `Result.s_params`, `Result.freqs` |
| lumped/wire, differentiable | `forward(..., port_s11_freqs=freqs)` | `ForwardResult.s_params` (S11 vectors) |
| microstrip | `compute_msl_s_matrix(*, n_steps=None, num_periods=40.0, freqs=None, n_freqs=100, enforce_passivity=False, eps_override=None, ...)` | `MSLSMatrixResult` |
| waveguide | `compute_waveguide_s_matrix(*, n_steps=None, num_periods=20.0, normalize=False, eps_override=None, sigma_override=None, strict_passivity=False, ...)` | `WaveguideSMatrixResult` |
| coax, one-port | `compute_coaxial_line_reflection(*, termination="short", n_steps=6000, freqs=None, n_freqs=11, cpml_axes="xyz", probe_count=12, probe_start_cells=8, probe_spacing_cells=4, feed_impedance=None, dut_impedance=None, eps_scale=None, ...)` | `CoaxialLineReflectionResult` |
| coax, two-port | `compute_coaxial_two_port(*, n_steps=6000, freqs=None, n_freqs=11, cpml_axes="xyz", probe_count=12, feed_impedance=None, eps_scale=None, ...)` | `CoaxialTwoPortResult` |

Result fields are listed on [Results & Observables](https://remilab.ai/rfx/api/results/#s-parameter-results).
`run(compute_s_params=True)` handles lumped and wire ports only. It does not
dispatch to the other families.

**Let rfx choose.** `sim.compute_s_matrix(lane=None, **kwargs)` reads the
registered ports, calls the matching calculator and forwards your keyword
arguments unchanged. `sim.s_matrix_lane()` returns that choice as a string and
runs nothing. Two cases need you to decide:

- One coaxial port is ambiguous. Pass
  `lane="compute_coaxial_line_reflection"` or `lane="compute_coaxial_two_port"`.
- Lumped/wire-only models raise, naming `run(compute_s_params=True)`, because
  that call needs your `n_steps`.

**Experimental cross-family calculators.** `compute_mixed_s_matrix()` (lumped
or wire plus microstrip) and `compute_coax_msl_transition()` (coax plus
microstrip) are diagnostics. Their magnitudes are not validated, and the
coax-to-microstrip matrix is not passive, so by default the call raises
`ValueError`. Measure each side with its own calculator instead.

### Checking a calculator before the run

```text
preflight_sparameters(*, calculator="run", strict=False, normalize=None,
                      include_general=False) -> PreflightReport
```

`calculator` is `"run"`, `"forward"`, `"msl"` or `"waveguide"`. It checks that
the registered ports suit that calculator, without running FDTD. Read
`report.ok`. `strict=True` raises on errors only.

```python
print(msl.preflight_sparameters(calculator="msl").ok)
report = wg.preflight_sparameters(calculator="waveguide")
print(report.ok, [issue.code for issue in report])
```

For waveguides it also audits the setup:

- `record_shorter_than_far_boundary_round_trip` (error): the record is shorter
  than one round trip to the far boundary. Raise `num_periods`.
  `record_far_boundary_band_below_cutoff` means the lowest frequency is below
  the port's cutoff, so the check could not be made.
- `port_index_mirror_asymmetry`: an opposite-facing port pair is not placed
  symmetrically. A healthy two-port guide always shows the informational
  `port_index_mirror_known_e_plane_offset` note, so a non-empty report is not a
  failure.
- `layout_measured_from_band_low_edge` (information): the band's lowest bin is
  within 6 % of cutoff. Size the layout from below the band.

The coaxial calculators check their own requirements when called.

## Complete coaxial example

`compute_coaxial_line_reflection()` needs a float32, 3-D, second-order uniform
Yee grid with CPML on all six faces (positive thickness on both z faces),
no periodic axis, and exactly one `face="top"` coaxial port. The line absorbs
on all three axes (`cpml_axes="xyz"`, the only accepted value); its outer wall
must stay more than one cell from every lateral absorber.
Nothing else may be registered: it rejects extra geometry, sheets, RLC
elements, probes, monitors and NTFF boxes rather than ignoring them.

```python
coax = Simulation(freq_max=40e9, domain=(0.008, 0.008, 0.040), dx=0.35e-3,
                  boundary="cpml", cpml_layers=16)
coax.add_coaxial_port(
    (0.004, 0.004, 0.020),          # x, y are used; z is derived internally
    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",
    probe_count=12,
    n_steps=4000,
    freqs=np.linspace(4e9, 12e9, 5),
)
print(refl.status, f"{refl.annulus_cells:.2f} annulus cells")
print(np.abs(refl.s11))
```

- `termination`: `"short"` or `"matched"`. With `"matched"`,
  `dut_impedance=R` makes the load a resistor R. Don't pass `dut_impedance`
  otherwise.
- `feed_impedance`: the feed resistor. `None` uses the line's analytic TEM
  impedance.
- `probe_count` must be at least 3, and every probe plane must fit between the
  load and the source; otherwise the call raises before running. The probe
  ladder is placed in cells, so refining the mesh shortens the span it covers.
  Give it enough physical length.
- `dx = 0.35 mm` puts about 4 cells across the 1.42 mm annulus. Keep at least
  4. `status` reads `"under_resolved"` below about 3.5 and `"contaminated"`
  when the field is not a clean single TEM mode at some frequency. Check that it reads `"passed"`.
- The result does not pass through the shared passivity check. Inspect
  `status`, `recurrence_residual` and `s11` yourself.

## Limits

- **Resolution.** Lumped and wire ports: about 60 cells per wavelength at the
  highest frequency for one-port S11. At 60 cells per wavelength, `|S11|` on
  resistive loads is within 0.54 dB of the closed form, and exact 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 (the
  lumped/wire chain battery, measured through `forward(port_s11_freqs=...)`).
  Coax: at least 4 cells across the annulus. Microstrip: at least 4 cells
  through the substrate (preflight warns below that).
- **Port reference plane.** A one-cell lumped or wire port reads V at its node
  and I half a cell toward the line. Lumped and wire ports return identical
  S11.
- **End lines at their ports, not in the CPML.** CPML terminates a guided
  line much less well than a plane wave. Let the ports terminate the line.
- **Coax results from before the three-axis absorber** (`cpml_axes="z"`, now
  refused) came from a line in a closed metal can and do not reproduce.
- **Graded meshes.** Lumped-port S-parameters and coaxial ports raise on a
  graded mesh. Microstrip and waveguide calculators run there but are
  experimental. See [Support Boundaries](https://remilab.ai/rfx/support/).
- **Microstrip bins.** Exclude every frequency bin where any entry of
  `MSLSMatrixResult.reliable` is `False`: S is solved jointly across drives, so
  the whole slice `S[:, :, k]` is affected. A `True` entry does not guarantee
  accuracy.
- **Sources, probes and monitors are not ports.** They have no reference
  impedance, so they cannot produce S-parameters.
