rfx
rfx — JAX-based RF FDTD electromagnetic simulator.
One finding, with named SI scalars or explanatory strings.
Values are copied and made read-only. Path is absent for checks performed before execution dispatch. The record contains no solver or array objects.
Rectilinear FDTD grid with uniform cell size.
Supports 3D and 2D (TMz/TEz) modes. In 2D mode the grid has
nz=1 and the Courant factor uses √2 instead of √3. The
existing 3D update equations naturally reduce to 2D because
z-derivatives vanish when the z-axis is periodic with one cell.
Parameters
freq_max : float
Maximum simulation frequency (Hz). Used for auto-resolution.
domain : tuple[float, float, float]
Physical domain size (Lx, Ly, Lz) in meters.
For 2D modes, Lz is ignored (set nz=1 internally).
dx : float | None
Cell size override. If None, auto-computed as λ_min / 20.
cpml_layers : int
Number of CPML absorbing layers on each face.
cpml_axes : str
Axes that receive CPML padding. Default "xyz".
mode : str
"3d" (default), "2d_tmz" (Ez, Hx, Hy), or
"2d_tez" (Hz, Ex, Ey).
Courant-stable timestep for 2D or 3D FDTD.
Estimate timesteps for given number of periods at freq_max.
Convert physical position to grid index (accounting for the
leading per-face CPML offset pad_{axis}_lo).
Raises
ValueError
If pos maps outside the grid. The pre-guard code
returned an out-of-range index silently, which then indexed
the wrong cell (or wrapped negatively) downstream.
Map a cell/edge extent without wrapping its exclusive high endpoint.
A constant coordinate denotes a plane and uses the point lookup. Non-periodic axes retain position_to_index's input-dtype division.
PRIMAL cell widths along axis, float64, full padded length.
Constant by construction: np.full(n, dx). The last entry is the
fence-post node-provider (see rfx/_grid_metric.py), so the sum
overshoots the realized extent by one cell -- use node_of.
DUAL (E-node) spacings along axis, float64.
On a constant axis dual[k] = (d[k-1]+d[k])/2 = d[k] exactly, and
dual[0] = d[0] by the same rule, so this equals cells bit for
bit. Spelled through the shared helper anyway so the two classes
cannot drift apart in how the rule is written.
Padded index of the node nearest physical coordinate x.
Divides in float64 before applying the shared lower-node tie rule.
position_to_index retains its input scalar's division dtype.
Declared periodic axes wrap modulo their node count. In 2-D
mode the z axis holds one cell and the answer is always 0, which is
what position_to_index returns there.
Physical coordinate of the E node at padded index i.
(i - pad_lo) * dx -- the closed form _uniform_axis_nodes
(rfx.geometry.rasterize_grid._uniform_axis_nodes) evaluates for the whole
axis, so this equals coords_from_uniform_grid(grid).<axis>[i] bit
for bit, and equals the bare i * dx on an axis with no pad.
The cell width at the "lo" or "hi" face of axis.
The number a CPML profile is entitled to calibrate against. Constant
here, so both sides give dx.
Whether every cell on axis has the same width.
Always True on this class. Decision 4 selects the uniform kernel
on this predicate rather than on "was a profile given", so that the
kernel and the metric have one source.
Whether axis's cell widths are a JAX tracer (decision 6).
Always False here: a uniform Grid holds a Python float.
Non-uniform grid with per-axis cell-size arrays.
dx / dy are the BOUNDARY cell sizes (used by CPML and any
legacy code that reads a scalar spacing); dx_arr / dy_arr /
dz hold the per-cell spacings. In the uniform-xy case,
dx_arr is jnp.full(nx, dx) and dy_arr is analogous.
pad_{axis}_{lo|hi} mirror rfx.grid.Grid per-face padding —
a face whose token is pmc/pec/periodic gets 0 cells on
that side even when the axis as a whole uses CPML. For back-compat
the six fields default to cpml_layers on every face (the
pre-per-face-allocation symmetric layout).
Create new instance of NonUniformGrid(nx, ny, nz, dx, dy, dx_arr, dy_arr, dz, dt, cpml_layers, inv_dx, inv_dy, inv_dz, inv_dx_h, inv_dy_h, inv_dz_h, pad_x_lo, pad_x_hi, pad_y_lo, pad_y_hi, pad_z_lo, pad_z_hi, dx_arr_f64, dy_arr_f64, dz_f64, face_layers)
Leading (lo) per-axis pad — the number coordinate-offset
callers subtract from array indices to recover user coords.
Whether axis's cell widths are a JAX tracer (decision 6).
True on the mesh-as-design-variable path, where the profile is a
trace leaf and has no host copy. A traced axis still steps the solver
and still differentiates; what it cannot do is answer a host-side
structural question, which is why index_of refuses on it.
PRIMAL cell widths along axis, full padded length n.
float64 on the host for a concrete axis -- the float64 spine, which is the source of truth (decision 1), NOT the float32 solver store. A traced axis returns its tracer unchanged, in jnp.
The last entry is the bounding-node duplicate (#562): it supplies the
N+1-th E node and carries no physical extent, so sum(cells)
overshoots the realized wall-to-wall extent by one cell. Read extents
from node_of or interior_cells.
A spine whose length disagrees with the axis is REFUSED rather than returned: the spine and the float32 store are two views of one padded profile, so a length mismatch means they are not, and every node derived from it would sit at the wrong index. A MISSING spine is a different case, handled below.
A grid built by hand (the NonUniformGrid(...) constructor called
directly, rather than make_nonuniform_grid) may carry no spine.
It then gets the float32 store widened back to float64 AND an
ExactNodeSpineMissingWarning, the same degraded line and the same
warning coords_from_nonuniform_grid emits -- silently falling
back is the #802 realization with no trace of why.
DUAL (E-node) spacings along axis.
dual[0] = d[0], dual[k] = (d[k-1]+d[k])/2 -- the control
volume an E node's material acts over, and the metric the NU E update
divides its curl by. float64 for a concrete axis; a traced axis goes
through e_node_dual_spacings so the gradient survives.
Reproduces e_node_dual_spacing_at(grid.cells(axis), k) for every
k bit for bit. Against the float32 solver store the two spellings
agree only to float32 roundoff -- that gap is the store's, not the
rule's, and it is why the accessor reads the spine.
Padded index of the node nearest physical coordinate x.
Refuses on a traced axis (decision 6, closes G9). The existing
position_to_index answers there by substituting a NOMINAL uniform
mesh of fallback_dx cells and returns an index for a mesh the
solver is not running -- silently the wrong cell whenever the traced
profile is graded. That fallback stays in position_to_index for
0a because consumers still call it; 0b retires it as each consumer
moves onto this accessor.
Which node (#1295, #1342): the nearest one by float64 distance, and
at a tie (two distances agreeing to 1e-9 of the local cell) the
LOWER one -- nearest_node_index, the function the sheet snap
itself calls, on the node line the conductors are rasterized on
(_axis_node_positions of the float64 spine: the closed form
(i - pad_lo) * cell on a constant axis, the cumulative sum on a
graded one). A port, source or probe and a sheet declared at the
same coordinate therefore land on the same node, bit for bit.
position_to_index and pos_to_nu_index resolve through the
same function. The uniform Grid still rounds an exact tie to the
even node until #1342.
A coordinate outside the interior span is REFUSED, as on the uniform
grid. _axis_position_to_index clamps instead: argmin over the
edge list always returns some index, so asking for a position a metre
outside a 6 mm domain silently gives the last face and the caller
stamps its material there. That legacy clamp stays where it is for
0a -- consumers still call position_to_index -- and 0b retires it
as each consumer moves onto this accessor.
The bound is the interior, [node_of(pad_lo), node_of(last interior
node)], because that is the range the edge list can represent; a
coordinate inside an absorber pad has no interior node to name. The
comparison carries DECLARED_SPAN_TOL_M (1e-12 m) of slack at
each end. That is not a fudge factor: it is the node tolerance the
mesh builder itself claims. make_band_profile guarantees that
every declared interface lands on a cumulative node coordinate to
1e-12 m and that the cells sum to the declared span to the same
figure, and assert_cells_span_declared_profile holds the
constructor to it. A grid that only places a node to 1e-12 m cannot
tell a coordinate 1e-13 m past the face from the face, so refusing
one would be refusing a distinction the grid does not make. Outside
that band the refusal is exact.
That figure grows with the column: the builder documents 1e-11 m on a 10 m column of 1e5 cells. On a board or a waveguide, the sizes this lane meshes, 1e-12 m is the right order. A caller meshing metres should read the builder's guarantee before trusting the boundary case.
Physical coordinate of the E node at padded index i.
Origin at the first interior node, so node_of(axis, pad_lo) ==
0.0. Derived from the cells by cumulative sum, except that a
CONSTANT axis takes the closed form (i - pad_lo) * d[0] -- which
is what keeps a uniform-valued NU axis bit-identical to the uniform
lane's own node line (#807). Both branches are
_axis_node_positions, so this equals
coords_from_nonuniform_grid(grid).<axis>[i] bit for bit.
A traced axis returns a traced scalar (jnp cumulative sum), the same
arithmetic coords_from_nonuniform_grid runs in-trace.
Cost: this builds the whole axis to return one entry, because reusing
the spine producer is what makes the answer bit-identical to the
coordinate arrays. Nothing calls it in a loop in 0a. A consumer in 0b
that needs many nodes asks coords_from_nonuniform_grid once, or
this grows a cached array -- not a second spelling of the arithmetic.
The cell width at the "lo" or "hi" face of axis.
The number a CPML profile is entitled to calibrate against, per face.
make_nonuniform_grid pins the first and last interior cells of x
and y to the scalar dx / dy and pads each face with copies of
them, so on a built grid both sides equal that scalar; z has no
scalar and this is the only way to ask.
cpml.py fills its six per-face slots from this accessor.
Whether every cell on axis has the same width.
The predicate decision 4 selects the uniform kernel on, instead of
"was a profile given" (rfx/api/_mesh.py:152-156).
A TRACED axis returns False. Constancy is not decidable on the
host there, and False is the safe direction: it routes the axis
to the non-uniform kernel, which is correct for a constant mesh too,
whereas True would hand a possibly graded mesh to a kernel that
folds one scalar.
Create a non-uniform Yee grid.
Parameters
domain_xy : (Lx, Ly) in metres
Only used when dx_profile / dy_profile are None — in
that case the xy mesh is uniform with spacing dx.
dz_profile : 1D array of z cell sizes in metres (physical domain only)
dx : float
Boundary cell size (also used for CPML padding and as the
uniform-xy spacing when no xy profile is provided).
cpml_layers : int
Number of CPML cells added to each face (when that face is
absorbing — see pec_faces / pmc_faces).
dx_profile, dy_profile : 1D arrays or None
Optional per-cell x / y spacings for the physical (non-CPML)
interior. When provided, the first and last values must match
dx (they set the boundary cell size used by the CPML).
pec_faces, pmc_faces : set of str or None
Face labels (x_lo, x_hi, y_lo, y_hi, z_lo,
z_hi) where the boundary is PEC / PMC. Per-face pad count
is forced to 0 on these faces so the reflector plane aligns
with the user domain edge. Added 2026-04 to close the
PMC+CPML composition gap on the non-uniform mesh path.
cpml_axes : str
Axes that participate in CPML allocation (default "xyz").
Per-face allocation still applies; an axis absent from
cpml_axes gets pad=0 on both faces.
dt : float or None
Concrete time step (s) to use INSTEAD of the Courant step this
builder derives from min(cell). None (default) keeps the
derived step and is byte-identical to the historical behaviour.
Why it exists: the derived step is a function of the smallest
cell, so a mesh that is a design variable moves ``dt`` with the
deformation, and every observable read off the run then carries
the step change as well as the geometry change. Pinning one step
across a deformation family makes the sweep one antenna at one
step. The value must be CONCRETE — a traced ``dt`` would put the
step itself in the derivative.
dt_min_cell : float or None
Smallest cell size (m) the caller's TRACED axes reach anywhere in
the family they will run. Required with dt when any profile is
a tracer, because a tracer carries no host value to measure the
realized Courant limit against. Concrete axes are always measured
from their realized profile; this declared number is used only for
the axes that cannot be.
Build a 1-D cell-size profile whose interfaces land on node planes
and whose adjacent-cell ratios obey max_ratio everywhere.
Axis-agnostic: the same output serves dx_profile, dy_profile
or dz_profile of make_nonuniform_grid().
Parameters
edges : sequence of float
Sorted physical coordinates (m) of every interface that must lie
on a node plane, INCLUDING the domain start and end.
cell_sizes : sequence of float
Target cell size (m) per segment, len(edges) - 1 entries.
max_ratio : float
Cap on every adjacent-cell ratio (default 1.4 — the validated
z-axis multi-band cap; pass 1.3 for an in-plane profile, the
in-plane threshold, see docs/guides/support_matrix.md). Must be
finite and >= 1.0001 (a 2x transition at that ratio already
needs 6932 cells; the plateau solve's cost grows with the ramp
length — measured 0.03 s at 1.0001 on a 40-cell profile and
0.4 s for a 1 um pin descending into 10 mm of 100 um cells, an
8110-cell profile).
protected : sequence of bool, optional
Per segment. A protected segment is realized UNIFORM —
n = max(min_cells, ceil(span / target - 1e-9)) equal cells —
and never hosts a ramp cell; a free segment (default) may host the
ramps its neighbours need.
boundary_cell : float, optional
Pins cells[0] == cells[-1] == boundary_cell exactly (the x/y
CPML contract of make_nonuniform_grid()). Both end segments
must be free and wide enough to hold the pinned cell plus its ramp
(ValueError otherwise).
min_cells : int
Minimum cell count of a PROTECTED segment (default 1).
Returns
np.ndarray
float64 cells. Invariants, for any valid input: every edge is a
cumulative node coordinate to 1e-12 m; every adjacent ratio is
<= max_ratio (to 1e-9), INCLUDING seams between two protected
segments; the sum equals edges[-1] - edges[0] to 1e-12 m;
with boundary_cell, both end cells equal it bit-exactly.
The two 1e-12 m figures are float64 accumulation of the cells
(a running sum, as the rasterizer positions nodes) and hold for
columns up to about 1 m with up to 1e4 cells; the miss grows
with column length and cell count (measured 1e-11 m on a 10 m
column of 1e5 cells, 2.2e-9 m on 100 m / 1e6 cells), while each
cell itself matches its declared value to 1e-15 relative.
Raises
ValueError
On inconsistent input: edges not strictly increasing or two edges
closer than 1e-12 m (the node tolerance — merge them), a
non-positive cell size, max_ratio not finite or below 1.0001,
a protected end segment with boundary_cell, an end segment
shorter than the pinned cell or too short to hold the pin plus
its ramp, and a profile that would exceed 1,000,000 cells (a
sliver segment pulls its neighbours down to its size under the
ratio law: a 1e-9 m protected sliver between 1 mm blocks asks
for 1.4e6 cells).
Notes
Ramps are geometric (per-step ratio spread evenly, <= max_ratio)
and placed INSIDE the coarser free segment on whichever side needs
one; the free run's plateau value is solved so the run sums to its
declared span exactly (no cell above its target, no rescale). Two
adjacent PROTECTED segments whose seam exceeds the cap cannot be
served by a ramp: the coarser one's uniform cell is reduced (more
cells) until the seam complies, iterated to a fixed point. The cost
is cells AND, whenever the refined block was the coarsest declared
cell, a smaller minimum cell than declared — the time step follows
the global minimum, so dt drops with it. Measured on 3000 seeded
stacks (rng 1; 2-6 segments, spans 20 um-3 mm, targets 0.05-2x the
span, 70 % protected, caps 1.2-1.4): the realized minimum sits below
the declared minimum in 410 of them, down to 0.31x in the worst case
(two single-cell protected blocks whose span ratio just exceeds the
cap at 1.2). Hand case: [0, 0.1, 0.1355] mm, [25, 100] um, both
protected, 1.4 realizes 5 x 20 um + 2 x 17.75 um, minimum 0.71x
the declared 25 um. A free segment too thin for any ramp is a
refinement source for its neighbours in the same way.
Declarative FDTD simulation builder.
Parameters
freq_max : float
Maximum simulation frequency (Hz).
domain : (Lx, Ly, Lz) in metres
Physical domain size. For 2D modes Lz is ignored.
boundary : "pec", "cpml", or "upml"
Boundary condition. Default "cpml".
cpml_layers : int
Number of CPML layers per face. Default 16 (ignored for "pec").
dx : float or None
Cell size override (metres). Auto-computed if None.
mode : str
"3d" (default), "2d_tmz" (Ez, Hx, Hy), or
"2d_tez" (Hz, Ex, Ey).
snap : {"strict", "declared"}
Strict (default) refuses PEC sheets solved more than 1% off their
drawn in-plane size. "declared" accepts the difference as a warning
and records the choice in realized geometry; no solved numbers change.
dt : float or None
Concrete time step (s) for the NON-UNIFORM lane, used instead of
the Courant step derived from the smallest cell. None (the
default) keeps the derived step and is byte-identical to before;
passing it without any of dz_profile / dx_profile /
dy_profile is refused, because the uniform lane derives its own
step in Grid.courant_dt.
Why it exists: the derived step is a function of ``min(cell)``, so a
mesh that is a design variable moves ``dt`` as it deforms and every
observable read off the run carries the step change alongside the
geometry change. One pinned step makes a deformation sweep one board
at one step. A step above the Courant limit of the REALIZED cells is
refused, never clipped.
dt_min_cell : float or None
Smallest cell size (m) the caller's TRACED axes reach anywhere in
the family they will run. Required with dt when a profile is a
JAX tracer, because a tracer carries no host cell size for the
limit to be measured against at build time. Concrete axes are always
measured from their realized profile; a traced axis is measured
against this declared floor when the run builds its mesh, and the
run is refused if the realized cells fall below it.
precision : str
"float32" (default), "mixed", or "float64". When
"mixed", field arrays (E, H) use float16 for ~2x memory
reduction while material coefficients and DFT accumulators stay
float32. When "float64", field arrays use float64 storage
AND the Yee update arithmetic runs at float64 (issue #630 — prior
releases hard-pinned the Yee curl/update arithmetic to float32
regardless of storage dtype, so float64 fields were silently
re-quantized to float32 every timestep; the arithmetic dtype is
now jnp.promote_types(field_dtype, jnp.float32), so
"float32"/"mixed" are numerically unaffected and
byte-identical to before). "float64" requires JAX's x64 mode
to already be enabled by the caller (jax.config.update(
"jax_enable_x64", True) or jax.experimental.enable_x64()) —
without it JAX silently downcasts float64 arrays back to
float32, and preflight() warns if this mismatch is detected.
Material coefficients (eps_r/sigma/mu_r) and
precomputed CPML profile arrays stay float32 either way (fixed
setup-time constants measurably bias the primal but do not
create the per-timestep rounding-lattice noise floor that the
compute-dtype pin did — see issue #630's f64-lift measurement).
"mixed"/"float64" are UNIFORM-SINGLE-DEVICE-LANE ONLY
today: field_dtype is threaded only by
rfx/runners/uniform.py — the non-uniform-mesh, distributed
(devices=/distributed=True), and subgridded
(refinement) runners do not thread it, so a non-float32
precision there would silently run float32 fields. run() /
forward() raise NotImplementedError rather than do that
silently; preflight() also warns in advance for the
non-uniform-mesh case (the distributed case is a call-time
run()/forward() kwarg, invisible to preflight, so its
NotImplementedError at dispatch is the only enforcement point).
ACCURACY CAVEAT for ``"mixed"`` with a CPML boundary (issue #644):
float16 field storage is fine for transient and field-shape work
(measured 0.20% relative L2 error on Ez vs ``"float32"`` at 50
steps), but it raises the absorber's residual floor. On the
fixture in ``tests/unit/grid/test_mixed_precision.py`` — a 40**3 domain,
dx = 3.0 mm, point source, 400 steps — total field energy settles
to about -76 dB below peak under ``"float32"`` but only about
-59.5 dB under ``"mixed"``: roughly 17 dB worse, consistent with
float16's ~1e-3 machine epsilon quantizing the stored fields.
Those two numbers are from that one fixture, not a universal
bound; the floor scales with float16 epsilon, so expect the same
order wherever the field storage is float16. Practical rule:
``"mixed"`` is not a drop-in ``"float32"`` substitute when the
quantity of interest is a reflection coefficient, an S-parameter,
or anything else living near the absorber floor — use
``"float32"`` (the default) for those.
solver : str
"yee" (default) for the standard explicit scheme or
"adi" for the experimental ADI-FDTD path. The homogeneous,
lossless split with compatible domain boundaries removes the
explicit CFL stability restriction. Interior PEC (sheets, wires,
volumes) is refused at every factor on both lanes: its projection
does not inherit that guarantee. Cavity accuracy without interior
PEC is tested in 2D TMz (mode="2d_tmz", 2% resonance gate at
5x CFL, test_adi_cavity_resonance) and 3D
(full Zheng–Chen–Zhang two-sub-step scheme since
2026-07-13, issue #338: 2% PEC-cavity eigenfrequency gate at 2x
CFL, ~15 cells/wavelength). 3D dispersion error grows ~dt^2, so
preflight emits an adi_3d_accuracy envelope advisory when
adi_cfl_factor > 2 on a 3D grid — large factors trade
wavelength-scale accuracy for stiff-mesh throughput.
adi_cfl_factor : float
Timestep multiplier relative to the grid's Yee CFL timestep when
solver="adi". Default 2.0; for quantitative wavelength-scale
3D results prefer <= 2.0 (see solver).
stencil_order : int
Spatial finite-difference order for the explicit Yee update: 2
(default, the standard (2,2) scheme — BYTE-IDENTICAL to prior
releases) or 4 (the (2,4) fourth-order-in-space stencil). Order
4 is supported ONLY on the plain uniform Cartesian vacuum/dielectric
path: boundary pec/periodic (NOT cpml/upml), default yee
solver (NOT adi), uniform mesh (no dz/dx/dy profile), no dispersive
materials, no anisotropic/subpixel eps, no conformal PEC, no Kerr,
not distributed. Any unsupported combination with
stencil_order=4 raises NotImplementedError. Order 4 derates
the timestep by ~0.857x for stability (the (2,4) CFL bound).
Add a z-axis refinement region for SBP-SAT subgridding.
Refinement acts only through run() on a uniform mesh. Other
solve entry points, including forward() and optimization, refuse it.
The experimental runner covers the specified z-range across the
full x/y interior at dx_fine = dx_coarse / ratio. xy_margin
enables an experimental research-only local x/y window whose fine
region is inset from the physical x/y boundaries by that distance.
Parameters
z_range : (z_lo, z_hi) in metres
Physical z-range for the fine region.
ratio : int
Refinement ratio (fine cells per coarse cell). Default 4.
xy_margin : float or None
Experimental x/y inset in metres. None keeps the full x/y
interior. A finite non-negative value creates a local window
spanning [xy_margin, Lx - xy_margin] and
[xy_margin, Ly - xy_margin]. Production validation still
rejects this lane until waveform gates and crossval pass.
tau : float
SAT penalty coefficient (default 0.5). Higher values give
stronger coupling but more dissipation.
validation : {"production", "research", "off"}
Production refuses the unstable, unverified subgridded lane before
stepping (#1465). An empty PEC cavity's first resonance is 8 % high
and its field grows. Use dx/dy/dz profiles for local resolution.
Explicit "research" or "off" runs with a warning; neither
mode establishes physics support.
topology : {"overlap_z_slab", "stage2_disjoint_3d"}
Internal topology selector. "overlap_z_slab" is the current
public runner. "stage2_disjoint_3d" records the selected
centered/two-interface integration lane but remains a research-only
contract until its public runner wiring and waveform gates pass.
Return the production-envelope validation report for subgridding.
Every mode reports the subgridded lane as unsupported because it is unstable and unverified (#1465). Experimental envelope checks remain available for research diagnostics; opting in does not confer support.
Register a named material.
Parameters
chi3 : float Third-order (Kerr) susceptibility in m^2/V^2. When non-zero, an ADE correction is applied after each E-field update inside the scan body.
Add a geometric shape filled with a named material.
Parameters
shape : Shape material : str Registered or library material name.
Notes
A PEC shape is a VOLUME under the lattice ownership contract
(#931): its primal cells are sampled at cell centres and every E
edge incident to an occupied cell is shorted, so a Box drawn
z_a -> z_b on node planes realizes walls at BOTH planes with
realized thickness = drawn thickness. A PEC Box with exactly one
zero-extent axis (lo == hi) is a SHEET declaration, realized
exactly as add_thin_conductor() would realize it. A PEC Box
thinner than one local cell (but not zero) is refused at
assembly; so is any PEC shape that rasterizes to zero cells.
There is no per-entry realization knob.
Return the immutable host record of this configuration's built geometry.
Build only: entities, solved sheet spans, signed face residuals in
metres, domain padding, and driven port edges. Dense diagnostic arrays
are built on request here. run() attaches a compact record built
independently from its own assembly as Result.realized_geometry.
Input-fidelity audit: declared vs solved, per entity, in input
units — run BEFORE any solve. See rfx.fidelity.fidelity_report()
(design rule: never predicts result-side impact; rasterization,
realization class and materialization only).
Add a soft point source (no impedance loading).
Unlike add_port(), this does NOT add a resistive load.
Ideal for resonance characterization where port loading would
damp the cavity response.
Uses a differentiated GaussianPulse by default (near-zero DC
content, ~1e-4). This prevents static charge accumulation on PEC
surfaces.
Parameters
position : (x, y, z) in metres
component : "ex", "ey", or "ez"
waveform : excitation pulse
(default: GaussianPulse(f0=freq_max/2, bandwidth=0.8))
amplitude_kind : 'field' | 'current' | None
What the waveform amplitude MEANS — issue #571. Boundary- and
mesh-independent once explicit:
- ``'current'``: the amplitude is a current moment I(t) in A·m,
realized as ``E += Cb * I / dV`` on every path and boundary
(Yee ``Cb = (dt/eps)/(1 + sigma*dt/(2*eps))``; ADI refuses
``'current'`` and requires explicit ``'field'``,
``dV`` = local cell volume). Resolution-independent injected
power; Meep's convention; the declaration default.
- ``'field'``: the amplitude is a raw E-field increment per
step, ``E += w(t)`` on every path and boundary.
Cell-volume convention: ``dV = dx * dy * dz`` at the source
cell; a 2D grid is treated as ONE CELL DEEP (``dz`` one cell,
duck-typed to ``dx``), so ``dV = dx**3`` on a cubic 2D grid,
not the per-unit-length ``dx**2``
(``rfx/api/_source_semantics.py``). On a non-uniform mesh
``dV`` is the E node's control volume — the primal per-cell
width on the component's own axis and the DUAL spacing
``(d[k-1]+d[k])/2`` on the two transverse axes (issue #672);
the two coincide on a uniform profile.
``None`` means ``'current'`` (E += Cb*I/dV, I is a current moment in A·m)
on every path and emits one
`DeprecationWarning` per Simulation. Pass the kind
explicitly to silence it.
The open-boundary + ``subpixel_smoothing`` cross-path residual
(0.18 % amplitude, issue #582) is a solver defect independent
of this parameter.
Add a polarized point source.
Parameters
position : (x, y, z) in metres
polarization : str or tuple
- "ez", "ex", "ey" — linear single-component
- "circular" or "rhcp" — accepted complex-component shortcut;
time-domain quadrature is not independently verified
- "lhcp" — accepted complex-component shortcut; same limitation
- (Ex, Ey) tuple — normalized component weights; real tuples are
the documented linear-polarization scope
- "slant45" — 45° linear (Ex = Ey)
waveform : excitation pulse (default: GaussianPulse)
amplitude_kind : 'field' | 'current' | None
Threaded through to every internal add_source() call —
see add_source for the semantics. None resolves to
'current' and warns once per simulation; pass the kind explicitly
to silence the warning.
Add an SMA-style coaxial probe port.
Parameters
position : (x, y, z) — center of port on cavity wall (metres) face : "top", "bottom", "front", "back", "left", "right" pin_length : float — pin protrusion into cavity (default 5mm) pin_radius : float — center pin radius (default 0.635mm, SMA) outer_radius : float — outer conductor radius (default 2.055mm) impedance : float — port impedance (default 50 ohm) waveform : excitation pulse (default GaussianPulse at freq_max/2)
Add a thin conductor with subcell correction.
Parameters
shape : Shape Geometric region of the conductor. sigma_bulk : float Bulk conductivity (S/m). Default: copper (5.8e7).
**This single value decides the model.** At or above 1e6 S/m the
shape becomes a PEC sheet and ``thickness`` and ``eps_r`` are
never read (`rfx.materials.thin_conductor.apply_thin_conductor()`
returns the material arrays untouched and only ORs the mask). Below
1e6 S/m the shape becomes a lossy sheet whose conductivity is
``sigma_bulk * thickness / d_norm``, where ``thickness`` IS
load-bearing and ``d_norm`` is the local sheet-normal spacing
(see ``surface_impedance_f0`` below for the graded-mesh case).
Every real metal — copper 5.8e7, aluminium 3.5e7, even stainless
steel 1.4e6 — is on the PEC side, so by default
(``surface_impedance_f0`` unset) for metals this call models a
lossless perfect sheet and nothing else. Pass
``surface_impedance_f0`` to opt in to band-centre Leontovich
conductor loss instead (see below).
thickness : float
Physical thickness (metres). Default: 35 µm (1 oz copper).
Only used on the lossy path (sigma_bulk < 1e6); ignored
entirely for metals — see sigma_bulk above.
eps_r : float
Relative permittivity (default 1.0). Lossy path only, same as
thickness.
surface_impedance_f0 : float or None
Opt-in band-centre Leontovich surface-impedance loss
(issue #669). Default None = exact legacy behaviour on every
path. When set to a positive frequency (Hz), the sheet — for ANY
sigma_bulk > 0, metals included — is modelled as a resistive
sheet of sheet resistance Rs0 = sqrt(pi*f0*mu0/sigma_bulk)
(the thick-conductor surface resistance at f0), realized
NODE-THIN (#677): a per-step operator applies an exact
exponential-stepping resistive update E = A*E + B*curlH
with sheet conductivity sigma_sheet = (1/Rs0)/d_norm on
exactly the tangential E edges a PEC sheet on the same cells
would zero — so toggling surface_impedance_f0 on a metal
changes LOSS, never the conductor's surface position (before
#677 the sheet folded into materials.sigma as a full-cell
slab, moving resonances by geometry). d_norm is the local
spacing normal to the sheet: the cell size on a uniform grid,
and on a graded one the E-node DUAL spacing (d[k-1]+d[k])/2
(#671). Both the uniform and the non-uniform (dz_profile)
run()/S-parameter lanes apply the operator; lanes that do
not (distributed, subgridded, ADI, UPML, dispersive-overlap,
multimode-waveguide, MSL-junction/mixed, optimize/topology
drivers) refuse f0 sheets loudly rather than silently dropping
them; compute_msl_s_matrix applies the sheet through its
run()/forward() device dispatches (#679). The sheet
contributes NO PEC cells and no materials.sigma/eps_r
entries.
**Consequence for material inspection (#695): an f0 sheet is
INVISIBLE to the assembled arrays.** It is absent from
``pec_mask`` and it adds nothing to ``materials.sigma``, so a
conductor test written the obvious way --
``pec_mask | (sigma > 1e3)`` -- finds no metal at all on a
board whose traces are all f0 sheets, and a connectivity
check built on it reports a perfectly healthy model as
disconnected. Use `Simulation.conductor_mask()`, which
returns ``pec_mask | (sigma > threshold) | union(f0 sheet
cell masks)`` -- the whole conductor footprint in one
spelling, on the grid the run will actually use. The runtime
edge masks (``mask_ex/ey/ez``) live on the
``SheetImpedanceCtx`` the runners build; the accessor returns
the CELL footprint, which is what an occupancy or
connectivity check wants.
Model scope (be precise about what you get):
- **Magnitude loss only.** The reactive part ``Xs = Rs``
(internal inductance) is NOT modelled, so resonances read
slightly high in frequency.
- **Frequency-flat within a run**: ``Rs`` is frozen at ``f0``
with relative band error ``|sqrt(f/f0) - 1|`` (about +-11%
across an 8-12 GHz band centred at 10 GHz).
- **Thick-conductor model**: valid for ``thickness >= ~3``
skin depths at ``f0``. For thin films the DC path (omit this
kwarg, ``sigma_bulk < 1e6``) is the correct model;
``thickness`` deliberately does NOT enter ``sigma_eff`` in f0
mode (Leontovich loss is thickness-independent).
- **Transmission leakage** ``(2*Rs/eta0)**2`` makes the sheet
unusable for shielding-effectiveness claims beyond ~60 dB.
(Validated against the closed form ``T = 2Rs/(2Rs+eta0)``
to within a few percent, frequency-flat —
tests/oracle/test_leontovich_alpha_oracle.py.)
- The operator touches TANGENTIAL sheet edges only; the
sheet-normal E component is never loaded (#677 removed the
pre-existing isotropic-fold caveat).
- ``f0`` and ``sigma_bulk`` are differentiable DoFs in f0 mode
(closed forms ``d(sigma_sheet)/df0 = -sigma_sheet/(2*f0)``,
``d(sigma_sheet)/d(sigma_bulk) =
+sigma_sheet/(2*sigma_bulk)``). Geometry (the rasterized
mask) stays non-differentiable. Traced values skip the
concrete-value validation below.
Sheet shapes (issue #674): ANY shape implementing
``mask_on_coords`` and ``bounding_box`` may carry a
surface-impedance sheet — a patterned ground plane with
clearance holes, a meandered arm, an imported CAD outline
(`~rfx.geometry.mesh_import.MeshShape`), a disc, a
strip. The realization is per occupied cell, so a hole is
simply a cell the operator never touches. What is NOT supported
is a body with HEIGHT: the sheet must rasterize to exactly ONE
cell layer along its normal, because ``sigma_sheet`` is the
conductance of a single E node. A 3-D solid, a bent/L-shaped sheet, or a slab
thicker than a cell raises ``ValueError`` when the grid is
built — folding such a body per cell would multiply the sheet
conductance by the layer count while still reporting ``Rs0``.
A sheet that rasterizes to ZERO cells raises too: a sub-cell
mesh slab registers only where a grid node falls inside it, and
silently vaporized metal is the #369 class. (A ``Box`` drawn
with equal lo/hi on its normal axis snaps to the nearest node by
construction, so it always satisfies both.)
Validation at add time (concrete values only): ``f0 <= 0`` and
``sigma_bulk <= 0`` with ``f0`` set each raise ``ValueError``,
as does a shape with no ``mask_on_coords`` or no bounding box.
The rasterized one-layer and non-empty checks need the grid, so
they raise at build time, on BOTH lanes (the legacy DC path
also refuses non-Box shapes on a non-uniform mesh).
Notes
A PEC sheet occupying ONE cell is modelled as a surface, not as a
conductor of that cell's thickness: rfx.boundaries.pec.apply_pec_mask()
zeroes a tangential E component only where the mask has a neighbour
along that component's axis, so the normal component survives as
surface charge. Stamping the same conductor TWO cells thick therefore
changes the model rather than the thickness. Measured on an MSL thru at
dx = 84.67 µm: one cell gives Re(Z0) = 44.1 Ω and two cells 41.9 Ω.
Reproduce with
scripts/diagnostics/thin_conductor_cell_thickness_probe.py.
Note this pair no longer proves the model change on its own. Until
issue #511 was fixed the one-cell reading was 38.8 Ω, so the thicker
stamp appeared to read the HIGHER impedance — backwards for a
fringing-capacitance effect, which is how the model change was first
argued. That inversion was an extractor artifact: the modal voltage
summed one Ez edge too many, and a two-cell stamp happens to mask that
edge, so only the one-cell number was biased. With both corrected the
ordering is the ordinary one (thicker → lower Z0) and the 2.25 Ω step
is consistent with either a model change or a genuine thickness
effect. The model change is still real — it follows from
apply_pec_mask directly — but this measurement no longer
discriminates between the two.
Conductor LOSS is therefore unavailable for metals through this API. On the lanes this repo gates that is a small omission — copper loss on a 10 mm 50 Ω microstrip is 0.04-0.05 dB at 3-4.5 GHz against a 0.45 dB gate budget, and the metal is 26-36 skin depths thick so its geometry is electromagnetically a half-space — but it is a real limit for anything whose loss budget IS the conductor, e.g. resonator Q.
Declare a thin PEC conductor by NODE INDICES instead of metres.
add_thin_conductor draws metal in metres, and the grid then
decides which node lines that reaches. This draws it on the node
lines directly: the conductor occupies node plane_index along
normal_axis and the inclusive node ranges i_range /
j_range on the other two axes, in increasing axis order — for
the default normal_axis=2 that is i_range on x and
j_range on y.
Indices are INTERIOR (unpadded) node indices: interior node i
sits at i * dx from the domain origin on a uniform mesh, and is
the i-th interior node on a graded one. Absorber padding is
added when the sheet is realized.
Why it exists: the conductor's PHYSICAL size is then whatever the
cells between those nodes add up to, so stretching the cells makes
the metal longer without moving it off the lattice and without any
sub-cell metal. That is what lets jax.grad reach a patch edge
through dx_profile / dy_profile / dz_profile — a metric
sheet on a mesh that is a JAX tracer is still refused, because its
corners cannot be read off a traced node line.
The declaration is mesh-independent by construction, so the same call gives the same footprint on the uniform and non-uniform lanes, traced or not. A range spanning a single node line is refused: one node line carries no E edge, so the metal would carry no current.
Only the lossless PEC sheet is supported (sigma_bulk >= 1e6,
surface_impedance_f0 unset); thickness and eps_r are
accepted for signature compatibility with
add_thin_conductor() and are not read, exactly as they are
not read there for a metal.
Add a lumped port (single-cell) or wire port (multi-cell).
Parameters
position : (x, y, z) in metres
component : "ex", "ey", or "ez"
impedance : port impedance in ohms (default 50)
waveform : excitation pulse (default: GaussianPulse at freq_max/2).
Ignored when excite=False.
extent : float or None
When provided, the port spans from position along the port
axis by this distance (metres), creating a multi-cell WirePort.
For example component="ez", extent=0.0015 spans the port
from z to z + 0.0015.
radius : float or None
Opt-in thin-probe radius in metres (requires extent).
None preserves the legacy filament, whose effective radius
is approximately 0.20 times the transverse cell size. The
local self-field model requires a square, locally uniform
transverse mesh, uniform spacing along the pin, and radius
<= 0.2 times the transverse spacing. Supported
on single-device, nondispersive 3-D second-order Yee run/forward;
unsupported solvers and material updates raise before stepping.
excite : bool (default True)
When True the port has BOTH a resistive termination AND a
time-domain source (legacy behaviour).
When False the port is a passive matched load only — no
source, just the σ=1/(Z0·A) resistive termination.
Passive ports are required for multi-port S-parameter
extraction: excite one port at a time and probe V/I at the
others to fill off-diagonal entries of the S matrix.
direction : {"+x", "-x", "+y", "-y"} or None
Outward-normal direction of the port (from the port cell
into the external world). Used by the S-matrix post-
processing to orient the V/I → (incoming, outgoing) wave
decomposition. When None, the runner auto-detects from the
port's position (closest boundary face).
reference_plane_cells : int or None
Opt-in reference-plane port waves for the wire-port S-matrix
OFF-diagonal extraction (issue #313). When set to an integer
N >= 1, run(compute_s_params=True) registers TWO line
V/I reference planes for this port at N and 2N cells
outboard (into the DUT along the line axis, i.e. opposite
direction): gap-voltage line integrals at integer Yee
planes plus dual adjacent Ampere loops around the PEC signal
trace, accumulated in the production scan. Off-diagonal
S[i, j] entries whose ports BOTH opt in are then computed
from the plane waves — forward/backward split with the
MEASURED two-plane line impedance
Zc^2 = (V1^2 - V2^2)/(I1^2 - I2^2) (never the nominal
port impedance) and phase-only de-embedding to the port plane
with the MEASURED per-bin beta from the same two planes.
The Phase-0 closed-box flux referee showed the port-cell wave
pair does not conserve power (the port plane is near-field
dominated) while the plane waves close the power budget at
all bins; the plane path removes the drive-side |S21|
deflation kappa(f) = 1.49-1.86 of issue #313.
None (default) keeps the shipped port-cell behaviour,
byte-identical. The DIAGONAL S_jj always stays on the
byte-frozen legacy path either way, so forward() /
1-port S11 results are unaffected. Requires a wire port
(extent=) with an explicit direction transverse to
the port component, a PEC signal trace uniform across both
planes, and the uniform-mesh run() lane: the
non-uniform and subgridded lanes raise
NotImplementedError with the opt-in set, and the
distributed lane does not support compute_s_params at
all (it warns that the kwarg is unsupported and ignores
it).
Choosing N: place BOTH planes (N and 2N cells) roughly >= 10
cells from every port so the two-plane Zc/beta measurement
sits outside the port near-fields — the Phase-0
pre-registration rule. Measured on the canonical 16 mm thru
(dx = 0.5 mm, gap-trimmed V, 2026-07-10 battery): N=3 planes
read beta/(w/c) = 1.16-1.20 and Zc = 52-53 ohm with Im/Re up
to 8.2% (near-field contaminated; |S21| closed-box-referee
residual -3.1% at 7 GHz, row energy |S11|^2+|S21|^2 up to
1.019), while N=10 planes read the clean mid-line
beta/(w/c) = 1.046-1.059 and Zc = 47.9-48.6 ohm (Im/Re <=
1.2%) and closed the closed-box referee within 0.9% at all
bins.
Add a microstrip-line (MSL) port spanning the full trace cross-section.
Unlike add_port() with extent=... (a one-cell-transverse
wire port), this port covers the full width × height
cross-section under the trace and uses 3-probe numerical
de-embedding to extract β and Z0 empirically downstream of the
feed plane.
Parameters
position : (x, y, z_lo)
Physical feed-plane point. The coordinate on the PROPAGATION
axis (named by direction) is the feed plane; the
coordinate on the other in-board axis is the trace centre;
z is the substrate bottom. So a "+x" port reads it as
(x_feed, y_centre, z_lo) and a "+y" port as
(x_centre, y_feed, z_lo).
width : float
Trace width in metres, spanning the in-board axis that is NOT
the propagation axis (y for an x-directed port, x for a
y-directed one).
height : float
Substrate thickness in metres — always the z extent, because
the substrate normal is fixed to z (see direction).
direction : str
One of "+x", "-x", "+y", "-y" — the direction
the launched wave propagates. The board lies in the xy-plane
and the substrate normal is always z, so the feed may run
along either in-board axis (issue #661: a CAD-imported board
does not get to choose its orientation).
``"+z"`` / ``"-z"`` are REJECTED with a clear error rather
than supported: a z-propagating microstrip needs its
substrate normal along x or y, which ``position`` + scalar
``height`` cannot express, and which the Laplace mode solve,
the ``"ez"`` source component, the modal voltage
``V = Σ Ez·dz`` and the trace-conductor PEC scan all assume.
Accepting it would return a z-normal answer for a board that
is not oriented that way.
``mode="eigenmode"`` remains ``"+x"``/``"-x"`` only.
impedance : float
Target Z0 in ohms (default 50). Used for the σ distribution.
waveform : GaussianPulse or callable, optional
Excitation waveform. Defaults to a band-limited Gaussian
centred at freq_max/2. Ignored when excite=False.
excite : bool
True → resistive termination + active source; False
→ passive matched termination only.
n_probe_offset : int, optional
Distance (cells) from the feed plane to the first probe plane.
Automatic offset and spacing counts are recomputed at driver time
in the cells under the port, including graded runways, uniform
dy_profile different from dx, and auto meshes. A ladder
crossing a grading ramp retains counts based on scalar dx.
When None, bound to the LARGER of two near-field clearances:
(a) the wavelength reactive far-field (issue #80 Fix B),
round(0.5 * lam_min_eff / (2*pi) / dx) with
lam_min_eff = c / freq_max / sqrt(eps_r_sub_estimate); and
(b) the source FRINGING transient round(5 * h_sub / dx),
which decays over a few substrate thicknesses (h_sub = the
port height), NOT over λ. For a thin high-εr substrate (b)
dominates: clearing only (a) leaves probe 0 inside the fringing
transient and corrupts the V·I-split S11 of a high-Q resonant
load (issue #80 patch: |S11|=8.94/1.11 → passive ~0.99 once
cleared). These two terms are only the UPSTREAM (lower) edge:
at compute_msl_s_matrix time the auto default additionally
solves the DOWNSTREAM constraint (issue #469) — the deepest
probe must stay ≥ λ_g/4 (at freq_max) clear of the nearest
reflector — picking the midpoint of the compliant interval
when a reflector bounds it, keeping the upstream edge when
none does, and warning loudly when the interval is empty (the
feed line is too short for a clean N-probe measurement).
Pass an explicit value to override (explicit values are never
adjusted); < 5*h_sub/dx triggers a preflight near-field
warning.
n_probe_spacing : int, optional
Distance (cells) between consecutive probe planes. Automatic
counts use the same driver-time runway recount as the offset.
When None,
bound so the total N-probe array span stays ~lam_min_eff/8
independent of n_probes:
round(lam_min_eff / 8 / (n_probes - 1) / dx). For
n_probes=3 this is exactly lam_min_eff/16 (issue #80
Fix B). Shrinking the adjacent spacing as n_probes grows
keeps the probe array from running off short lines.
An AUTO spacing is additionally WIDENED at driver time
(issue #681): ``compute_msl_s_matrix`` /
``compute_mixed_s_matrix`` re-solve it toward a total span of
``(n_probes−1)·λ_g(f_max)/4`` (λ_g from the HJ ε_eff), capped
by the downstream-reflector interval and the absorbing
boundary — a ~0.1·λ_g span leaves the β fit noise-fragile
(measured: median β error 5.5% at 0.10 λ_g vs 0.8% at
0.30 λ_g under 1% probe noise). An explicit value is never
touched.
n_probes : int
Number of equally-spaced voltage probe planes registered for
the N-probe least-squares wave-decomposition extractor (issue
#80 Fix C). Probe n sits at offset + n*spacing cells
from the feed plane. N >= 3 is required; the default 5
over-determines the 2-unknown (alpha, gamma) fit, which
removes the 3-probe quadratic's q→1 singularity. The current
probe at probe 0 is always recorded for the absolute Z0.
name : str, optional
Port label used in result dicts. Auto-generated when omitted.
eps_r_sub : float, optional
Substrate relative permittivity. Used both for the eigenmode
source and as eps_r_sub_estimate for the wavelength-bound
probe-placement defaults (issue #80 Fix B). When None the
estimate is resolved from the dielectric geometry already
registered under the port (a shape whose bounding box contains
the substrate mid-point and whose material has eps_r > 1);
if no such dielectric is found it falls back to 1.0, which is
conservative (largest lam_min_eff → largest offset).
Add a lumped RLC element at a single cell.
The element is modelled via Auxiliary Differential Equations (ADE) that are updated each timestep alongside the standard Yee update. Any combination of R, L, C is valid (set unused components to 0).
Parameters
position : (x, y, z) in metres component : "ex", "ey", or "ez" R : float Resistance in ohms (default 0). L : float Inductance in henries (default 0). C : float Capacitance in farads (default 0). topology : "series" or "parallel"
Notes
For series topology with a single component (e.g., pure L with R=0 and C=0), the element is handled via material folding, not the full series ADE current tracker. To ensure the series ADE path is used, specify at least two non-zero components.
Add a normal-incidence plane-wave TFSF source.
Current scope is intentionally narrow: x-directed propagation,
ez/ey polarization, 3D mode, and CPML boundaries. For
oblique incidence, only the single transverse-axis plane implied
by the chosen polarization is supported.
Parameters
closed_box : bool
Use a finite six-face total-field box with CPML on all three
axes. Normal incidence, uniform 3D Yee only. The default keeps
the historical x slab with periodic y/z. Read the realized
inclusive node bounds with tfsf_box_indices().
waveform : {"differentiated_gaussian", "modulated_gaussian", "continuous_wave"}
Pulse shape injected into the 1D auxiliary grid. (continuous_wave
is only supported at normal incidence — angle_deg == 0.)
``differentiated_gaussian`` (default, legacy rfx): baseband
pulse with no carrier, DC-free, spectrum peaks near
``f0·bandwidth``. Good for broadband measurements but
spectrum extends well below ``f0``.
``continuous_wave``: a single-frequency sinusoid at ``f0`` with a
raised-cosine turn-on (~8 periods), then constant amplitude. Use for
steady-state / narrowband measurements that need a well-defined
time-average ``⟨E²⟩ = A²/2`` (e.g. the quantitative Kerr SPM oracle,
#446). Not for broadband S-parameters — it excites only ``f0``.
``modulated_gaussian``: carrier-modulated Gaussian matching
Meep's ``GaussianSource(frequency=f0, fwidth=f0·bandwidth)``.
Spectrum is a Gaussian centered at ``f0`` with 1/e
half-width ``f0·bandwidth``. Use this for matched rfx-vs-Meep
crossval comparisons.
A ``CustomWaveform`` may supply a fixed, JAX-compatible real
scalar function of time, multiplied by ``amplitude``. It is
added to the auxiliary E node at ``t=n*dt``; it is not the
launched E amplitude. Normal incidence only; use a fixed
record length (``until_decay`` has no source-off contract).
These new source inputs support ordinary ``forward`` and its
internal checkpoints. External JAX transformations, including
grad/value_and_grad/JVP/vmap/jit/checkpoint/scan, are refused:
transformed scattered-field traces do not meet the cross-trace bar.
method : {"bloch", "methodB"}
Oblique-incidence engine (ignored for angle_deg=0, which always
uses the normal 1D-aux path). "bloch" (default) is the narrowband
complex-Bloch 2D-aux path (fields go complex64; frequency-domain
monitors are not transform-aware — see tfsf_2d.py). "methodB"
is the open-domain oblique path (real float32, 1D-aux-along-k̂ +
4-edge box): the transverse axis becomes OPEN/CPML, z stays thin
periodic (2.5-D, z-invariant far field), and NTFF/DFT/flux monitors
read physical fields. "methodB" currently requires
polarization='ez' and a single-device uniform grid.
Realized inclusive total-field node bounds on the padded grid.
The normal slab returns x only; closed_box=True returns x/y/z.
NTFF cell-centre interpolation must clear these bounds, including
the adjacent Yee samples (the NTFF placement check enforces this).
Add a rectangular waveguide port.
x_position is interpreted along the selected port-normal axis from
direction. For example, direction='-y' uses x_position as the
physical y-coordinate of the port plane.
Current scope supports axis-normal boundary ports using rectangular
apertures, with boundary='cpml' and mode='3d'.
Calibration options:
reference_plane/probe_plane: explicit reported planes in physical coordinates along the port normal axiscalibration_preset='source_to_probe': auto-reportS11at the snapped source plane andS21from source to the snapped probe planecalibration_preset=Noneor'measured': use the snapped stored reference/probe planes directly
Sampling still occurs on the nearest snapped grid planes, and the result metadata reports those actual measurement planes explicitly.
waveform selects the source pulse shape. Default
"modulated_gaussian" is the Meep-style bandpass pulse — no
sub-cutoff DC content, so the in-band TFSF filter collapses to
identity, reducing directional leakage in the H+E injection pair.
"differentiated_gaussian" matches historical (legacy) rfx behaviour.
Preliminary measurement shows directionality backward/forward
ratio 13.3 % → 8.2 % for a WR-90 at f₀=10 GHz; further gains
await the discrete-eigenmode profile work (P3).
Return the B1 face declaration with the current feature requirements.
No runner consumes this descriptor in B1. rcs=True collects the
RCS requirement for a caller preparing that operation.
Add a Floquet port for periodic structure / phased array analysis.
The Floquet port injects a plane wave at the given scan angle and extracts Floquet mode amplitudes (S-parameters) from the unit cell response. Requires periodic BC on the two axes perpendicular to the port normal.
Parameters
position : float
Physical coordinate along the port normal axis (metres).
axis : str
Port normal axis: "x", "y", or "z".
scan_theta : float
Scan angle theta from broadside (degrees). Default 0.
scan_phi : float
Scan angle phi in the transverse plane (degrees). Default 0.
polarization : str
"te" or "tm". Default "te".
n_modes : int
Number of Floquet modes to extract (default 1 = specular).
freqs : array or None
Analysis frequencies. Auto-generated if None.
n_freqs : int
Number of frequency points when freqs is None.
f0 : float or None
Source center frequency. Default: freq_max / 2.
bandwidth : float
Source fractional bandwidth. Default 0.5.
amplitude : float
Source amplitude. Default 1.0.
name : str or None
Optional name for the port. Auto-generated if None.
Add a vector probe that records ALL 6 field components.
Records Ex, Ey, Ez, Hx, Hy, Hz at the same position. Results accessible via result.time_series columns [0..5].
Add a frequency-domain 2D plane probe.
Parameters
axis : "x", "y", or "z" Plane normal axis. coordinate : float Physical coordinate in metres along the selected axis. component : field component name One of ex/ey/ez/hx/hy/hz. freqs : array or None Probe frequencies in Hz. Default: linspace(freq_max/10, freq_max, n_freqs). n_freqs : int Number of frequencies if freqs is None. region : tuple or None Half-open transverse array-index crop (lo1, hi1, lo2, hi2). A 1 by 1 crop is a point DFT at that Yee component. name : str or None Optional result key.
Add a Poynting flux monitor on a plane (Meep flux-region equivalent).
Accumulates frequency-domain E and H tangential components to
compute integral Re(E x H*) . n_hat dA at each frequency.
Parameters
axis : "x", "y", or "z"
Plane normal axis.
coordinate : float
Physical coordinate in metres along the selected axis.
freqs : array or None
Monitor frequencies in Hz.
n_freqs : int
Number of frequencies if freqs is None.
size : (float, float) or None
Positive finite extents in the two tangential directions. A finite
window snaps to cell edges, clamps to the physical interior with
a warning on any clamp, and excludes CPML/bounding-node slots.
None means the full allocated plane (legacy behaviour).
center : (float, float) or None
Physical centre of the flux region in the two tangential
directions. None defaults to the declared domain midpoint
on uniform grids and the realized interior midpoint on graded grids.
For example, for an x-normal monitor the two tangential
axes are (y, z). Exactly two finite values are required.
Preflight reports finite requested/realized bounds in metres
through report.flux_regions.
name : str or None
Result key. Default: flux_{axis}_{idx}.
Add a near-to-far-field transform box for radiation patterns.
Parameters
corner_lo, corner_hi : (x, y, z) in metres Opposite corners of the Huygens box. freqs : array or None Frequencies (Hz). Default: n_freqs points from freq_max/10 to freq_max. n_freqs : int Number of frequencies if freqs is None.
Accumulate the structure's own current as block moments, in-loop.
The radiation of a structure is what the current in it radiates. On
the Yee lattice that current is an identity the solver already
enforces — J = curl_h H - eps0 dE/dt at every electric-field edge
— so it can be read off the fields the step is holding, with nothing
modelled and nothing fitted. This monitor sums it over the slab
between corner_lo and corner_hi into a few numbers per
in-plane block (the total current moment and its first two spatial
moments about the block's own centre) and DFTs those, instead of
accumulating tangential E and H over a Huygens surface.
Parameters
corner_lo, corner_hi : (x, y, z) in metres
Opposite corners of the slab, in the same frame as
add_ntff_box(). The z range picks the node planes; the
whole thickness goes into one block.
block_size : float
In-plane block side in metres, rounded to a whole number of
cells. The realized side is what the monitor reports.
freqs : array
Frequencies (Hz).
order : int
2 (the only accepted value) keeps P, Q and T — 30 numbers per
block. Lower orders are refused; the low-level
rfx.current_moments.build_current_moment_monitor keeps them.
Notes
margin_cells : int or (mx, my, mz) Extra cells around the declared corners. off_cells : int Shift of the in-plane partition origin, in cells. 0 and half a block are the two the block rule was measured with.
Notes
The slab must lie in the interior: inside the absorber the E update
is not Ampere's law, so a current read there is the absorber's
fiction. Periodic/Bloch axes, TFSF sources, stencil_order=4, a
graded or dx != dy in-plane mesh, a traced mesh profile, and every
lane whose scan body does not accumulate the monitor are refused
rather than approximated. So is a model the pattern would silently
leave out: magnetic material (mu_r != 1), a PEC or PMC domain
face, a waveguide, coaxial or Floquet port, a microstrip port with
mode="eigenmode", and any dielectric, conductor, dispersive cell,
port, lumped element or source that is not inside the slab (its
outermost edge layer counts as outside).
The named arguments here are the whole public surface. The low-level builder additionally takes deliberately wrong metrics, centres, curl signs and time stamps so the mutation harness can measure what the declared checks catch; those never reach a user's declaration.
Finalize mesh spacing/profiles and extent, returning the selected grid.
Call after adding mesh-driving materials/geometry and before placing lattice-aligned conductors. Later geometry cannot refine this mesh; unresolved features still raise under the normal conductor contract. Repeated calls preserve the same mesh. Build a new Simulation to remesh.
Grid previews (including preflight) alone do not finalize the mesh. Register boundary conditions and ports before retaining grid indices: port registration may change padding, though physical node positions in the domain stay fixed. Caller declarations remain available unchanged.
Return a consolidated mesh-quality and memory-planning report.
The report is intentionally advisory: it reuses preflight()
for physics/geometry warnings, reuses estimate_ad_memory()
when n_steps is provided, and adds a uniform-fine comparator
that estimates how many cells a globally fine mesh would need if
it used the minimum cell size present in any non-uniform profile.
This is the Stage-1 production-near "subgrid-like" planning surface: it helps users decide whether existing non-uniform mesh plus segmented checkpointing is enough before attempting research-only true subgridding.
checkpoint_every and checkpoint_segments are forwarded to the
same AD estimator used by plan_ad_memory(); they are mutually
exclusive. n_warmup is forwarded as reverse-mode tape metadata
only, matching estimate_ad_memory.
Return an advisory mesh plan for this configured simulation.
Create a Simulation with auto-derived parameters.
Parameters
freq_range : (f_min, f_max) in Hz Analysis frequency range. All parameters (dx, domain, CPML, n_steps) are derived from this. accuracy : "draft", "standard", or "high" **kwargs : additional Simulation constructor args (override auto values)
Returns
Simulation with optimal configuration for the frequency range.
Example
>>> sim = Simulation.auto(freq_range=(1.5e9, 3.5e9))
>>> sim.add(Box(...), material="pec")
>>> result = sim.run(until_decay=1e-5)
>>> modes = result.find_resonances()
Run all pre-simulation checks and return warnings.
Resolves and caches the mesh from the current static declaration, without changing declared dx/domain/profiles. Auto-mesh selection is emitted as a UserWarning, separately from validation findings.
Parameters
strict : bool
If True, raise ValueError on the first issue instead of
collecting warnings.
check_ntff : bool or "advisory"
True (default): run the full NTFF check family (PEC-overlap
hard error + λ/4 / λ/2 near-field gap advisories + the
sub-wavelength ground-plane pattern advisory, issue #334).
"advisory": run only the advisories — the tier run()
uses, because the λ/4 and small-ground-plane warnings are
physics-relevant to any far-field computation while the
PEC-overlap hard error remains an inverse-design gate
(issue #303).
False: skip the family entirely.
check_resolution : bool
Run the tightened resolution check (existing _validate_mesh_quality
uses per-material thresholds already — this flag kept for
symmetry and future tightening). Default True.
check_ad_memory : bool
Run AD memory estimate and warn if > 85% of available VRAM.
Requires n_steps_for_memory. Default False (diagnostic only).
n_steps_for_memory : int or None
Step count for AD memory sizing. Required when check_ad_memory.
available_memory_gb : float or None
Override VRAM detection. If None, best-effort via JAX devices.
Returns
PreflightReport
A list subclass of PreflightIssue (each a str
subclass), back-compatible with the legacy list[str] return.
Empty if no issues found. Finite flux-window geometry is recorded
separately in flux_regions, including for issue-free reports.
Preflight the selected S-parameter calculator without running FDTD.
This is a routing/contract check for the port-family-specific S-parameter APIs. It answers "which calculator should this simulation use?" before an expensive run starts:
calculator="run"checksrun(compute_s_params=True)for lumped/wireadd_port(...)families.calculator="forward"checksforward(port_s11_freqs=...)for uniform single-device S11 vectors.calculator="msl"checkscompute_msl_s_matrix(...).calculator="waveguide"checkscompute_waveguide_s_matrix(...).
Parameters
calculator:
One of "run", "forward", "msl", or "waveguide"
(the corresponding method names are accepted as aliases).
strict:
If True, escalate ERROR-severity findings to a raise: collect
everything, then raise a single ValueError listing the errors
(aggregate-then-raise). The underlying NotImplementedError is
recorded as an error-severity issue and re-surfaced as part of that
aggregated ValueError (its exact type is not preserved).
Errors only, not "any issue": the waveguide setup audits report
advisory and informational findings on a perfectly valid routing,
and a healthy two-port guide ALWAYS carries the informational
E-plane note, so escalating on emptiness would make
``strict=True`` raise on every correct waveguide setup. Advisories
are still returned in the report and printed; read them there.
(This differs from ``preflight(strict=True)``, whose report has no
informational tier.)
normalize:
Waveguide non-uniform preflight uses this to mirror
compute_waveguide_s_matrix(normalize=...). None means the
method default, currently False.
include_general:
If True, append the ordinary geometry/material preflight()
issues after the S-parameter routing check.
Returns
PreflightReport
A list subclass of PreflightIssue (back-compatible with
the historical list[str]). Carries no ERROR-severity issue when
the selected calculator is valid for the registered port families —
read report.ok / report.errors rather than emptiness.
calculator="waveguide" additionally runs the three setup
audits (_validate_cfg_record_vs_far_boundary(),
_validate_cfg_port_index_mirror_covariance() and
_validate_cfg_layout_from_band_low_edge()), which report
advisory and informational findings on a perfectly valid routing —
a healthy two-port guide always carries at least the known
E-plane-offset note. Those audits are evaluated at
compute_waveguide_s_matrix's own default num_periods
(WAVEGUIDE_DEFAULT_NUM_PERIODS); the record-length message
names the num_periods that clears the threshold, which does not
depend on the value it was evaluated at.
Compute MSL S from probe-plane V/I with fitted line diagnostics.
Returned S uses power waves for the positive real per-port analytic
references: a=(V+RI)/(2sqrt(R)), b=(V-RI)/(2sqrt(R)).
reference_impedances records those R values separately from the
fitted Z0 and source/load resistances. For unequal references,
transmission S differs from the legacy voltage ratio by
sqrt(R_input/R_output); equal-reference results are unchanged.
S is the RAW extracted S-matrix on every channel — concrete,
eps_override, traced — and the default enforce_passivity=False
is what makes that one statement instead of three. The projection
below cannot run under tracing or on the eps_override channel (it
would zero the gradient wherever the clip bites), so while it was the
default the S a caller measured and the S jax.grad differentiated
were different functions wherever it bit. One function, chosen to be
the measured one (PI decision, 2026-09-21; the contract is
docs/design_notes/chain_closure_contract.md).
A raw S may exceed the passive bound, and the result says so rather
than hiding it: sigma_max_excess[k] = max(sigma_max(S(f_k)) - 1, 0)
is filled on every concrete call (None only under tracing), and a
warning names the bin count and the worst sigma_max whenever any
bin exceeds 0. A passive structure cannot scatter more power than it
receives, so such a bin is a measurement artifact — the usual causes
are a record that ended before the structure rang down and a mesh too
coarse for the geometry (settling_db and reliable say which).
enforce_passivity=True opts back into the post-process: the
assembled S(f) is projected onto the passive set per frequency
(singular values clipped to 1 — the nearest matrix in spectral norm
with ||S||_2 <= 1), the unprojected matrix is kept in S_raw,
the per-bin clip amount in passivity_correction, and a warning
names the touched bins. It is constraint enforcement, not a physics
fix, and it is NOT small where the raw extraction is bad: on a thru
fixture whose raw sigma_max ran 1.19-1.91, projecting moved |S21|
1.000 -> 0.61-0.72 and rotated its phase by up to 17 degrees, while
Z0 and beta stay raw. Never quote projected values as physics
where passivity_correction is large.
EXEMPTION (unchanged): even with enforce_passivity=True no
projection is applied under tracing or on the eps_override
channel (traced or concrete), so that finite-difference and
jax.grad objectives see the same function; S is then the raw
extraction with S_raw/passivity_correction absent and no
projection warning. sigma_max_excess is still filled on the
concrete eps_override channel, so the bound violation stays
visible there (measured sigma_max 1.18 on a coarse thru).
Surface-impedance sheets (add_thin_conductor(...,
surface_impedance_f0=...)) are supported on this lane (#677/#679):
the sheet is realized node-thin by the per-step operator inside the
run()/forward() device dispatches — no lane-level ctx is
built here. Combination refusals fire downstream at whichever lane
entry the call actually reaches, and the two channels carry their
OWN copies: on run() the dispersive-substrate and
boundary='upml' refusals come from run_uniform with run-lane
wording, while the forward() / eps_override channel never
enters run_uniform and raises instead from the forward-lane
entry in rfx/api/_execute.py, naming that lane. #679 added the
upml half there: before it, an eps_override call on a
boundary='upml' sim silently ran the very combination run()
refuses (the sheet operator overwriting UPML's split-field E update
at its edges). ADI refuses on both channels through the shared
refuse_f0_sheets helper; the subgridded and distributed lanes
carry their own call sites at whichever entry reaches them —
FENCE_REGISTRY in tests/unit/materials/test_sheet_impedance.py is the
authoritative per-lane list, AST-guarded against drift.
subpixel_smoothing / conformal_pec are run()-only
keywords, so that combination is unreachable on the eps_override
channel rather than refused there.
The TRACE must remain PEC: an f0 sheet realizes no PEC edge, and
the closed Ampere-loop current and the V span anchor on the
realized PEC wall planes (#931 §1.9 — a PEC trace may be a volume
Box or a sheet; both are found). The Hammerstad-Jensen beta/Z0 anchors
and the real-beta N-probe fit assume a lossless line, so a sheet
lying INSIDE a probed span adds per-length loss the fit cannot
represent — reported Z0/q shift (the Z0 honesty guard may
warn) while the V-I production S stays valid. A non-z-normal sheet
(x/y-normal) carries Ez in its tangential edge set: crossing the
feed plane or the V-integration column it legitimately modifies the
measured/launched Ez — place sheets clear of feed and probe planes.
For each registered MSL port, runs one FDTD simulation with that
port driven and the others passive. The passive ports are NOT
assumed matched — |a_passive/a_driven| = 0.243-0.248 at the
shipped R = 50 Ω (0.19-0.93 across terminations), re-measured on
current main 2026-08-30 (#524, VESSL 369367257265, PR #799; the
July figure quoted here before was 0.07-0.51) — so the S-matrix is
recovered by solving the
full wave system S = B·A⁻¹ over all drives rather than by the
per-column ratio b_j/a_d (issue #507; the ratio reported the far
port's echo as the structure's own reflection). At each port
n_probes downstream DFT plane probes record Ez and the first
probe also records the transverse H components for a closed
Ampere-loop current. Production S is formed from this first-plane
V/I and the analytic Hammerstad-Jensen reference impedance. The
N-probe least-squares fit of
V_n = α e^{-jβx_n} + γ e^{+jβx_n} supplies only the reported
beta/Z0 diagnostics. Those fitted values do not enter S. S is
referenced to each first probe plane, without translation back to
the physical feed planes. The two bracketing H planes are linearly
interpolated to the E-node using their physical coordinates, then
the leapfrog temporal offset is corrected. Comparing different
observation offsets still requires accounting for reference planes
and impedances. probe_clearance reports the separate downstream
layout recommendation; it is not a signal or accuracy score.
Parameters
n_steps : int or None
Timesteps per FDTD run. None → auto from num_periods.
num_periods : float
Source-period multiples used to derive n_steps when not
provided. Default 40 (MSL transients are slow to drain).
freqs : array, optional
Frequency grid. Defaults to
linspace(freq_max / 10, freq_max, n_freqs).
n_freqs : int
Number of frequencies if freqs is None.
raw_3probe_dump_path : str or None
Optional .npz path. When provided, write the real
simulation-derived N-probe voltage/current phasors used by the
extractor, together with the production power-wave S-matrix, so the
de-embedding can be independently checked without rerunning
FDTD. The dump schema is rfx.msl_nprobe_dump v4: it explicitly
records the power-wave convention and actual reference impedances.
Legacy v3 records use voltage waves. raw_v has shape
(n_driven, n_ports, n_probes_max, n_freqs).
scripts/diagnostics/replay_msl_3probe_dump.py is SUPERSEDED
for modern dumps (it expects the retired 3-probe/single-ratio
v1 schema). Use rfx.validation.load_port_vi_dump_npz and
replay_smatrix_from_port_vi_dump for independent S replay.
strict_extractor : bool
Honesty guard for the de-embedding (issue #80 Fix A). After
extraction, the per-frequency |q| and extracted Z0
are validated against physical bounds (|q| <= 1 for a passive
line; extracted Z0 within 10 % of the analytic
Hammerstad-Jensen value). When False (default) a violation
raises a loud warnings.warn(); when True it raises
ValueError instead. With the N-probe extractor (Fix C)
|q| and Z0 should be healthy so this rarely fires — it
is the safety net for pathological geometries.
enforce_passivity : bool
Opt into the passivity projection as a post-process. False
(default) returns the raw extracted S; see the section above
for what each setting costs and which channels the projection can
reach. Changed from True on 2026-09-21.
checkpoint_segments : int or None
Gradient-checkpointing segment count for the reverse-mode AD tape on
the uniform mesh (the standard MSL path), forwarded to
forward() (only active on the differentiable eps_override
channel). Must DIVIDE the auto-computed n_steps exactly — padding
is rejected because it would shift the DFT accumulator windows.
Choose the divisor nearest sqrt(n_steps) so backward memory scales
~sqrt(n_steps)*carry instead of n_steps*carry — required for
converged num_periods>=20 AD that otherwise OOMs (G-AD-CHECKPOINT).
Default None leaves forward-only runs and small-period AD unchanged.
checkpoint_every : int or None
Non-uniform-mesh counterpart of checkpoint_segments (chunk size,
not segment count; issue #73). Forwarded to forward(); raises
NotImplementedError on the uniform path — use
checkpoint_segments there.
report_every : int or None
Issue #667 — progress reporting for long solves. Forwarded to
each per-drive run() call, tagged MSL drive pI/N so
the drives are distinguishable in a log. None (default) is
OFF and leaves the solve byte-identical. The measured case this
exists for: a 42.15 M-cell / 225,000-step call whose last log
line was written at second 0 and was still the last line 4 h
10 min later, with no way to tell slow from wedged. Ignored
(with a warning) on the differentiable eps_override
channel, which routes through the traced forward().
Returns
MSLSMatrixResult
One-port coaxial reflection on a real transmission line (broad-E5).
Builds a coextensive coax line (face='top', pin along -z) terminated in a
matched resistive feed near the +z boundary, drives a TEM TFSF source one
cell below the feed, and reflects off a calibration termination at the
-z end: "short" (Γ=-1), "open" (Γ=+1), or "matched" (Γ→0).
With termination="matched" and dut_impedance=R the DUT is instead a
known resistive load (analytic Γ=(R-Z0)/(R+Z0)) — used by the broad-E5
envelope to test non-trivial reflection magnitudes against exact truth.
The reflection is read from the modal voltage V(z)=∫E_r dr sampled at
probe_count equally spaced planes and a matrix-pencil estimate of the
complex propagation constant (β self-measured, Z0-free).
Unlike the single-plane coaxial lane removed in #1212 (V/I in a hardcoded
closed PEC box — non-physical |S11|>1), this uses an absorbing CPML feed
so a real line exists. Resolution recipe: keep ≥~4 cells across the
(outer-inner) annulus (raise freq_max to shrink dx); the result
status reports "under_resolved" below ~3.5 cells.
The conductors deliberately stop ~2 cells short of the +z PML — running PEC into CPML is numerically unstable.
The line sits in open space: the run absorbs on all three axes
(cpml_axes=None reads the grid; all three axes are required), so what
leaves the line's unshielded ends — the open termination, and the pin and
shell ending one cell past the matched feed — is absorbed as it would
radiate from a cable. cpml_axes="z" is refused: it left the lateral
pads as vacuum backed by the PEC grid faces, a closed metal can that held
that field, and the open's |S11| rose above 1 and grew with the record
(issue 1218).
The enclosing Simulation must use float32 precision and the
three-dimensional, second-order uniform Yee solver with
boundary="cpml" and cpml_layers > 0. Unsupported precision,
solver, grid, boundary, TFSF, and refinement settings raise before the
grid is built. The run requires positive CPML thickness on all six faces
and no periodic-axis override, and the line's conductors must keep at
least one whole cell from every lateral absorber (the stamper refuses a
line without that room).
This method constructs its own coaxial line, TEM source, DFT planes, and
termination. Do not add separate geometry, thin conductors, lumped RLC
elements, probes or field monitors, or NTFF boxes; those registrations
are rejected rather than ignored. Use the documented Simulation, port, and method
arguments instead.
The registered coaxial port supplies its x/y centre, face, inner
and outer radii, and excitation waveform. The method derives its axial
layout internally, so the port's z coordinate and pin_length do not
place the line, and the port's impedance does not set either load.
Use feed_impedance for the feed and dut_impedance only with
termination="matched". probe_count must be an integer of at
least three, and every requested plane must fit between the DUT and
source; otherwise the method raises before starting the FDTD run.
Differentiable (eps_scale)
Pass eps_scale (a scalar or (nx, ny, nz) jnp array) to make
the reflection differentiable w.r.t. the dielectric under jax.grad.
It MULTIPLIES the stamped eps_r (eps_r <- eps_r * eps_scale),
applied AFTER the numpy conductor/dielectric stamps so the fixed geometry
(PTFE fill in eps_r, PEC pin/shell in sigma) is preserved and only
modulated — a well-conditioned design channel (unlike replacing the fill
with air). When provided, the field→voltage→reflection assembly runs on
the jax.numpy path (coaxial_line_plane_voltage_jnp + the traced
extractor) so the gradient flows design → FDTD → DFT planes → Γ. With
eps_scale=None the result is byte-identical to the validated numpy
path. The AD↔FD gate is tests/unit/autodiff/test_coax_end_to_end_ad.py.
Two-drive coaxial 2-port S-parameters (#489 stage 2) — VALIDATED WITH SCOPE.
VALIDATED WITH SCOPE, not EXPERIMENTAL (issue #489, PI
decision 2026-08-06 —
docs/guides/sparameter_support_matrix.md, the S-parameter
family companion where this row lives). Covered: the two-port
through-line class on this single coax geometry family, bracketed
by an external openEMS referee (crossval 21) on |S21| and,
via the port's own measured beta, phase; a mesh-refinement
convergence witness (p ~= 1.5); an end-to-end eps_scale
AD gate (GRAD_SAFE); and this method's own measured
reciprocity/cond(A). NOT covered: every DUT this method can
currently gate against is azimuthally symmetric (TM0n only) —
transition discontinuities that excite TE11 are still outside
this evidence (see compute_coax_msl_transition(), which
stays EXPERIMENTAL, diagnostic-only), nor does the evidence
generalize to other coax geometry families. See the class
docstring on the returned CoaxialTwoPortResult for the
full evidence chain and scope statement.
Builds ONE through coax line spanning the z axis with a matched
annular-resistor feed near EACH z end (mirroring the validated
1-port compute_coaxial_line_reflection() layout at both ends:
each feed sits between that end's own TEM TFSF source and that end's
own CPML, i.e. strictly on the scattered-only side of that drive's
TFSF boundary — never in the path of that drive's own launched
wave), then drives each end's source in turn (two separate FDTD
runs). A probe array of probe_count equally spaced planes near
each end recovers that array's local two-wave decomposition
(matrix-pencil, Z0-free, same machinery as the 1-port method); the
forward/back amplitudes are evaluated at each port's OWN reference
plane (its feed's axial position) and assembled into a full 2x2
S-matrix via rfx.sources.coaxial_port.
solve_two_port_from_wave_amplitudes() — the two-drive solve that does
NOT assume the non-driven port sees zero incident wave (unlike the
naive S[j,i] = b_j/a_i ratio, which has a hard terminator-
reflection floor on a through line; see that function's docstring
and docs/research_notes/20260729_i489_coax_two_port_design.md).
Port 1 is the +z end (mirrors the 1-port fixture's own face='top'
orientation); port 2 is the -z end (mirror image, face='bottom').
Both drives share the SAME registered add_coaxial_port(...)
geometry (x/y centre, pin/outer radii) and excitation waveform; the
registered port's own position/face/pin_length do not
place either end of the line (mirrors the 1-port method's own
contract). Requires port.face == 'top' (arbitrary but consistent
with the 1-port method; the value is not otherwise used to orient
this method's own internally-built fixture).
Unlike the 1-port method, the returned result is routed through
_finalize_sparam_result() (which runs the passivity/finiteness
self-check via _warn_if_nonpassive_smatrix()) before being
returned — the 1-port CoaxialLineReflectionResult bypasses that
check (design-note incidental defect 3); this result does not.
This method constructs its own coaxial line, TEM sources, DFT planes, and feeds. Do not add separate geometry, thin conductors, lumped RLC elements, probes or field monitors, NTFF boxes, or TFSF sources; those registrations are rejected rather than ignored.
Same solver/precision/boundary contract as
compute_coaxial_line_reflection() (float32, 3D uniform Yee,
boundary='cpml' with positive CPML thickness on all six faces,
cpml_axes='xyz', no periodic axes, no non-uniform mesh, no
refinement). The run absorbs on all three axes: the line's two ends,
one cell past each feed, are unshielded, and with absorbers on z only
the lateral pads were a closed PEC can that held the field leaving them
(issue 1218). cpml_axes='z' is refused.
Differentiable (eps_scale, #489 leg 3)
Pass eps_scale (a scalar or (nx, ny, nz) jnp array) to make
the S-matrix differentiable w.r.t. the dielectric under jax.grad —
same name, semantics, and MULTIPLIES-the-stamped-eps_r design as
the 1-port compute_coaxial_line_reflection()'s own eps_scale
(eps_r <- eps_r * eps_scale, applied once, after both feed stamps,
so it reaches BOTH drives' FDTD runs — the through line has no DUT
break to scope it to). When provided, both drives route their
voltage extraction through rfx.sources.coaxial_port.
coaxial_line_plane_voltage_jnp() (the same differentiable twin the
1-port path uses) instead of the concrete
~rfx.sources.coaxial_port.coaxial_line_plane_voltage(), and the
assembly (_assemble_coaxial_two_port_from_voltages()) and the
two-drive solve (~rfx.sources.coaxial_port.
solve_two_port_from_wave_amplitudes()) both dispatch to their own jnp
cores. With eps_scale=None the result is byte-identical to the
validated numpy path (dual-path design, not a rewrite — mirrors the
1-port method's own contract). The per-drive ring-down settling_db
witness needs a concrete time series and is skipped on this path
(stays nan, same reasoning as the 1-port method's own
"can't Python-branch on a tracer" note on its rec_resid
contamination check); status is therefore "under_resolved" or
"differentiable" here, never "contaminated"/"passed".
cond_warn is also silently INERT on this path: the ill-
conditioning warning it controls is Python control flow keyed on a
concrete cond(A) value
(rfx.sources.coaxial_port.solve_two_port_from_wave_amplitudes()'s
NumPy branch), which the traced jnp core
(_solve_two_port_from_wave_amplitudes_jnp) cannot evaluate and
does not attempt to — this mirrors the settling_db/status
losses above (same "can't Python-branch on a tracer" reason), not a
separate defect; cond_a is still RETURNED (as a tracer), so a
caller can inspect it after concretizing the result if degeneracy
matters to them. The AD gate is tests/unit/autodiff/test_coax_two_port_ad.py.
extra_flux_monitors (issue #589 flux-adjudication instrument):
an ENERGY-WITNESS channel, not an extractor change. Pass the entry
objects Simulation.add_flux_monitor registers (build them on a
scratch Simulation with the same domain and hand over its
._flux_monitors); each internal drive run then accumulates the
requested Poynting-flux planes, and per-drive spectra come back
name-keyed on result.flux_monitors
({port_name: {monitor_name: (n_monitor_freqs,) float64}}, net
flux, positive = +axis). The S-parameter math is untouched — the
non-perturbation witness (S bit-identical with and without
monitors) is gated in
tests/unit/sparams/test_coax_msl_transition.py::test_extra_flux_monitors_do_not_perturb_s.
The registered-monitor guard is unchanged: monitors registered ON
this sim still raise, because this method builds its own probes.
EXPERIMENTAL coax<->microstrip transition 2-port S-parameters (issue #489 leg 4).
EXPERIMENTAL — not in the validated set
(docs/guides/sparameter_support_matrix.md / .json). One
pre-declared fixture has been run against this method; see that
fixture's own predeclaration
(tests/unit/sparams/test_coax_msl_transition.py) for the measured
envelope. NOT_TRACEABLE (see ~rfx.api._spec.
CoaxMSLTransitionResult's class docstring for the full honesty
contract, including why the MSL side is extracted via the coax
matrix-pencil fit rather than the diagnostic-only N-probe fit
488 uses). Since issue #838 this lane REFUSES by default: a
non-passive extracted S raises ValueError unless the caller
passes strict_passivity=False, which returns the diagnostic
matrix with a UserWarning instead.
Generalizes issue #488's mixed lumped/wire<->MSL assembler
(compute_mixed_s_matrix()) to a coax<->MSL pair by combining,
UNCHANGED, the less-invasive half of each family's own validated
machinery instead of writing a new geometry-specific extractor:
- The coax side is built exactly like
compute_coaxial_two_port()'s own single-ended stub (CPML, TEM TFSF source, matched annular- resistor feed, then a probe array), reusing~rfx.sources.coaxial_port.stamp_coaxial_line(),~rfx.sources.coaxial_port.stamp_coaxial_annular_resistor(), and~rfx.sources.coaxial_port.build_coaxial_tem_plane_source_specs()verbatim — but only ONE end (this method has no second coax port). - The MSL side is consumed exactly like
compute_mixed_s_matrix()consumes its MSL ports: the caller registers arbitrary DUT geometry (substrate, trace, ground plane, and — unique to this transition — the ground-plane clearance hole and the vertical pin-to-trace post that connects the two families) via the ordinarysim.add(Box(...)/Cylinder(...), material=...)API, and this method reuses~rfx.sources.msl_port.compute_msl_mode_profile(),~rfx.sources.msl_port.setup_msl_port(), and~rfx.sources.msl_port.make_msl_port_sources()verbatim for the MSL port's own termination/excitation.
This method does NOT build the junction geometry itself (the ground
plane, its clearance hole, or the pin-to-trace post) — those are
DUT-specific and belong to the caller's own registered geometry,
exactly as compute_mixed_s_matrix() never builds its own
substrate/trace. What this method DOES fix, by construction, is
WHERE each port's own S-parameter reference plane sits: both are
placed AT the physical launch discontinuity (see junction_x
below and port.position[2] on the registered
add_coaxial_port()), specifically to minimize the
pre-declared "reference-plane mismatch" failure mode (the coax's
axial z-feed-plane convention has no direct analogue in the MSL's
along-trace x reference plane — these are different geometric axes
entirely).
Registration contract
Exactly one add_coaxial_port() (face='bottom' — the
physical convention this method assumes: the coax stub is built
FROM the domain's low-z CPML face UP TO position[2] (rounded to
the nearest grid z-node, half-cell ties to the lower node under
the conductor-plane convention), where position[0], position[1] is the coax
axis centre (x, y) and position[2] is the physical height of
the caller's OWN registered ground-plane conductor — i.e. this is
the ONE parameter that ties this method's auto-built coax stub to
the caller's own junction geometry; get it wrong and the pin will
either dangle in a gap or overlap the caller's own PEC). Exactly
one add_msl_port(), whose position[2] (substrate bottom /
ground height) MUST equal the coax port's position[2] to within
one grid cell — both refer to the SAME physical ground plane. No
other ports (lumped/wire/waveguide/Floquet), no TFSF, no lumped
RLC, no pre-registered probes/DFT planes/flux monitors/NTFF (this
method builds its own). self._geometry must be non-empty (the
caller's substrate/trace/ground-plane/pin-post Boxes and
Cylinders). The MSL source interval must meet its realized conductor
surfaces. The auto-built stub stops below the junction node and
preserves the caller's materials at and above it. These checks do
not certify the junction's connectivity or its modal accuracy.
Same solver/precision/boundary contract as
compute_coaxial_two_port() (float32, 3D uniform Yee,
boundary='cpml' with positive CPML on all six faces, absorption on
all three axes): the coax's own far end needs an absorbing z face
exactly like the coax lane, the MSL trace radiates in x/y too,
and the caller's own ground plane is an INTERNAL stamped/registered
PEC layer rather than the domain's z boundary — this is also WHY
this method cannot reuse a PEC z_lo domain boundary as the MSL
ground reference the way compute_mixed_s_matrix()'s
magnitude_channel="flux" fixtures do (that would conflict with
the coax's own need for an absorbing z_lo face).
Two-drive extraction (never a naive single-ratio)
Drives the coax source, then the MSL source, in two separate FDTD
runs (never assuming the non-driven port sees zero incident wave —
the terminator-floor problem compute_coaxial_two_port()
already solved for symmetric coax applies here too, and likely
worse, since the coax feed resistor and the MSL Hammerstad-Jensen
termination have no reason to share a termination quality). Each
port's own forward/backward wave amplitudes are recovered from its
own probe ladder via ~rfx.sources.coaxial_port.
coaxial_line_reflection_from_plane_voltages() (Z0-free matrix-pencil
fit — see ~rfx.api._spec.CoaxMSLTransitionResult for why
this is used for the MSL side too, not the lane's own N-probe fit),
converted to POWER waves via each port's own analytic reference
impedance, and assembled by
~rfx.sources.coaxial_port.solve_two_port_from_wave_amplitudes()
(the same generic two-drive solve compute_coaxial_two_port()
uses). See _assemble_coax_msl_transition_from_voltages() for
the full pure-assembly derivation.
Parameters
junction_x : float
Physical x-coordinate (metres) of the coax-to-trace launch
discontinuity — the MSL side's own S-parameter reference plane.
Must match wherever the caller's own registered pin-to-trace
post / trace Box actually begins; this method does not infer it
from geometry (mirrors the coax side's own reference plane
being the registered port's position[2], not inferred).
eps_r_sub : float, optional
Substrate relative permittivity. If None, taken from the
registered add_msl_port()'s own eps_r_sub (which must
then be set explicitly — this method does not attempt the
geometry-bounding-box auto-detection add_msl_port()
itself offers, to keep the eps anchor unambiguous for the
Hammerstad-Jensen Z0 used in the power-wave normalization).
probe_count, probe_start_cells, probe_spacing_cells : int
The COAX side's own probe array (mirrors
compute_coaxial_two_port()'s identically-named
parameters — this method's coax stub is short by
construction, between the near-source feed and the junction,
so these three rarely need to grow).
msl_probe_count, msl_probe_start_cells, msl_probe_spacing_cells : int, optional
The MSL side's OWN probe array, independent of the coax
parameters above (added issue #489 leg 4 attempt 2 — the two
families' probe ladders were coupled through one shared set
of parameters through attempt 1, which is fine when both
ladders are short but breaks as soon as the MSL side needs a
ladder spanning a meaningful fraction of a guided wavelength,
since the coax stub's own short z-extent cannot host the same
span). Each defaults to None, meaning "use the
correspondingly-named coax parameter" — this preserves
attempt 1's exact behavior (and its committed fixture's
numbers) when left unset.
NEAR-FIELD STANDOFF (issue #823) — the one constraint these
three are NOT free of. Every probe must clear BOTH ends the
ladder is referred to (the MSL port's own feed plane AND the
reference plane at ``junction_x``) by at least
``max(3, round(5*h_sub/dx))`` cells. Both are launch
discontinuities, and within a few substrate thicknesses of one
the field is not the guided mode yet, so a matrix-pencil fit
that includes such a probe reports the LADDER's error as the
field's: on the settled attempt-3 run (VESSL 369367257533) a
single probe 0.4 mm = 1.33*h_sub from the feed dragged the
full-ladder residual from 3.7e-3 to 0.342, and this lane spent
three attempts attributing that to junction physics. The
threshold is the repo's EXISTING issue-#80 Fix B constant —
see `rfx.api._preflight.msl_source_near_field_standoff_cells()`
for the derivation that licenses reusing it here, and for the
W/h limitation it carries. Preflight cannot enforce it on this
lane (these are METHOD arguments and never reach the
registered ``_MSLPortEntry``), so this method evaluates the
same predicate on its own REALIZED ladder and emits a
``UserWarning`` naming the offending probes. REPORT-ONLY:
nothing is refused. Read it together with
``result.ladder_split_gamma_dev`` /
``result.ladder_split_reflection_decades`` (computed when
``return_ladder_voltages=True``, ``None`` otherwise), which say
whether the ladder actually disagrees with itself.
strict_passivity : bool, default True
Default True: this lane REFUSES a non-passive extracted S,
raising ValueError from the shared guard
(_warn_if_nonpassive_smatrix() via
_finalize_sparam_result()) instead of returning the matrix.
Pass strict_passivity=False to get the diagnostic matrix back
with a UserWarning instead of the raise. The default is
True here and False on the single-family coax two-port lane
(compute_coaxial_two_port()), which is unaffected by this
(issue #838, PI decision 2026-09-20).
extra_flux_monitors (issue #589 flux-adjudication instrument):
an ENERGY-WITNESS channel, not an extractor change. Pass the entry
objects Simulation.add_flux_monitor registers (build them on a
scratch Simulation with the same domain and hand over its
._flux_monitors); each internal drive run then accumulates the
requested Poynting-flux planes, and per-drive spectra come back
name-keyed on result.flux_monitors
({"coax"|"msl": {monitor_name: (n_monitor_freqs,) float64}},
net flux, positive = +axis). The S-parameter math is untouched —
the non-perturbation witness (S bit-identical with and without
monitors) is gated in
tests/unit/sparams/test_coax_msl_transition.py::test_extra_flux_monitors_do_not_perturb_s.
The registered-monitor guard is unchanged: monitors registered ON
this sim still raise, because this method builds its own probes.
Unless skip_preflight=True, the shared sheet-size verdict runs
before stepping: snap="strict" blocks an off-size sheet and
snap="declared" warns. Other preflight families are not run here.
return_ladder_voltages (issue #589 label-independent ladder dump):
a second read-only channel, additive in exactly the same sense. When
True, the RAW per-probe modal voltages this method already
computed for both drives are attached to
result.ladder_voltages (None otherwise) so the ladders can be
re-read OFFLINE — adjacent-pair phase slope (which way the dominant
wave travels), standing-wave ratio, subset matrix-pencil fits —
without a second FDTD run and without trusting any incident/outgoing
LABEL. The dict is documented field-by-field on
~rfx.api._spec.CoaxMSLTransitionResult. It is built from
.copy() of arrays that are complete before, and consumed by,
_assemble_coax_msl_transition_from_voltages(), so every
S-parameter number is bit-identical with the option off or on —
gated by tests/unit/sparams/test_coax_msl_transition_ladder_dump.py::
test_return_ladder_voltages_does_not_perturb_s (byte-identity A/B,
the same discipline as the flux witness above; the round-trip
assertion there also proves the dump IS what the assembler consumed).
The same flag switches on the issue-#823 LADDER SELF-CONSISTENCY
WITNESS, result.ladder_split_gamma_dev /
result.ladder_split_reflection_decades (None when the flag
is off): a disjoint-half refit of each ladder, i.e. a Python loop of
2 drives x n_freqs x 2 matrix pencils per ladder over exactly the
arrays the dump exposes. It is computed AFTER the assembler from
those arrays and moves no S-parameter number; it is opt-in so that
a default call pays for no extra pencil solves. Documented
field-by-field on ~rfx.api._spec.CoaxMSLTransitionResult.
Returns
CoaxMSLTransitionResult
Return the FULL conductor cell footprint of this simulation.
Since #677 a surface-impedance (surface_impedance_f0) thin
conductor is a node-thin per-step operator: it touches neither
pec_mask nor materials.sigma, so the obvious hand-written
conductor test pec_mask | (sigma > 1e3) finds NOTHING for a
board whose every trace is an f0 sheet and reports a healthy
model as disconnected. There are now three places a conductor
can live, and this accessor is the one spelling that covers all
three::
pec_mask | (sigma > sigma_threshold) | union(f0 sheet masks)
Parameters
grid : Grid or NonUniformGrid or None
Grid to rasterize against. None (default) builds the
grid this simulation would actually run on -- the non-uniform
grid when any of dx_profile / dy_profile /
dz_profile is set, the uniform grid otherwise -- so the
returned mask has the shape of the real run's arrays.
sigma_threshold : float or None
Conductivity (S/m) at or above which a cell counts as
conductor. Default CONDUCTOR_SIGMA_THRESHOLD
(1e3). The comparison is strict >, matching the
sigma > 1e3 spelling this replaces.
Returns
jnp.ndarray
Boolean array of grid.shape. All-False when the model
has no conductor at all.
Notes
The f0 contribution is the sheet's rasterized CELL mask (the
one-layer footprint apply_pec_mask would zero for the same
shape), not the tangential EDGE masks the runtime operator
applies. Cell footprint is what a connectivity / occupancy check
wants; the edge masks live on SheetImpedanceCtx.
Return (shape, NamedSharding) for distributed forward designs.
The global shape is (nx_padded, ny, nz) with no ghosts, where
nx_padded rounds the NU grid's x extent up to a multiple of the
device count. Sharding is P("x") on the ordered devices
(default: all global JAX devices). Every process must use the same
list. Construct large designs with jax.make_array_from_callback
or jax.make_array_from_single_device_arrays using this layout;
no whole-domain device allocation is necessary. Forward ignores
alignment-pad values, and their returned gradients are zero.
Place a concrete local whole-domain design in the forward layout.
This convenience initializer copies addressable x slabs from a host
array; it is not an autodiff operation. Differentiate forward with
respect to the returned global array. For designs too large for one
host, construct shards directly using distributed_override_layout.
On multiple processes each caller supplies the same concrete array.
Run a minimal differentiable forward simulation.
This path is designed for jax.grad / jax.value_and_grad and
intentionally returns only the observables needed by differentiable
objectives instead of the broader stateful run() result.
Parameters
gradient : {"autodiff", "adjoint"}
Opt-in settled-spectrum reciprocity design eps adjoint.
Design sigma overrides are refused (#1424): on lossless design cells the conductivity gradient has no settled value in a finite record
(a static sensitivity remains although the field decays); this adjoint returns settled-spectrum gradients only. Fixed lossy materials are allowed.
Stores design-edge DFTs and runs a second ordinary forward.
ForwardResult.adjoint_settling reads the primal design-edge max|E| over the last period of the lowest bin, capped at the record length, divided by the record maximum.
Above about 1e-2 means unsettled; small proves nothing: it follows the strongest field and says nothing about the adjoint record.
Use gradient_record_length_witness to witness an adjoint gradient.
Uniform Yee design_box only; unsupported paths raise. Checkpoint
options are unused by the adjoint (its storage does not depend on them).
eps_override : jnp.ndarray or None
Replacement permittivity array with shape grid.shape.
With distributed=True, eps/sigma/occupancy overrides also accept
x-sharded global arrays from distributed_override_layout().
That form is required across processes; gradients keep its sharding.
Use shard_distributed_override() for a concrete local initializer.
sigma_override : jnp.ndarray or None
Replacement conductivity array with shape grid.shape.
mu_r_override : jnp.ndarray or None
Replacement relative permeability array with shape grid.shape.
Differentiable (the H-update coefficient is dt/(mu_r*MU_0*dx)), so
jax.grad flows w.r.t. a magnetic-material design variable (ferrite /
magnetic-RAM inverse design). Uniform single-device lane only; a
non-uniform mesh or distributed=True raises (it would otherwise be
silently dropped — a zero-gradient no-op).
design_box : (corner_lo, corner_hi) or a DesignRegion, or None
Issue #1179. Bounding box, in metres, of a design region whose
permittivity is handed in separately as design_eps_override
instead of being written into a whole-grid eps_override.
Accepts a (corner_lo, corner_hi) pair or any object carrying
corner_lo / corner_hi (DesignRegion,
TopologyDesignRegion). Corners resolve to cells the way
topology_optimize resolves them — grid.position_to_index
on each corner, both ends INCLUSIVE — and the realized cell
counts, not the declared extent, are what
design_eps_override must match. One difference: a corner
outside the interior is CLAMPED by topology_optimize and
RAISES here, so a region that reaches into the absorber is a
smaller design region there and an error on this path.
What it buys: the Yee E update is linear in the fields, so the
backward pass needs the primal field only where a coefficient is
a design variable. With the grid-wide coefficients left constant
and only this box recomputed from the traced permittivity,
reverse-mode AD stores box-shaped arrays per timestep where
``eps_override`` stores grid-shaped ones. The gradient is the
same gradient (measured to 2.5e-14 relative in float64 against
``eps_override`` on a 57x57x41 open-domain fixture); this is a
change of formulation, not an approximation, and it needs
neither ``checkpoint_segments`` nor any recompute.
**Pass ``checkpoint=False`` with it.** ``checkpoint=True`` (this
method's default) remats every step, which saves the scan CARRY
— the six grid-sized field arrays — once per step no matter what
the coefficients are. Measured on a 23x21x19 grid over 80 steps
with a 3x3x3 box: 0.59 MB of residuals at ``checkpoint=False``
against 26.2 MB at ``checkpoint=True``. The box removes the
coefficient term; only turning the per-step remat off removes
the carry term with it.
The GRADED MESH carries it too (#1183): the corners resolve
against the non-uniform grid's own cell edges and the box redo
uses the graded curl, so a thin substrate meshed with a
``dz_profile`` gets the same box-shaped tape. On that lane a
waveguide port, an MSL port and ``until_decay`` raise — the
first two write their own field or ``materials`` across a whole
plane from the background permittivity.
Fenced, not degraded — for a design PERMITTIVITY
(*design_eps_override*): ``distributed=True``,
``solver='adi'``, ``boundary='upml'``, Debye/Lorentz
dispersion, Kerr, subpixel/anisotropic permittivity,
``pec_occupancy_override``, ``stencil_order=4``, the oblique
Bloch path, combining with ``eps_override`` / ``sigma_override``
/ ``mu_r_override``, a box that reaches into the CPML absorber,
and a box holding a source, port, lumped-RLC or
surface-impedance-sheet cell all RAISE (a lumped or wire port
may stay inside with *design_box_holds_ports*). Each of those either
computes E by some other rule inside the box or reads the
background permittivity at a box cell, and a silently dropped
design variable is a zero gradient that reads like convergence.
A design OCCUPANCY has its own, shorter list — see
*design_occupancy_override*; in particular it does combine with
``eps_override`` and with ``pec_occupancy_override``, because
it reads no material array.
design_eps_override : jnp.ndarray or None
Relative permittivity at the design_box cells, shaped like the
realized box. Usually the traced quantity. Required with
design_box unless design_occupancy_override is given
instead, and rejected without a box.
design_sigma_override : jnp.ndarray, 3-tuple of them, or None
Conductivity at the design_box cells, same shape. None
(default) keeps the run's own conductivity there, so a lossy
background inside the box is carried exactly rather than
silently replaced by a lossless one. A 3-tuple
(sigma_x, sigma_y, sigma_z) gives each E component its own
conductivity — what a conducting SHEET needs, whose current runs
along its two in-plane edges only. See
~rfx.simulation.DesignBoxSpec.
design_occupancy_override : jnp.ndarray or None
Issue #1183. Relaxed-conductor occupancy in [0, 1] at the
design_box cells — metal-shape design (a notch stub, topology
optimisation) rather than a dielectric one. It REPLACES
pec_occupancy_override at those cells and carries it
everywhere else, and the gradient is the same gradient
pec_occupancy_override gives with the same values written
into it.
What it buys is the same trade: ``apply_pec_occupancy`` scales
every E component by ``1 - M``, so a traced whole-grid occupancy
makes the backward pass keep three GRID-sized primal E arrays
per timestep. Confined to a box, the arrays it keeps cover the
box plus one cell — the reach of the incident-cell rule — and
``1 - M`` itself is built once outside the time loop, because an
occupancy is a design variable and not a field. Pass
``checkpoint=False`` with it, for the reason above.
Uniform single-device Yee lane only; a graded mesh,
``distributed=True`` and ``solver='adi'`` raise. So do a
periodic axis (the incident rule wraps across the seam, so the
box's window is no longer the whole set of cells its occupancy
can move), combining it with *design_eps_override* in one call,
a box covering a cell the port setup CLEARS the occupancy at
(the port cell, its six face neighbours, an MSL port's in-plane
diagonal owners), and the Kottke occupancy lane
(``RFX_PEC_OCC_KOTTKE=1``), where the occupancy becomes an
inverse-eps tensor inside the E update that the box's values
never reach.
design_box_holds_ports : bool
Let a permittivity design_box contain lumped and wire ports
(default False: a box holding a port raises). A port is a
device on its edges — a 50 ohm load and a drive built from the
drawn materials before the time loop — so each edge a port drives
or loads inside the box's update window keeps the coefficients
the drawn materials give it, for the update and the drive alike:
the design values do not reach it, their derivative through it is
exactly zero, and a box holding a port with the background
written into it gives the run without the box. Holding keeps the
DRAWN material in the port's feed gap, so the modelled structure
differs from the same design written as a whole-grid
eps_override by that gap. Passive ports (excite=False)
are held too. The other edges of the port's cells stay design
variables; at a port's cell a design value is the cell's volume
material. The held edges come back as
ForwardResult.design_box_held_edges, (axis, i, j, k) with
axis 0, 1, 2 for Ex, Ey, Ez. A soft source (even on a port's
edge), a magnetic source and a lumped RLC element in the box
still raise; an MSL port's termination is not held.
pec_mask_override : jnp.ndarray or None
Additional hard PEC mask to merge with geometry-defined PEC.
pec_occupancy_override : jnp.ndarray or None
Relaxed conductor occupancy field in [0, 1] for
differentiable PEC-style optimisation.
n_steps : int or None
Number of timesteps. If None, auto-computed from num_periods.
num_periods : float
Number of periods at freq_max for auto step count.
checkpoint : bool
Enable gradient checkpointing (default True).
checkpoint_segments : int or None
If set, route the uniform single-device forward through the
segmented-checkpoint path (issue #73), which trades ≈2x compute
for ≈sqrt(n_steps) reverse-mode memory. Must divide n_steps
exactly (padding is rejected so DFT accumulator windows do not
shift). None (default) keeps the legacy per-step
jax.checkpoint behaviour. Currently wired only on the
uniform single-device path; non-uniform meshes and
distributed=True raise NotImplementedError (use
checkpoint_every on the non-uniform path instead).
emit_time_series : bool
Emit the probe time series in the returned ForwardResult
(default True). emit_time_series=False skips the time-series
buffers and is currently only supported on the non-uniform
forward path; on the uniform path it raises
NotImplementedError (frequency-domain objectives such as NTFF
and S-params still emit the series there).
checkpoint_every : int or None
Non-uniform-mesh counterpart of checkpoint_segments (a chunk
size, not a segment count; issue #73). When set to a positive
integer K, the non-uniform scan is run as a scan-of-scan that
remats every K steps for ≈sqrt(n_steps) memory. Currently
only supported on the non-uniform forward path; on the uniform
path it raises NotImplementedError (use checkpoint_segments
there). None (default) leaves forward-only runs unchanged.
n_warmup : int
Number of leading timesteps to run with the carry
stop_gradient'd so reverse-mode AD builds no tape for the
initial transient (issue #40). Only the trailing
n_steps - n_warmup steps participate in autodiff. Must
satisfy n_warmup < n_steps. Default 0 (all steps
differentiated).
The truncation error DEPENDS ON DISTANCE FROM THE SOURCE TO THE
DESIGN REGION — it is not simply "an approximation" that always
costs accuracy (an earlier version of this docstring implied the
error applies unconditionally; that was an overgeneralization
from a single, worst-case fixture — see below). Severing the
carry severs every gradient path through steps ``< n_warmup``,
but only steps during which the WAVEFRONT HAS ALREADY REACHED
the design region carry any such path to sever; before the
wavefront arrives the field there is ~0, so severing those
steps discards ~nothing and the gradient is (near-)exact.
Compute a safe bound yourself::
K_safe ~= floor(min_distance(source, design_region) / (C0 * dt))
(``min_distance`` is the minimum over EVERY active source AND
over each source's own spatial extent, not just its nominal
position — a TFSF plane-wave source illuminates from an
entire box face, not a point; grid steps for the wavefront to
reach the closest design cell, ``C0`` = vacuum lightspeed,
``dt`` = ``sim._build_grid().dt`` /
``sim._build_nonuniform_grid().dt`` — a conservative floor valid
for any slower propagation). ``n_warmup <= K_safe`` stays
within this repo's ~1.5% AD-vs-FD noise floor (near-exact,
<0.1%, well below ``K_safe``); well beyond ``K_safe``, error
grows further and can reach the full gradient magnitude by
the time ``n_warmup`` reaches the loss window. Two measured
regimes (issue #626 part 2 / addendum, both vs an independent
central-FD oracle): a NEAR-SOURCE placement (design cell ~3
cells from the source, so ``K_safe ~ 0``) shows error growing
smoothly from a ~1.5% noise floor up to 58% at the
loss-window boundary itself, and EXACTLY zero (gradient fully
vanished) once ``n_warmup`` extends far enough into the loss
window (``tests/unit/autodiff/test_n_warmup.py::
test_warmup_truncation_error_grows_with_k``); a
FAR-FROM-SOURCE placement (design cell 62 cells from the
source, ``K_safe=108`` measured from the grid's own ``dt``)
shows AD matching the ``n_warmup=0`` FD oracle to <0.01%
through ``K_safe``-20, then growing SMOOTHLY (not a sharp
cliff) as ``n_warmup`` approaches ``K_safe`` -- 0.26% at
``K_safe``-4, 0.60% at ``K_safe``-2, 1.19% AT ``K_safe``
itself (still inside the noise floor above) -- continuing to
grow past it toward 75% by ``K_safe`` + 92 —
``scripts/diagnostics/i626_n_warmup_wavefront_locality.py``
(finely sampled around ``K_safe``; an earlier version of this
docstring, read from a coarser sweep that skipped every point
near the boundary, overstated the transition as a sharp
cliff "exactly at K_safe"). The near-source curve above is
the WORST case, not the general
case: use ``K_safe`` to decide whether YOUR source/design
placement is in the exact or the truncated regime before
assuming ``n_warmup`` costs accuracy. Forward (non-AD) output
is exactly ``n_warmup``-invariant (bit-identical) in every
placement measured; only the gradient is affected. Currently
only supported on the non-uniform / distributed-non-uniform
forward paths; on the uniform single-device lane it raises
``NotImplementedError`` (issue #626 — previously silently
accepted and ignored, worse than an error). Use
*checkpoint_segments* for uniform-lane reverse-mode memory
relief instead — it is exact regardless of placement, at the
cost of recompute rather than gradient accuracy.
skip_preflight : bool
Skip the consolidated preflight() validation suite that
normally runs before the forward simulation (default False).
Use only when preflight has already been run by the caller
(e.g. rfx.optimize runs it once at entry) or to bypass a
known-spurious warning on a deliberate configuration.
distributed : bool, optional
Opt-in, unstable, pending GPU evidence (issue #44). When
True and a non-uniform mesh is configured, route the
differentiable forward through the sharded NU runner
(rfx.runners.distributed_nu.run_nonuniform_distributed_pec)
using a 1-D x-slab decomposition across devices. Defaults
to False (single-device path, no behaviour change). In
v1.6.2 the distributed forward path is NU-only (DP3): a
uniform mesh raises NotImplementedError. TFSF sources
and waveguide ports are unsupported on this path and raise
NotImplementedError at preflight.
devices : list of jax.Device or None, optional
Devices for the distributed run. When distributed=True
and devices=None, defaults to jax.devices(). When
an explicit list is supplied, every device must already exist
in jax.devices() (otherwise ValueError). Passing
devices= without distributed=True raises
ValueError — there is no silent activation of the
distributed lane.
exchange_interval : int, optional
Ghost-cell exchange interval for the distributed runner
(default 1). Only 1 is currently supported by
run_nonuniform_distributed_pec; other values are
forward-compatible reservations and will raise inside the
runner.
port_s11_freqs : array-like or None
Frequencies (Hz) at which to accumulate per-port V/I DFTs inside
the JIT scan body so that ForwardResult.s_params is populated
with wave-decomposition |S11| values for lumped/wire ports
(issue #72) — the AD-traceable counterpart of
run(compute_s_params=True). Required by the
minimize_s11_at_freq_wave_decomp() objective. Currently
supported on uniform and graded single-device paths;
distributed=True raises NotImplementedError.
rlc_values_override : dict or None
Differentiable-value injection for lumped RLC elements added with
add_lumped_rlc() (WP 4-E). LumpedRLCSpec stores plain
floats, so a component value can only enter the AD tape as a tracer
through this override. Maps a 0-based element index (registration
order over add_lumped_rlc() calls) to a dict with any of the
keys "R" / "L" / "C" whose values replace the
registered floats — e.g.
forward(rlc_values_override={0: {"R": R, "C": C}}, ...) makes R
and C of the first RLC element differentiable. A missing element
index or key falls back to the registered spec float. None
(default) uses the registered floats for every element. Only wired
on the uniform single-device path. NOTE: on this path a registered
lumped RLC element now correctly affects forward() even without
an override — previously it was a silent no-op.
Uniform-path only; the non-uniform / distributed lanes do not
iterate self._lumped_rlc in forward().
ringdown : rfx.ringdown.RingdownSpec or None
Issue #1254. Complete the wire-port S-parameters of a record cut
while the structure still rings, inside the differentiable
program: ForwardResult.ringdown.s_params is the completion
run(ringdown=...) gives of the same record (the poles of the
port voltage and current identified on [window_start T, T],
the unrecorded tail of each spectrum added in closed form), and
jax.grad through it differentiates the infinite-record
spectrum (the poles by the frozen Gauss-Newton implicit
derivative, the residues re-fitted), not the cut record.
.s_params_long is the completion from the window that starts
twice as early; the gradient through it, against the gradient
through .s_params, is the gradient's witness
(rfx.ringdown.gradient_witness()). .report holds the
checks of the value (W0, W1, WE and the rest, as run()'s),
computed on the host from a concrete result and None while
traced. Wire ports only (add_port(..., extent=...)); on the
uniform lane one port and port_s11_freqs= are needed (the
bins), on the graded lane the completion uses the lane's own
bins. Everything run(ringdown=...) refuses is refused, and
so are distributed=True and emit_time_series=False.
None (default) leaves every output and the traced program as
they were.
**_removed_kwargs Rejection shim for removed keywords, providing migration errors; it does not accept additional simulation options.
Returns
ForwardResult
Minimal differentiable observables (time series and optional NTFF).
settling_db and settling_witness score retained user-probe
records on the concrete result using the run() diagnostic and
underflow floor. They are host diagnostics, unavailable during
tracing, and can be read on concrete records outside the jitted
objective. Other result fields keep their existing JIT
restrictions, including the host Grid object. Absent
records yield None rather than a passing value. Use
settling_verdict(result.settling_db) for the shared -40 dB bar.
Notes
forward() refuses refinement: it has no subgridded solve.
forward() can be called inside jax.jit, so an optimisation
step compiles once: step = jax.jit(jax.value_and_grad(loss))
compiles on its first call and reuses the program afterwards, while
an un-jitted jax.value_and_grad(loss) compiles the whole solve
again on every call (#1225). Under an outer jax.jit (single
device), the set-up built from the model alone is evaluated while
tracing and the time stepping is compiled into the caller's program
(#1367, #1354), so a function with no traced input compiles too. The
set-up's arrays become constants of that program: the first call
costs a plain call's set-up on top of the compile, and the compiled
program holds those arrays for as long as JAX caches it. The loop
compiles over operands XLA cannot read, as in a plain call; what is
computed from a traced input before the loop (a port's drive from a
traced permittivity, #1320) is compiled with the caller's program,
so the jitted value and gradient agree with a plain call to float32
rounding, not necessarily bit for bit.
Run the simulation.
Parameters
n_steps : int or None
Number of timesteps. If None, auto-computed from num_periods.
num_periods : float
Number of periods at freq_max for auto timestep count.
checkpoint : bool
Enable gradient checkpointing for reverse-mode AD.
compute_s_params : bool or None
Compute S-parameter matrix. Default: True when ports exist.
s_param_freqs : array or None
Frequencies for S-parameters. Default: 50 points from
freq_max/10 to freq_max.
s_param_n_steps : int or None
Timesteps for S-parameter simulation (may need more than
the main simulation for frequency resolution).
until_decay : float or None
When provided, overrides n_steps and runs until field
energy decays to this fraction of peak. E.g. 1e-3.
Boundary-dependent stop (issue #169, RESOLVED for absorbing
boundaries): On absorbing boundaries (cpml / upml)
the stop now uses the total interior-domain energy
U = sum(E^2 + H^2) over the non-CPML interior — a whole-domain
quantity that does not pass through the per-cell interference nulls
of the old single-cell point-field stop, so it is suitable for
flux / S-parameter / transmission measurements on guided / low-loss
geometries (#169 resolved). On closed / PEC boundaries domain
energy does not decay, so the stop falls back to the historical
instantaneous single-cell field; that fallback keeps the original
limitation (valid only for lossy / radiating ring-down, not flux /
S-param / transmission gating on guided / low-loss closed geometries
— use a fixed n_steps there, see the
rfx.simulation.run_until_decay() note).
Non-uniform (dx/dy/dz-profile) meshes (issue #383): the same
interior-energy stop runs on absorbing (cpml/upml)
boundaries via a chunked host loop over the NU scan step
(dV-weighted energy — per-cell dx*dy*dz — so graded cells
are weighted faithfully). Closed-boundary NU sims refuse
until_decay (no point-field fallback on the NU lane).
On the NU decay path all step-sized buffers are allocated at
decay_max_steps and flux monitors must keep the default
rectangular DFT window. (The non-uniform runner additionally
rejects its internal checkpoint_every segmentation option
when a decay stop is requested; that option belongs to the
forward() lane and is not reachable through run().)
DC floor (issue #388): a soft-source waveform with net DC
content deposits a static end-charge whose self-energy floors
the interior-energy criterion; run() warns quantitatively
when a planned soft-source waveform's relative DC exceeds 1e-3
(raise the GaussianPulse cutoff, narrow a
ModulatedGaussian to bandwidth <= 0.4, or use a fixed
n_steps). This advisory does NOT catch low-rel_DC floors
amplified by thin source cells (the #388 patch class measured
the floor at rel_DC ~6e-5) — see issue #388.
decay_check_interval : int
Check decay every N steps (default 50).
decay_min_steps : int
Always run at least this many steps (default 100).
decay_max_steps : int
Hard upper limit on steps (default 50000).
decay_energy_consecutive : int
Absorbing-boundary only: number of consecutive sub-threshold
interior-energy checks required before stopping (default 2;
>= 2 mandatory — the interior energy is not null-free and a
single check can false-fire on a transient inter-packet dip).
radiated_flux_box : tuple or None
Physical lower/upper corners of a closed box enclosing the radiator,
clear of CPML. Selects outgoing-flux decay instead of interior-energy
decay on absorbing boundaries; None keeps the energy criterion.
flux_env_checks : int
Number of recent checks whose maximum absolute flux forms the
radiated-flux envelope (default 4).
snapshot : SnapshotSpec or None
Field snapshot schedule and selection; None disables snapshots.
subpixel_smoothing : bool or str
Material-interface smoothing rule; False disables smoothing.
True enables dielectric smoothing; "kottke_pec" selects the
unified PEC occupancy rule on supported lanes.
skip_preflight : bool
Skip advisory preflight checks. Runtime admission guards still apply.
decay_monitor_component : str
Field component to monitor (default "ez"). Used only by the
closed/PEC point-field fallback stop.
decay_monitor_position : tuple or None
Physical position to monitor. If None, use domain center.
conformal_pec : bool
Enable Dey-Mittra conformal PEC for second-order accuracy
on curved PEC surfaces. Default False (staircase PEC).
conformal_min_weight : float
Minimum conformal weight for CFL stability clamping.
Default 0.1. Recommended range: 0.05-0.3.
devices : list of jax.Device or None
When a list with len > 1 is provided, run the simulation
distributed across those devices using 1D slab decomposition
along the x-axis via shard_map. Supported combinations are
listed in docs/guides/support_matrix.md.
exchange_interval : int, optional
Ghost exchange interval in timesteps; only integer 1 is supported.
report_every : int or None
Issue #667 — progress reporting for long solves. When set,
print one [PROGRESS] ... line every N timesteps giving
steps done / total, wall elapsed, implied rate and ETA, on the
same stdout channel as preflight so a redirected job log
captures it. None (default) is OFF and leaves the solve on
its unchanged code path, so nothing moves for existing callers.
Supported on the **uniform lane only** (both the fixed
``n_steps`` scan and ``until_decay``); the distributed,
non-uniform, ADI and subgridded lanes warn and drop it rather
than silently producing no output. It is forward-only and
raises under ``jax.jit``/``grad``/``vmap`` (the check inspects
the carry, the per-step inputs and the material / geometry
arrays — the routes ``forward()``/``optimize()`` trace through).
On the fixed-``n_steps`` scan the reporting turns the single
compiled scan into ``ceil(n_steps / report_every)`` calls to
the SAME compiled scan with ``carry`` threaded through — a
continuation, not a re-solve, and bit-exact against
``report_every=None`` (locked by
``tests/unit/runners/test_run_progress_reporting.py``).
Each report costs one device synchronisation plus one print.
On a 118k-cell CPU fixture that cost was **not resolvable above
the measurement noise** at any cadence tried: a paired
off-vs-off self-control read -4.8% [-9.3, -0.9], and every
cadence from 2000 down to 25 landed inside that floor. The one
clean figure is +0.7-0.8% for a single-chunk run, bounding the
fixed per-report cost. So choose ``report_every`` for a useful
reporting interval, not to minimise an overhead this benchmark
could not detect; only the direction (finer cadence costs more)
is established. None of this speaks to the large-grid GPU
regime, which was not measured.
report_label : str
Short tag prefixed to each progress line (e.g. "drive p1").
Ignored when report_every is None.
ringdown : rfx.ringdown.RingdownSpec or None
Issue #1254 — complete the wire-port S-parameters of a record
cut while the structure still rings. The slow poles are
identified on [window_start * T, T] of the port voltage and
current (the source must be off there) and the unrecorded tail
of each spectrum is added in closed form. The result is
Result.ringdown (.s_params on the bins of
Result.s_params, .report with the window, the poles and
the witnesses: WE when available (otherwise W2), no growing
pole, passivity, source off). Every other output is the one the
same run gives without ringdown=. Wire ports
(add_port(..., extent=...)) on the uniform (one port) and
graded-mesh lanes, fixed record length; other ports, TFSF, Kerr,
devices= and until_decay are refused with the reason.
until_identified : bool
Issue #1254 — with ringdown=, end the run once the completion
is witnessed instead of at n_steps, which becomes the longest
record allowed. The record is checked on a geometric schedule
(every check at least 1.25x the record of the last); the run
stops at the first check where every source is off over
[T/4, T], the error witness WE is within witness_tol
there and at the check before, the record is at least half the
amplitude decay time Q / (pi f) of the slowest mode whose own
unrecorded tail moves the completed S by witness_tol (a static
field left in a closed box is left out; the largest such decay
time over the checks the two-in-a-row rule rests on), and the
completion reads passive with no growing pole. The result is the
one run(n_steps=T, ringdown=...) returns for the stopping
record T, with the stop report (every check's record, WE,
the pole that set the floor, the conditions and the check's wall
time) in Result.ringdown.stop. Each check identifies the
record twice (0.1-3 s on a CPU for the test cavity); on a small
model that costs more than the steps it saves. A weakly coupled
high-Q mode holds the floor for half its own decay time, which
can be longer than n_steps. When no check
stops the run, the whole n_steps record is completed and the
report names the condition that failed. Refused without
ringdown=, with until_decay, snapshot=, DFT planes
and flux monitors.
Returns
Result
Return or write the native runtime scene artifact.
path=None returns a JSON-serializable dict. Otherwise the scene is
written as indented JSON and the output ~pathlib.Path is
returned.
Return a runtime artifact report dict for this simulation.
Write a runtime artifact bundle and return ArtifactBundle.
Estimate reverse-mode AD memory for this simulation.
Returns a static-estimate AD_MemoryEstimate with forward, checkpointed-AD, and non-checkpointed-AD sizes in GB, plus a best-effort warning if estimated AD memory exceeds 85% of available VRAM. This artifact is planning evidence, not a certificate of peak runtime memory.
When checkpoint_every is provided, the returned
ad_segmented_gb reflects the non-uniform segmented scan-of-scan
chunk-size path from issue #31. When checkpoint_segments is
provided, it reflects the uniform segmented-scan segment-count path
from issue #73.
The segmented model (issue #277) counts BOTH terms of the rematerialized backward pass:
- segment-boundary storage — carry + cotangent per active segment,
2 * active_segments * field_bytes; - the live-segment rematerialization tape — the backward pass
replays one segment at a time, so that segment's per-step field
tape (
n_steps // checkpoint_segmentssteps on the uniform path,checkpoint_everysteps on the chunked path) is resident on top of the boundary storage. The non-uniform runner pads the trailing chunk with zero source to a full chunk, so the full chunk length is the correct live-tape size; both live terms are capped at the active (post-warmup) step count.
The segmented estimate is therefore minimized when the two terms
balance, near sqrt(2 * n_steps) steps per segment — NOT by
pushing the segment count to either extreme. Use
plan_ad_memory() to pick a balanced knob for a budget.
n_warmup must be an integer with
0 <= n_warmup < n_steps and reduces reported reverse-mode tape
time. The legacy
ad_checkpointed_gb field keeps its old (optimistic) heuristic
for backwards compatibility; it
is NOT accurate for FDTD on the non-uniform path — see the
class docstring.
Explain the static reverse-mode AD memory estimate.
This method uses the same conservative accounting as
estimate_ad_memory(), then decomposes the selected AD memory
number into named contributors. It is meant to answer "what is making
this AD run large?" without claiming profiler evidence or a runtime
peak bound.
Choose a segmented-AD memory plan for a memory budget.
The planner reuses estimate_ad_memory() and returns a
calibrated conservative planning artifact rather than mutating the
simulation or certifying runtime peak memory. If ordinary reverse-mode
AD already fits under target_fraction * available_memory_gb, both
checkpoint knobs are None. Otherwise it returns a segmented
candidate: checkpoint_every for non-uniform grids or
checkpoint_segments for uniform grids. Only wire the candidate when
segmented_fits is true; a non-fitting plan keeps the least-memory
candidate as diagnostics.
n_warmup must be an integer with 0 <= n_warmup < n_steps and
reduces active reverse-mode time. Raw estimates must also fit after
multiplying by safety_factor before fit flags are set, so
near-boundary plans stay conservative.
Return a one-call AD-memory preflight planning artifact.
The report composes existing static memory planning, static explainability, optional mesh advisories, and optional trace-time JAX saved-residual diagnostics. It does not run FDTD and does not upgrade static planning evidence into runtime memory proof.
Return a fail-closed compiler-memory certificate for one executable.
This method accepts an already-compiled JAX object and reads only its
memory_analysis() output. It does not compile callables, run FDTD,
profile runtime memory, or convert static memory plans into guarantees.
Green statuses are bounded to the supplied compiled object and complete
exact-scope metadata.
Compute a theoretically clean axis-normal boundary-aperture waveguide S-matrix.
Requires solver="yee". On a uniform mesh, precision selects
field storage for every device and reference run, as on run().
A non-uniform mesh supports only precision="float32".
Parameters
num_periods : float
Length of the FDTD run (in source-period multiples) used to
derive n_steps when n_steps is not given. The
spectra are computed POST-SCAN from the recorded modal V/I
time series via a rectangular full-record DFT (matching
OpenEMS's utilities.DFT_time2freq); num_periods
therefore governs both the CPML drain horizon AND the DFT
integration window. Phase 2 cleanup (2026-04-25) removed
the legacy num_periods_dft early-gate knob — the rect
full-record DFT is finite-energy on the recorded transient
so no gating is needed even on strong reflectors.
normalize : bool or "flux"
Controls the S-parameter extraction algorithm:
``False`` (default) — modal V/I decomposition, no reference
run. Magnitude includes Yee impedance mismatch
(Z_TE_num/Z_TE_exact ≈ 3 % at dx/λ = 0.07). Use for
|S11| of strong reflectors (PEC short, high-Q resonators)
where this error is smaller than the ±10–20 % round-trip
dispersion error introduced by ``normalize=True``. On the
differentiable chain: a traced ``eps_override`` /
``sigma_override`` flows through it.
``True`` — two-run modal normalization. Cancels one-way
Yee dispersion for **transmission** (off-diagonal) by
dividing device outgoing waves by reference outgoing waves
at the same port. **Does not** cancel dispersion for
reflection (round-trip vs one-way path mismatch); use
``normalize=False`` or ``normalize="flux"`` for S11 of
strong reflectors. Host-assembled and **outside the
differentiable chain**: a traced ``eps_override`` /
``sigma_override`` (``jax.grad``, ``jax.jit``) raises
``NotImplementedError`` at this call, before any FDTD run;
a concrete override still runs forward.
``"flux"`` — hybrid power-flux extraction. Magnitude from
Poynting-vector DFT (|S|² = P_flux / P_inc), phase from
modal V/I. Corrects both the Z_TE impedance-mismatch error
in S11 and the round-trip dispersion error in the
``normalize=True`` diagonal formula. Costs 2 × N_ports
FDTD runs (same as ``normalize=True``). On the
differentiable chain like ``False``. On the uniform lane the
result promotes field, frequency, and JAX default float precision,
like ``False``: complex64 with x64 off and at least complex128 with
x64 on, including explicit float32 frequencies and float32/mixed fields.
Modal records retain at least the frequency and default float precision.
The non-uniform lane's result follows the ``freqs`` precision:
complex64 by default, complex128 under ``JAX_ENABLE_X64``.
**Reference impedance.** On ``False`` and ``True`` each
S_ij is the modal voltage-wave ratio ``b_i / a_j``, where
the waves at a port are decomposed with that port's OWN
discrete modal impedance (Yee-discrete Z_TE or Z_TM from
the port's cutoff and cell size); no ``sqrt(Z_i / Z_j)``
renormalization is applied. ``"flux"`` takes |S_ij| from
the power ratio and its phase from the same modal waves.
The returned matrix therefore equals a power-wave S only
when every port shares one cross-section and mode — the
validated scope (straight guides and same-guide junctions).
Ports with dissimilar cross-sections are outside it.
**Multimode.** ``n_modes > 1`` on any port is assembled on
the host for every ``normalize`` value and is outside the
differentiable chain; ``eps_override`` / ``sigma_override``
differentiation is single-mode only.
checkpoint_segments : int or None
Segmented gradient checkpointing for the uniform waveguide
AD path (issue #73 / PR #125). Splits the n_steps scan
into K segments that are rematerialised via
jax.checkpoint during the backward pass, reducing peak
reverse-mode memory from O(n_steps·|carry|) to
O((K + n_steps/K)·|carry|) (≈ O(√n_steps·|carry|) at the
optimal K ≈ √n_steps, at ≈ 2× backward compute cost).
K is forwarded to rfx.simulation.run with
checkpoint=True; K MUST exactly divide the
auto-computed n_steps (the runner rejects non-divisors —
choose the nearest divisor of √n_steps; padding is rejected
because it would shift the V/I DFT windows). Default
None is byte-identical to the pre-checkpoint scan.
On a NON-uniform mesh (``dx_profile`` / ``dy_profile`` /
``dz_profile``, issue #73) ``checkpoint_segments=K`` is now supported: ``K`` is
translated to the NU runner's ``checkpoint_every`` chunk size — the
divisor of ``n_steps`` nearest to ``n_steps/K`` — and applied to the
*device* run only (the vacuum reference is constant in the design
variable). The chunk MUST divide ``n_steps`` (same as the uniform
path): a non-divisor chunk would let the NU runner's zero-padding add
spurious ring-down steps to the carry-accumulated flux DFT and shift
the S-matrix. With an exact divisor the result is forward-IDENTICAL
and the ≈O(√n_steps) tape reduction is realised under ``jax.grad``
with ``normalize='flux'`` + an ``eps_override`` / ``sigma_override``
design variable.
port_reference_sims : list[Simulation] or None
Per-driven-port matched-straight-guide reference simulations for
interior-PEC multi-port structures (T-junctions, branches,
septa). port_reference_sims[i] is a Simulation describing
the STRAIGHT continuation of driven port i's guide with no
junction — same domain / dx / boundary, geometry = port
i's guide walls extended straight through. Only valid with
normalize='flux' (raises otherwise); single-mode ports only;
uniform mesh only; not combinable with eps_override /
sigma_override.
**Multi-port junctions (port_reference_sims).** The default flux
path references incident power ``P_inc`` to a single shared VACUUM
run (empty domain, no interior PEC). For a straight guide the walls
come from ``pec_axes`` so that vacuum reference already carries the
guided ``P_inc`` correctly. For a junction the interior PEC septum /
branch is stripped from the vacuum reference, which then radiates
into free space: ``P_inc`` is mis-normalized and every ``|S|``
inflates hard (non-passive; ``normalize=True`` gave max|S|~230,
``normalize='flux'`` gave max|S|~9.8, |S11|~1.9 on the verified
compact 3-port T-junction). Passing ``port_reference_sims[i]`` = the
matched straight guide for driven arm ``i`` moves ``P_inc`` toward
the true single-mode guided incident power (PEC-folded identically
to the device run).
**Far-port discipline (required for a physical S-matrix).** This
plumbing is NECESSARY but NOT SUFFICIENT. A physical junction
S-matrix additionally requires: (1) each port's probe plane placed
>= 5 evanescent decay lengths of the next higher mode away from the
junction; (2) CPML thickness >= ~0.5 guide wavelengths at band
centre; (3) a converged mesh. On far-port geometry (arms 90/90/70 mm,
48 mm CPML, dx 1.0/0.667 mm) the matched-reference flux path reaches
passivity 1.006/1.002, reciprocity 0.001, mesh-convergence 0.0297
and 0.087 vs MEEP (r2000, cross-device). On COMPACT geometry the
matched reference fixes |S11| (1.86 → 0.49, physical) but the overall
matrix stays non-physical (residual max|S|~3.9); the two in-method
advisories (probe clearance, CPML thickness) warn when the far-port
discipline is not met.
The absorber half of that discipline is checked on **every**
uniform path, not just this one: an in-method advisory (issue
#494) fires whenever the absorber on a port's propagation axis is
thinner than ``0.5 * lambda_g`` at the **lowest** measured
frequency, which is where ``lambda_g`` is longest and the
``cpml_layers=16`` default weakest. It is checked in-method
because the functional entry points run no ``sim.preflight()``.
Treat ``0.5 * lambda_g`` as a floor, not a target: at the WR-90
band edge the measured residual ``|S11|`` ripple was 0.0706 at
0.30 ``lambda_g``, 0.0366 at 0.50 and 0.0093 at 0.75, so a
0.5-``lambda_g`` absorber can still set the accuracy envelope
instead of discretization. The advisory is silent on a
non-uniform mesh (``cpml_layers * dx`` is ambiguous under a
graded profile) and on a band that starts at or below cutoff
(``lambda_g`` is undefined there; the ``port_freqs_below_cutoff``
preflight owns that case).
This enables junction measurements UNDER the documented
discipline; it does NOT make arbitrary compact junctions
valid. Consult ``docs/guides/sparameter_support_matrix.md`` for
the current waveguide evidence and scope.
Mixed-family S-matrix: lumped/wire ports + MSL ports (issue #488).
End-to-end S-parameters on ONE structure carrying two port
families — the first supported pair is a homogeneous lumped OR
wire set (add_port) together with MSL ports (add_msl_port),
e.g. a vertical probe feed launching onto a microstrip line.
Each port is driven in turn (lumped/wire ports first, then MSL
ports, registration order within each family); the non-driven
lumped/wire ports remain physical matched resistor loads and the
non-driven MSL ports are passive probe columns. Extraction reuses
the validated per-family wave machinery unchanged and combines the
waves in the Kurokawa power-wave convention (each wave divided
by sqrt(Re Z0) of its own port) — with unequal reference
impedances across families a pseudo-wave ratio would be off by
sqrt(Z_j/Z_i) (issue #460); reciprocity of a reciprocal
structure is the committed internal falsifier for this choice.
MAGNITUDE CHANNEL (magnitude_channel, default "flux"):
the #488 falsifier battery + an independent Poynting-flux referee
measured the port-cell V*I accounting undercounting delivered
power ~3x at a wire probe feed (the OPEN issue-#313 class) and
the analytic Hammerstad-Jensen Z0 anchor diverging from the
measured line ratio on an interface-aligned mesh. Off-diagonal
MAGNITUDES are therefore taken from Poynting flux by default:
auto-registered surfaces (a closed 5-face box around each
lumped/wire port over the z-lo ground; a full cross-section plane
at each MSL port's probe-0 x) give per-drive net powers, and
|S_ij|^2 = P_arrive,i / (P_net,j / (1 - |S_jj|^2))
where S_jj is the per-family diagonal supplying the reflection
correction; no Z0 anchor enters the magnitude. Off-diagonal PHASE
still comes from the wave channel (see below).
magnitude_channel="wave" keeps the raw power-wave magnitudes
(diagnostic; carries the #313 deflation at lumped/wire ports).
HONESTY NOTES (read before quoting numbers):
Neither diagonal is verified on this lane, and the returned diagonal is not always the measured one. Two separate findings: (a) the wire port-cell V*I accounting was measured undercounting delivered power ~3x against an independent Poynting referee (the open issue #313 reaching the diagonal at a near-field-dominated vertical probe), and on an end-fed fixture the MSL probe plane's local
V/Iwas ~591 ohm and strongly reactive while the reported|S22|was 0.03 — those cannot both be right. (b) Withenforce_passivity=True(the default),_project_passiveis a JOINT SVD clip: when any entry is non-passive it rewrites others as a side effect. Measured on the committed test fixture, the shipped MSL diagonal came out ~4x its unprojected value. Soresult.S's diagonal is a projected quantity, not a raw per-family measurement — readS_rawandpassivity_correctionfor what was actually measured, and treat any diagonal-derived conclusion accordingly.The flux magnitudes inherit a flux-accounting envelope (box leakage + finite DFT; the Phase-0 referee class measured ~1.3% on the canonical thru) and an ill-conditioning guard: when
1 - |S_jj|^2is small (near-total reflection at the driven port) the normalization is unreliable and a warning names the column.- A per-column power sum near 1 is NOT evidence on this
channel. Substituting the definition gives
sum_i |S_ij|^2 = |S_jj|^2 + (P_arr/P_net)(1 - |S_jj|^2), which is identically 1 whenever the arriving power equals the net launched power — for ANY value of the diagonal. The flux normalization makes passivity an identity, not a measurement, so do not quote column power as a passivity check here (it still detects power GAIN, i.e.P_arr > P_net). The independent internal check on this channel is RECIPROCITY:S_ijandS_jicome from different runs with different normalizations, so their agreement is real evidence. It runs automatically on every extraction (seereciprocity_tol), audits the RAW matrix, and is the check that a wrong diagonal trips. - With
magnitude_channel="wave", off-diagonal magnitudes that RECEIVE at a lumped/wire port cell inherit the issue-#313 near-field deflation of the default port-cell waves. The returneds21_power_witnesscross-checks the MSL-receiving direction against a delivered-power normalization; quote it alongside|S|. - Cross-family off-diagonal PHASE mixes two reference-plane conventions (port cell vs de-embedded MSL probe plane) and a component-mixing ±1 (probes.py sign-convention fence); magnitude is the validated observable.
settling_dbis the ring-down witness (above −40 dB = truncation suspect); preflight output is part of the result.
v1 restrictions (loud NotImplementedError): uniform mesh only,
no waveguide/Floquet/coax/TFSF registrations, no bare sources or
0-ohm ports (they would fire in every drive run), no
reference_plane_cells wire ports, no mixed lumped+wire set
(same fence as the production scan driver), imperative only (no
eps_override AD channel). The default "flux" channel adds
two more, because the per-port flux box omits its bottom face and
treats the port extent as a height: a PEC z_lo boundary
and vertical (component="ez") lumped/wire ports are
required. magnitude_channel="wave" makes neither assumption.
Parameters
reciprocity_tol : float, default 0.06
Relative |S_ij| vs |S_ji| disagreement above which the
reciprocity witness warns. Deliberately set BELOW the 9%
residual measured on this lane's own reference fixture, so
that fixture warns rather than passing silently: a tolerance
above the known residual would document the check and never
fire it.
Returns
MixedSMatrixResult
Compute S-parameters on whichever lane this simulation's ports select.
A single entry point over the per-family calculators. It inspects the
registrations, chooses exactly one delegate, and forwards **kwargs
to it UNCHANGED -- no default is invented here, so the delegate's
signature, defaults, preconditions, warnings and return type are exactly
what you would get calling it directly.
Dispatch table
=================================== ========================================== ================================
Registered Delegate Returns
=================================== ========================================== ================================
waveguide ports only compute_waveguide_s_matrix WaveguideSMatrixResult
MSL ports only compute_msl_s_matrix MSLSMatrixResult
1 coaxial port only compute_coaxial_line_reflection or CoaxialLineReflectionResult
compute_coaxial_two_port -- ambiguous, or CoaxialTwoPortResult
pass lane=
1 coaxial + 1 MSL compute_coax_msl_transition CoaxMSLTransitionResult
lumped/wire (add_port) + MSL compute_mixed_s_matrix MixedSMatrixResult
lumped/wire only NotImplementedError -- the run lane (Result.s_params)
nothing ValueError --
anything else NotImplementedError --
=================================== ========================================== ================================
The lumped/wire family deliberately raises rather than calling run()
for you: that lane returns a Result (not an S-matrix result type),
and n_steps has no default there, so quietly substituting one would
be inventing exactly the kind of default this dispatcher refuses to
invent. Run it yourself::
res = sim.run(n_steps=..., compute_s_params=True)
res.s_params
Parameters
lane : str or None
Explicit lane name (see s_matrix_lane()). Consumed here, never
forwarded. Required for a single coaxial port, where
compute_coaxial_line_reflection and compute_coaxial_two_port
share one registration footprint; optional elsewhere, where it is
checked against the registrations rather than overriding them.
**kwargs
Forwarded verbatim to the chosen delegate.
Returns
Whatever the chosen delegate returns -- see the table.
Raises
ValueError
No ports registered, or lane= contradicts the registrations.
Also raised by the chosen lane itself: compute_coax_msl_transition
(1 coaxial + 1 MSL) refuses a non-passive extracted S by default and
is the only lane in the table that does — forward
strict_passivity=False through **kwargs to get the diagnostic
matrix with a warning instead (issue #838).
NotImplementedError
Lumped/wire-only (use the run lane), a single coaxial port with no
lane=, or a combination of port families no calculator covers.
TypeError
An unrecognised keyword argument. **kwargs swallows it here, so
the message names the DELEGATE that rejected it --
Simulation.compute_waveguide_s_matrix() got an unexpected keyword
argument 'foo' -- which also tells you which lane was chosen.
Notes
Preflight behaviour is inherited from the chosen lane and is not uniform
across the table: compute_msl_s_matrix and compute_mixed_s_matrix
run preflight automatically, the waveguide and coaxial lanes do not. See
each delegate's row in
tests/unit/preflight/test_preflight_advisory_emission_contract.py.
See Also
s_matrix_lane : the same choice, without running anything.
Which S-matrix lane this simulation's registrations select.
Runs no FDTD and touches no grid: it reads the port registries only, so
it is cheap to call on a half-built Simulation and it is what the
dispatch table's tests assert against.
Parameters
lane : str or None
Optional explicit lane name, one of the compute_* names below or
"run(compute_s_params=True)". Required on the single-coaxial-port
row, where two lanes share one registration footprint (see the module
docstring). On every other row it is checked against the lane the
registrations determine and raises ValueError if it disagrees --
it can select a lane, never override the physics fence.
Returns
str
The delegate's method name, e.g. "compute_waveguide_s_matrix", or
the literal "run(compute_s_params=True)" for the lumped/wire
family, which has no compute_* method.
Raises
ValueError
No ports registered at all (the message lists the add_*_port
builders), or a lane= that contradicts the registrations.
NotImplementedError
A single coaxial port (ambiguous -- pass lane=), or any
combination of families no calculator covers. The message names the
registered families with their counts.
Examples
>>> sim.add_waveguide_port(...); sim.add_waveguide_port(...)
>>> sim.s_matrix_lane()
'compute_waveguide_s_matrix'
Run a compiled FDTD simulation via jax.lax.scan.
Parameters
grid : Grid
materials : MaterialArrays
n_steps : int
gradient : {"autodiff", "adjoint"}
Adjoint requires a uniform real-valued design box. It stores local
DFTs instead of checkpoints; checkpoint options are unused.
See the module docstring of rfx.adjoint.
boundary : "pec", "cpml", or "upml"
cpml_axes : str or None: the grid's absorbing axes when omitted; subsets accepted
pec_axes : axes string or None
Axes on which to enforce PEC after each update. If None, uses
all non-periodic axes.
periodic : (bool, bool, bool) or None
Per-axis periodic boundary flags. If None, defaults to
(False, False, False) in 3D and (False, False, True)
in 2D modes.
debye : (DebyeCoeffs, DebyeState) tuple, or None
lorentz : (LorentzCoeffs, LorentzState) tuple, or None
tfsf : (TFSFConfig, TFSFState) tuple, or None
Optional total-field/scattered-field plane-wave source. When
provided, the 1D auxiliary FDTD is interleaved with the 3D Yee
updates using the Taflove leapfrog ordering.
sources : list of SourceSpec (precomputed waveforms)
probes : list of ProbeSpec (point time-series recorders)
dft_planes : list of DFTPlaneProbe or None
Optional running frequency-domain plane probes.
waveguide_ports : list of WaveguidePortConfig or None
Optional waveguide-port source/probe configs.
ntff : NTFFBox or None
If provided, accumulate near-to-far-field DFT on a Huygens box.
snapshot : SnapshotSpec or None
If provided, record a frame after every snapshot.interval-th
step (n_steps // interval frames; see SnapshotSpec), with
their positions and times in SimResult.snapshot_axes. Recording
does not change any other output: the time series, DFT and port
accumulators and the final state are bit-identical to the same run
without a snapshot. interval=1 records inside the step body, as
it always has; a larger interval runs the steps in blocks of
interval (lax.scan over blocks of an inner lax.scan over
steps, plus a scan over the remaining n_steps % interval) and
records after each block, so the step body is the one a run without
a snapshot compiles. The remainder scan is a second compile of the
step body; a remainder of ONE step is instead joined with the last
step of the last block into a two-step scan (a one-step scan is
inlined by XLA and changed the last bit), leaving the rest of that
block as a scan of interval - 1 steps. With report_every a
chunk that does not start on a multiple of the interval adds a short
head scan, and each distinct piece length compiles once
(rfx.snapshots.plan_snapshot_pieces lays the pieces out). With
checkpoint_segments the interval must divide the segment length
n_steps // checkpoint_segments.
checkpoint : bool
If True, wrap the scan body with jax.checkpoint to
trade compute for memory during reverse-mode AD. Reduces
backward-pass memory from O(n_steps) to O(1) per step.
return_state : bool
If False, do not expose the final FDTD state in the result.
This shrinks the differentiable output surface for optimisation.
report_every : int or None
Issue #667. When set, emit one [PROGRESS] ... line to stdout
every N steps: steps done / total, wall elapsed, implied rate and
ETA. None (default) is OFF and runs the unchanged single-scan
code path, so existing callers are byte-identical by construction.
When set, the same compiled scan is driven from the host in
``report_every``-step chunks with ``carry`` threaded through, so the
result is a continuation and not a re-solve; the DFT accumulators
and every port / flux / monitor state live in that carry.
Bit-exactness against ``report_every=None`` is locked by
``tests/unit/runners/test_run_progress_reporting.py``.
Costs, measured rather than hidden: each report inserts one device
synchronisation (without it the host would dispatch every chunk at
once and print a fabricated rate), all full chunks share one XLA
executable while a ragged final chunk compiles once more, and the
per-chunk outputs are joined once at the end with a bounded-arity
grouped concatenate (a flat one takes an argument per chunk, which
cost 232 s at 22,500 chunks against 0.99 s grouped).
Forward-only: it reads the host wall clock, so it raises under
``jax.jit``/``grad``/``vmap`` and is rejected together with
``checkpoint_segments``. The trace check inspects the carry, the
per-step ``xs`` and the material / geometry arrays — the routes
``forward()`` and ``optimize()`` actually trace through — rather
than asking JAX whether a trace is active, which its current API
does not expose. A tracer reaching the scan body by some other
closure alone would print trace-time lines; the computed values
stay byte-identical either way.
report_label : str
Short tag prefixed to each progress line, e.g. "MSL drive p1",
so the per-drive solves of one compute_*_s_matrix call are
distinguishable in a log. Ignored when report_every is None.
design_box : DesignBoxSpec or None
Issue #1179. One static box whose E update is redone from its own
(usually traced) permittivity, leaving materials constant so the
reverse-mode tape holds box-shaped arrays instead of grid-shaped
ones. None (default) is the unchanged path.
``_resolve_design_box`` rejects, from what this function resolves:
UPML, Debye/Lorentz, anisotropic eps, Kerr, ``stencil_order=4``, the
Bloch path, a box reaching into the CPML absorber, and a box holding
a source, magnetic source, lumped/wire S-param port or RLC cell or a
surface-impedance sheet edge. It does NOT carry the checks that need
the API object: the lane (non-uniform, distributed, ADI), the
collision with ``eps_override`` / ``sigma_override`` /
``mu_r_override``, ``pec_occupancy_override``, a PASSIVE port (it
leaves neither a source nor an accumulator here), and the promotion
of ``materials.eps_r`` to the design dtype. Those are
``Simulation.forward``'s, in ``_resolve_design_box_override``; a
caller reaching this function directly owns them.
design_occupancy : DesignOccupancySpec or None
Issue #1183. One static box whose PEC OCCUPANCY is the traced
quantity, for the same reason and with the same effect on the tape:
the 1 - M scaling is redone on the box's window from the field
before the grid-wide apply_pec_occupancy, so what the backward
pass keeps per step is window-shaped. None (default) is the
unchanged path. The box's occupancy REPLACES pec_occupancy at
the box cells; outside it, pec_occupancy is carried.
``_resolve_design_occupancy`` rejects a periodic axis, an empty box
and a box off the grid. ``Simulation.forward`` owns the lane and the
collision with ``design_eps_override``.
stop_fn : callable or None
Issue #1254 (Simulation.run(..., until_identified=True)). When
given, the scan runs in chunks of stop_interval steps through
rfx.progress.scan_with_progress(), the carry threaded through,
and stop_fn(steps_done, peek) is called after each chunk;
peek() returns the record so far as a namespace with
time_series ((steps_done, n_probes)), wire_port_sparams
(the (spec, accumulators) pairs at that step, as this function
returns them), grid and dt. A true return ends the run
there, and every output is the one a run of steps_done steps
returns (the chunked scan is a continuation of the single one).
Refused with a snapshot or checkpoint_segments. None
(default) is the unchanged path.
stop_interval : int
Chunk length of the stop_fn loop (default 250). Ignored when
stop_fn is None.
Returns
SimResult with final state, time series, and optional NTFF data.
Run simulation until field energy decays to decay_by of peak.
Runs compiled scan chunks with host-side decay checks at the historical step indices (0, check_interval, ...).
Parameters
decay_by : float
Stop when |field|^2 < decay_by * peak|field|^2.
check_interval : int
Check decay every N steps.
min_steps : int
Always run at least this many steps.
max_steps : int
Hard upper limit on steps.
decay_energy_consecutive : int
On absorbing boundaries (cpml / upml) the stop uses the
total interior-domain energy criterion (issue #169). It fires only
after the interior energy U has stayed below decay_by * peak_U
on this many consecutive checks. Default 2 (MANDATORY minimum):
the interior energy of a guided geometry is not null-free, dipping
through transient inter-packet minima that recover; a single below-
threshold check can false-fire on such a dip, so >= 2 is required
to absorb the chatter. Has no effect on closed/PEC boundaries (which
use the instantaneous point-field fallback).
radiated_flux_box : tuple or None
Opt-in (#388) RADIATED-FLUX stop for absorbing boundaries: a Huygens box
((x_lo, y_lo, z_lo), (x_hi, y_hi, z_hi)) in physical coordinates
enclosing the structure (clear of the CPML). When set, the stop uses the
outgoing Poynting flux through the box instead of the interior energy —
a radiation-settling criterion that ignores non-radiating trapped
energy (the static soft-source charge that FLOORS the energy criterion,
and near-Nyquist grid buzz), because neither carries net radiated power.
Appropriate for radiation / S-parameter measurements; the flux peak is
tracked from the first check (the radiation peaks during the source
drive). Default None keeps the interior-energy criterion, unchanged.
flux_env_checks : int
Number of recent checks whose |flux| max forms the envelope used by
the radiated-flux stop, smoothing the 2f0 Poynting oscillation. Default 4.
monitor_component : str
Field component to monitor ("ez", "hy", etc.). Used only by the
closed/PEC point-field fallback stop.
monitor_position : tuple or None
Physical position (x, y, z) to monitor. If None, use center of
the domain.
checkpoint_segments : int or None
Not supported on the decay path. run_until_decay uses a
host-driven early stop, so scan-level gradient checkpointing does
not apply. Passing a non-None value raises
NotImplementedError.
Notes
Differences from run():
checkpoint_segmentsis not supported (raisesNotImplementedErrorwhen notNone).snapshotrecords a frame after everysnapshot.interval-th step of the loop, asrun()does (#1258):actual_steps // intervalframes, positions and times inSimResult.snapshot_axes. Reading the frame does not touch the stepped state.checkpoint(jax.checkpointgradient tape) is accepted but silently ignored — gradient checkpointing has no effect on the host-driven decay path.- Flux-monitor DFT accumulation uses a rectangular (no-window) weight
instead of the streaming Hann window used by
run(). Flux values from the two paths are therefore not numerically identical even for the same step count.
The stop criterion depends on the boundary (issue #169, RESOLVED for absorbing boundaries).
Absorbing boundaries (
cpml/upml) — the stop is gated on the total interior-domain energyU = sum(E^2 + H^2)over the non-CPML interior slice, declared decayed onceU < decay_by * peak_Uondecay_energy_consecutiveconsecutive checks. Because it is a whole-domain energy, it does not pass through the per-cell interference nulls that the old single-cell point-field stopper hit between slow-tail wave packets, so it is suitable for flux / S-parameter / transmission measurements on guided / low-loss geometries. (On the cv03-class eps=12 guide it now stops at the flux-converged transmission within ~0.13%, vs the prior ~7% under-run of the point-field stop.)decay_energy_consecutive >= 2is mandatory because the interior energy is not null-free — it dips through transient inter-packet minima that recover, so a single below-threshold check can false-fire.Closed / PEC boundaries — domain energy does not decay in a lossless closed cavity, so the stop falls back to the historical instantaneous squared field at the single
monitor_componentcell (val_sq < decay_by * peak_sq). This fallback retains the original limitation: it is a valid decay witness only for lossy / radiating structures with a clean ring-down envelope, and is not suitable for flux / S-parameter / transmission gating on guided / low-loss closed geometries — for those use a fixedn_stepsviarun().
report_every : int or None
Issue #667. When set, emit one [PROGRESS] ... line every N
steps, at the first decay-chunk boundary at or past the interval,
plus a final line at the actual stop step. None (default) is OFF.
Reporting never changes the chunks or the decay-check cadence.
The denominator is max_steps, i.e. a CAP:
the line marks it (cap) and the ETA is an upper bound, because a
decay stop can fire at any check.
report_label : str
Short tag prefixed to each progress line. Ignored when
report_every is None.
Returns
SimResult
Run non-uniform FDTD via jax.lax.scan.
Parameters
cpml_axes : str or None: axes with absorber pads when omitted; subsets accepted sources : list of (i, j, k, component, waveform_array) probes : list of (i, j, k, component) wire_ports : list of dict with keys: mid_i, mid_j, mid_k, component, impedance, waveform_array s_param_freqs : (n_freqs,) array for S-param DFT debye : (DebyeCoeffs, DebyeState) or None lorentz : (LorentzCoeffs, LorentzState) or None dft_planes : list of DFTPlaneProbe or None Frequency-domain plane accumulators. The accumulation is identical to the uniform path (acc += field * exp(-j2pi f t) * dt); dt is scalar on both paths so no per-axis weighting is required.
Create a SourceSpec by precomputing a waveform function (raw E-field add).
WARNING: raw field source causes DC accumulation on PEC surfaces.
Prefer make_j_source for resonance detection.
Native amplitude convention (issue #571): 'field' — E += w.
amplitude_kind='current' uses the shared source-table builder's
Cb/dV coefficient and REQUIRES
materials (for Cb at the source cell). amplitude_kind of
None or 'field' is bit-identical to the historical output (no
multiply is applied at all). materials is accepted and unused in
that legacy/native case.
Create a ProbeSpec from a physical position.
Create a SourceSpec for a lumped port (Cb-corrected waveform).
The port impedance must already be folded into materials via
setup_lumped_port().
Create a properly normalized current source for non-uniform grid.
The waveform specifies CURRENT (Amperes). The E-field addition is:
E += (dt/ε) × I_source / dV
where dV is the E node's control volume: the PRIMAL per-cell width on
the component's own axis and the DUAL spacing (d[k-1]+d[k])/2 on the
two transverse axes (issue #672). On a uniform mesh the two coincide and
this is the familiar dx × dy × dz.
This gives resolution-independent injected POWER regardless of cell size. Same approach as Meep's internal source normalization.
Native amplitude convention (issue #571): 'current' —
amplitude_kind of None or 'current' is bit-identical to the
historical output (no extra multiply). amplitude_kind='field'
returns the waveform directly, without reading a material coefficient.
The shared builder preserves the current drive's differentiable Cb/dV.
Compiled simulation output.
time_series : (n_steps, n_probes) float array, or (n_steps, 0) if
no probes were specified.
ntff_data : NTFFData or None
Accumulated near-to-far-field DFT data (if NTFF box was used).
dft_planes : tuple[DFTPlaneProbe, ...] or None
Final accumulated DFT plane probes.
waveguide_ports : tuple[WaveguidePortConfig, ...] or None
Final accumulated waveguide-port configs.
wire_port_sparams : tuple | None
Final V/I/V_inc DFT accumulators for wire port S-params.
lumped_port_sparams : tuple | None
Final V/I DFT accumulators for lumped port S-params (issue #72).
Each entry is (LumpedPortSParamSpec, (v_dft, i_dft, v_ref_dft));
v/i are POST-injection (i with the Yee half-step phase) and
v_ref is the PRE-injection drive sample.
wire_refplane_sparams : tuple | None
Final reference-plane V/I DFT accumulators for the opt-in wire
S-matrix plane path (issue #313). Each entry is
(WireRefPlaneSpec, (v_dft, i_minus_dft, i_plus_dft)); two
entries per opted port (plane slots 0 and 1).
snapshots : dict[str, ndarray] or None
Field snapshots keyed by component name, each
(n_steps // interval, ...); see SnapshotSpec.
ntff_box : NTFFBox or None
NTFF box specification used for accumulation.
grid : Grid or None
Grid metadata needed by post-processing / objective helpers.
snapshot_axes : dict[str, rfx.snapshots.SnapshotAxes] or None
Per snapshot component: sample coordinates in metres, the slice
plane, and the step count and time of each frame (#1259). None
when no snapshot was recorded.
dt : float or None
The time step the scan actually advanced by, in seconds. It is
grid.dt except under stencil_order=4, whose step is derated
by _ORDER4_CFL_FACTOR; the time series and every frame are
sampled at this step, so time and frequency must be read with it,
not with grid.dt.
current_moment_data : (accumulator, kahan) or None
Block current moments accumulated inside the time loop
(rfx.current_moments). The accumulator is
(n_freqs, n_blocks, 3, n_weights).
current_moment_monitor : CurrentMomentMonitor or None
The slab, block map and centres those moments belong to.
Create new instance of SimResult(state, time_series, ntff_data, dft_planes, flux_monitors, waveguide_ports, wire_port_sparams, lumped_port_sparams, snapshots, ntff_box, grid, wire_refplane_sparams, snapshot_axes, dt, current_moment_data, current_moment_monitor, adjoint_settling, sparam_time_records, dft_time_records)
Mid-simulation field snapshot specification.
interval : int
Record a frame every interval time steps; an integer >= 1, anything
else is refused. Counting is 1-based over completed steps: a run of
n_steps records n_steps // interval frames, frame k
(0-based) holding the fields after step (k + 1) * interval, i.e.
after steps interval, 2*interval, .... The last
n_steps % interval steps record no frame. interval=1 records
every step. Memory scales with the frame count.
components : tuple of str
Field components to capture, e.g. ("ez",) or ("ex", "hy").
slice_axis : int or None
0, 1 or 2. If not None and slice_index is not None, capture a 2-D
slice at slice_index along this axis instead of the full 3-D field
(saves memory). With slice_index None the full field is recorded.
slice_index : int or None
PADDED lattice index along slice_axis: the same index space as
grid.shape and grid.position_to_index, CPML cells counted
(grid.position_to_index(pos)[axis] gives the index of a physical
position).
Where each recorded sample sits (metres) and when each frame was taken
(seconds): Result.snapshot_axes after a run, or
rfx.snapshot_axes(grid, spec, n_steps) before one (rfx/snapshots.py
states the lattice layout and the E/H time convention).
Structured simulation result.
Attributes
state : FDTDState
Final field state (useful for visualization).
time_series : (n_steps, n_probes) float array
Probe recordings over time.
s_params : (n_ports, n_ports, n_freqs) complex or None
S-parameter matrix (computed only when ports are present and
compute_s_params=True). With no driven port, passive-port ratios
from the main record are termination diagnostics, not an S-matrix
of the structure under independent port drives.
freqs : (n_freqs,) float or None
Frequency array for S-parameters.
ntff_data : NTFFData or None
Raw NTFF DFT data (use compute_far_field for radiation pattern).
ntff_box : NTFFBox or None
NTFF box specification (needed for compute_far_field).
dft_planes : dict[str, DFTPlaneProbe] or None
Frequency-domain plane probes keyed by name.
waveguide_ports : dict[str, WaveguidePortConfig] or None
Final accumulated waveguide-port configs keyed by name.
waveguide_sparams : dict[str, WaveguideSParamResult] or None
High-level calibrated waveguide S-parameters keyed by port name.
snapshots : dict[str, ndarray] or None
Field snapshots keyed by component name, each
(n_steps // interval, ...): frame k holds the fields after
step (k + 1) * interval (see SnapshotSpec). Indexed on the
padded lattice, like state.
grid : Grid or None
Grid metadata for post-processing helpers and advanced objectives.
settling_db : float or None
Worst identified pole-tail share over recorded channels and read bins,
in amplitude dB. Missing identification yields a non-pass witness.
settling_witness : dict or None
Status, reason, per-bin tail shares and identification errors, and
selected-record provenance for the value witness.
Create new instance of Result(state, time_series, s_params, freqs, ntff_data, ntff_box, dft_planes, flux_monitors, waveguide_ports, waveguide_sparams, waveguide_port_flux, snapshots, grid, dt, freq_range, settling_db, settling_witness, wire_port_sparams, ringdown, snapshot_axes, current_moment_data, current_moment_monitor, realized_geometry, sparam_time_records, dft_time_records, diagnostics)
Extract resonant modes from probe time series via Harminv.
Parameters
freq_range : (f_min, f_max) in Hz, or None to use stored range
probe_idx : which probe to analyze
source_decay_time : float or None
Time (s) after which source has decayed. If None, auto-
computed as 2×(3/π/f_center/bandwidth) — skips the Gaussian
excitation region for clean ring-down analysis.
bandpass : bool or None
Apply FFT bandpass before Harminv. Default: auto (True for
CPML results where DC/surface-wave artifacts exist, False
for PEC cavities where signal is clean).
waveform : source waveform or None
Used only when source_decay_time is None: when the
waveform exposes t0 (GaussianPulse /
ModulatedGaussian: t0 = cutoff*tau), the auto window
starts at 2*t0 so a longer-onset pulse (e.g.
cutoff=4.5 → 9*tau) is fully skipped. Without a
waveform the historical cutoff=3 envelope (2*(3*tau)) is
used, bitwise-identical to the previous hardcode.
Returns
list of HarminvMode
Check that the result's observables contain no NaN/Inf.
A non-finite time_series or s_params almost always means the
FDTD diverged rather than that the device is exotic. Returns True
when finite. With raise_on_nonfinite=True raises ValueError
instead of warning, so an automation loop can fail fast with a cause
hint right after run() instead of propagating silent garbage into
a downstream metric. Tracer-safe — a no-op under jax.grad/jit.
Returns
bool
True if all inspected observables are finite (or unavailable
for inspection, e.g. under tracing), False otherwise.
Frequency vector in Hz (freqs is already stored in Hz).
Returns
(n_freqs,) float array
Raises
ValueError If this Result carries no frequency vector.
Complex S-parameter vector S_mn vs frequency (1-indexed ports).
Parameters
m, n : int
1-indexed port numbers. s(1, 1) is S11.
Returns
(n_freqs,) complex array
Raises
ValueError
If this Result has no S-parameters, or if m/n are out
of range (the available port count is named in the message).
Magnitude of S_mn in dB: 20*log10(|S_mn|) (1-indexed ports).
The magnitude is floored at 1e-10 before the log (matching the
floor used by rfx.visualize.plot_s_params()) so an exact zero
yields a large negative dB value instead of -inf and the numeric
accessor agrees with the plotted curve at deep nulls.
Plot all S-parameter magnitudes vs frequency.
Thin wrapper over rfx.visualize.plot_s_params(), which builds
and returns its own matplotlib Figure (it does not accept an
externally supplied Axes).
Parameters
db : bool Plot magnitudes in dB (default) or linear. title : str Plot title.
Returns
matplotlib Figure
Raises
ValueError If this Result has no S-parameters.
Plot an S-parameter trajectory on a Smith chart.
Thin wrapper over rfx.smith.plot_smith().
Parameters
ports : (m, n) tuple of 1-indexed ports, optional
Which S-parameter to plot. Defaults to (1, 1) (S11).
**kw
Forwarded to rfx.smith.plot_smith() (e.g. z0,
ax, show_vswr, markers, title).
Returns
matplotlib Axes
Raises
ValueError
If this Result has no S-parameters, or ports are out of
range.
Plot the probe time series.
Thin wrapper over rfx.visualize.plot_time_series(). Requires
dt to be present. Simulation.run() populates it automatically;
supply dt when constructing a Result manually.
Parameters
labels : list of str, optional Per-probe labels. title : str Plot title.
Returns
matplotlib Figure
Raises
ValueError
If dt is not available in this Result.
Minimal differentiable simulation result.
Carries only the observables needed by gradient-based objectives,
avoiding the broader stateful surface of Result. The
settling_db and settling_witness properties score the concrete
user-probe record using the same host diagnostic as run(). They
report absence while records are traced; read them on the returned
concrete result, not as gradient objectives. Numeric
settling_probe_info preserves the selected columns, component
labels and the #1090 source-dominated flag as integers, without
inserting strings into a JAX result tree. settling_source_end_index
carries the first post-source sample, including free-space transit;
missing timing or insufficient post-source samples yields no witness.
These lazy properties are
not stored fields in _asdict(). wire_port_sparams is a tuple of
(meta, accs) pairs on both lanes; the lane-specific type of meta
and the accumulator channels are documented on Result.
lumped_port_sparams exposes the raw per-port
(V_dft, I_dft, V_ref_dft) tuples accumulated inside the JIT scan body
when forward(port_s11_freqs=...) is used. V and I are the
POST-injection physical channels (I carrying the Yee half-step phase);
V_ref is the PRE-injection drive sample, which only the N-port
off-diagonal incident wave consumes. Single-port objectives can keep
using s_params (per-port S11: the driven terminal reflection
rfx.probes.probes.driven_port_reflection() at an excited port, the
port-branch extract_lumped_s11() at a passive one). Multi-port AD
objectives (e.g. 2-port |S21| topology optimisation) read the raw
channels from this field and compose their own wave decomposition, since
a diagonal collapses each port to its self-reflection only.
dft_planes exposes the JIT-scan-accumulated complex DFT plane
probes registered via Simulation.add_dft_plane_probe(). Each
entry is a DFTPlaneProbe whose accumulator field is a
JAX-traceable complex array shaped (n_freqs, *plane_shape), with
plane-resolved field values usable by gradient-based objectives that
need plane-integrated V/I (e.g. waveguide-port or microstrip-port
line-integrated voltage / closed-loop current). None when no
plane probes were registered.
dt is the time step the solver advanced by, the spacing of
time_series. Read time and frequency with it, not with
grid.dt: stencil_order=4 steps at 0.857 * grid.dt.
ringdown is a rfx.ringdown.RingdownForwardResult when
forward(ringdown=RingdownSpec()) was called (issue #1254): the
completed wire-port S-parameters, traced and differentiable, and a lazy
host report on the concrete result. None otherwise.
design_box_held_edges is the tuple of (axis, i, j, k) E edges a
permittivity design box held on the drawn materials' coefficients
(forward(design_box_holds_ports=True); axis 0, 1, 2 for Ex, Ey, Ez):
() for a box that held none, None when no such box was given.
adjoint_settling is the primal design-edge max|E| over the last period of
the lowest monitor bin (capped at the record length), divided by the record maximum.
Above about 1e-2 means unsettled; small proves nothing: it follows the strongest field
and says nothing about the adjoint record. None for autodiff, NaN for zero signal.
Use rfx.gradient_record_length_witness() to witness an adjoint gradient.
Create new instance of ForwardResult(time_series, ntff_data, ntff_box, grid, s_params, freqs, lumped_port_sparams, wire_port_sparams, dft_planes, settling_probe_info, dt, ringdown, design_box_held_edges, current_moment_data, current_moment_monitor, adjoint_settling, settling_source_end_index, settling_freq_max, sparam_time_records, dft_time_records, diagnostics)
Host ring-down diagnostic from the concrete recorded user probes.
None means absent, including during tracing. This is a diagnostic,
not a differentiable objective. A returned JIT result can be scored
once its time series is concrete. Other result fields retain their
existing JIT restrictions (for example the host Grid object).
Use settling_verdict to judge the diagnostic.
High-level calibrated waveguide S-parameter data.
Create new instance of WaveguideSParamResult(freqs, s11, s21, calibration_preset, source_plane, measured_reference_plane, measured_probe_plane, reference_plane, probe_plane)
Waveguide scattering result assembled one driven port at a time.
settling_db records the worst per-bin pole-tail amplitude share
across modal V/I records for each drive. Paired extractions include
device and reference runs. settling_witness carries identification
status, reasons, and per-bin shares and errors.
s21_phase_residual_deg_rms is the post-solve discretization witness
(#894, post-v1.8 plan item 5): the RMS over the measured bins of
wrap(angle(S21) + beta(f)*L), in degrees, with beta the
EXTRACTOR'S OWN (_compute_beta at the port config's discrete
f_cutoff, dt and dx) and L the separation of the two
reference planes reported in reference_planes. Unlike a near-cutoff
|S11| headline it does not pass through the absorber: measured
invariant to absorber thickness (3x), record length (2.5x) and precision,
and second order in dx at every band down to f/f_c = 1.010
(tests/fixtures/waveguide_vi_envelope/s21_phase_residual_witness.json).
So it answers "is the port the problem?" when a reflection number looks
wrong -- for an EMPTY or matched guide between the two reference planes.
A device between them adds its own transmission phase to angle(S21),
and the number is then the device's phase against -beta*L, not a
port witness. It is REPORTED, never gated — nothing in the library compares it
with a threshold. None when the residual is not defined: a result that
is not a single-mode two-port, a traced AD run, two ports with different
discrete cutoffs or on different axes, a band whose every bin fell
below the weak-signal phase mask
(rfx.api._sparams.WAVEGUIDE_PHASE_MAG_FLOOR), or a normalize=True
lane, whose S21 has the empty-guide reference's propagation phase
divided out so the residual is not defined there (normalize="flux"
and normalize=False keep it).
s21_phase_residual_meta always carries beta_convention,
f_cutoff_hz, L_m, n_bins, masked_bins and normalize
(the lane the number was read under), plus a reason when the value
is None.
Create new instance of WaveguideSMatrixResult(s_params, freqs, port_names, port_directions, reference_planes, settling_db, s21_phase_residual_deg_rms, s21_phase_residual_meta, settling_witness)
Per-MSL-port downstream-reflector layout diagnosis, in metres.
satisfied means no considered conductor candidate violates the
existing recommendation. insufficient records a known violation;
unavailable means missing coordinates or unevaluated conductors
prevent that conclusion. A known violation remains insufficient even
when other conductors could not be evaluated.
This uses registered conductor bounds and the existing candidate scan,
not a modal-purity or S-accuracy test. It does not replace source/absorber,
settling or low-signal checks. Negative gaps mean the probe has passed
the candidate's first boundary along the port direction. None gaps
with satisfied status mean the scan found no candidate. Probe coordinates
are scalar positions on axis. The threshold is evaluated at the
simulation's design frequency, not independently at every result bin.
MSL S-matrix result.
Attributes
S : (n_ports, n_ports, n_freqs) complex
Power-wave S-matrix at each port's FIRST probe plane, formed from
a=(V+RI)/(2sqrt(R)), b=(V-RI)/(2sqrt(R)) with the positive real
analytic Hammerstad-Jensen reference R of each port. There
is no translation back to the physical feed planes. Bracketing H
samples are interpolated to this E-node plane using physical
coordinates, with the temporal leapfrog offset corrected separately.
The fitted Z0 below is a diagnostic, not the reference used to form S.
reference_impedances : (n_ports,) float, optional
Actual positive real R values used to define S, in ohms and in
port_names order. These are distinct from fitted Z0 and the
source/load resistances. Present on generated results; None is
retained for manually constructed legacy result objects.
freqs : (n_freqs,) float
Frequency grid in Hz.
Z0 : (n_ports, n_freqs) complex
Characteristic impedance extracted via the N-probe SVD
least-squares wave-decomposition fit (issue #80 Fix C),
per driven-port run (Z0[i, :] is from run with port i driven).
beta : (n_freqs,) complex
Propagation constant β from the N-probe least-squares fit
(issue #80 Fix C) at the first port's run.
reliable : (n_ports, n_freqs) bool, optional
Relative low-signal mask. reliable[p, k] is False when both
|V| and |I| at port p fall below 10% of that record's own band
medians in at least one driven run. All drive/port records used by
the solve are covered; S is retained unchanged.
A False entry does not by itself prove matrix corruption: a true
transmission zero can have zero passive-port phasors with a
well-conditioned drive matrix. Inspect absolute signal uncertainty,
settling and drive conditioning before interpreting those bins.
A True entry is not an accuracy certificate either. The numerical
threshold and the policies used by callers are unchanged.
probe_clearance : tuple[MSLProbeClearance, ...], optional
Separate downstream-reflector layout diagnosis in port order, using
the resolved probe ladder. Status is satisfied, insufficient or
unavailable; physical probe coordinates and signed gap estimates
accompany it. This is independent of reliable and does not
certify mode purity or S accuracy.
**The two fields answer different questions** (issue #726).
``reliable`` is a per-bin FIT-QUALITY mask; ``probe_clearance`` is
the GEOMETRIC condition, and neither is the other's proxy — on the
board runs that opened #726 ``reliable`` was True on 100 % of in-band
bins while the fitted Z0 was 2.4x the analytic value. What the
geometric condition costs was measured (VESSL 369367260508, cv06b
fixed-source, same source/load/DUT with only the p1 observation
offset varied, both arms settled below −118 dB): the FITTED Z0/beta
are what corrupt — the near-reflector arm's β scan railed on 51/51
bins over 3–5 GHz against 0/51 for the compliant arm — while raw S11
at the 3.77125 GHz notch bin read +0.026895 dB on the near-reflector
arm and +0.018065 dB on the compliant one, against the analytic 0 dB
that quarter-wave open-stub notch has. Both sit ABOVE unity on a
passive structure, by 0.31 % and 0.21 %, which is never reported here
as physics: they are raw, unprojected values carrying the coherent
power excess recorded in #838 (closed as not planned). Their 0.009 dB difference is one
fixture (cv06b), one bin (3.77125 GHz), an arm-to-arm difference —
NOT a bound on ``S`` — and the comparison's producer verdict was
``not_read``. What it does show is that ``S`` moves far less than the
fit, because it normalizes with the analytic Hammerstad–Jensen Z0
rather than with the fit.
So: gate on ``probe_clearance`` for the layout, on ``beta_railed``
for the fitted-value symptom, and treat ``Z0``/``beta`` as
UNREADABLE — not merely uncertain — when the status is
``insufficient``. ``S`` stands with the 0.009 dB caveat. The fix is
a longer uniform feed or a moved reference plane; raising
``n_probe_offset`` alone moves the probes toward the reflector.
``rfx.preflight.msl.MSL_PROBE_CLEARANCE_EFFECT`` is the single text
every warning about this condition embeds.
settling_db : (n_ports,) float, optional
Per-drive pole-tail amplitude share in dB over modal voltage,
loop-current, and witness-probe records at the extraction bins.
S_raw : (n_ports, n_ports, n_freqs) complex, optional
The S-matrix exactly as extracted, BEFORE passivity projection.
None on the default path, where S already IS that matrix;
set only when enforce_passivity=True clipped something, so no
information is discarded by enforcing the bound.
passivity_correction : (n_freqs,) float, optional
Per-frequency amount clipped by the passivity projection:
max(sigma_max(S_raw(f)) - 1, 0). None unless
enforce_passivity=True actually clipped a bin — it records
what the projection REMOVED, so on the default path there is
nothing for it to record and sigma_max_excess is the field to
read.
sigma_max_excess : (n_freqs,) float, optional
Per-frequency max(sigma_max(S(f)) - 1, 0) of the RAW
extraction, measured before any opt-in projection and filled on
every concrete call (None only while tracing, which has no
value to take singular values of). Zero where the extraction was
already passive. This is the honesty metric, and it does not
depend on enforce_passivity: a bin with a large excess is a
measurement artifact (check reliable and settling_db for
the cause), whether the returned S carries that excess or a
projection clipped it away. Where a projection did clip,
passivity_correction equals this on the touched bins.
Near neighbour, different quantity: the ``passivity_excess`` key of
`rfx.io.network_quality_metrics()` is a single SCALAR over the
whole sweep, measured in POWER (``max(sigma_max^2 - 1, 0)``). This
field is per frequency and in amplitude. The two names differ so
the numbers are not read as interchangeable; do not compare them.
port_names : tuple[str, ...]
assembly : str, optional
Which rule produced S — "multi_drive_solve" (normal:
S = B·A⁻¹ over all drives, issue #507) or
"single_ratio_fallback". The fallback is taken when the solve
returns non-finite entries on a degenerate drive system; it is the
SUPERSEDED per-column rule S[j, d] = b_j / a_d, which reports a
passive port's echo as the driven port's own reflection whenever
that port is not matched.
**Read this before trusting a fallback result.** The fallback's
characteristic symptom is column power above 1, which the default
``enforce_passivity=False`` leaves standing in ``S`` and
``sigma_max_excess`` measures. Under ``enforce_passivity=True``
the symptom is clipped out of ``S`` — but it is not erased from
the result: ``passivity_correction`` records how much was clipped
and ``S_raw`` keeps the unprojected matrix, and the run also emits
both a fallback warning and a passivity-guard
warning. So a fallback is not silent; this field is simply the
*specific* signal. Column power above 1 has several causes (an
under-settled record, a standing-wave null, a mis-scaled current) and
only one of them is the fallback — that is what this field
disambiguates. ``None`` while tracing (the finiteness test cannot run
on a tracer, so the solve result is taken as-is — see ``cond_a``).
cond_a : (n_freqs,) float, optional
Per-frequency condition number of the power-wave drive matrix A
(one common row scale cancels from this condition number). Bounds
DEGENERACY of the drive system only — it is not a reliability
or accuracy score, and a low value does not certify the result
(same contract as the coax lane's
rfx.sources.coaxial_port.solve_two_port_from_wave_amplitudes()).
None while tracing.
beta_railed : (n_ports, n_freqs) bool, optional
Per-port β-scan rail mask for the FITTED numbers (issue #681).
beta_railed[p, k] is True when port p's own-drive N-probe β
scan failed to bracket its residual optimum at bin k — the raw
argmin sat at an edge node of the ±35% window around the analytic
Hammerstad-Jensen guess, or the refined β landed within half a
grid step of a window limit. Z0[p, k] (and beta[k] for
p = 0) at such a bin is the scan-window limit, NOT a
measurement — do not quote it. Fitted Z0/beta do not enter S11/S21,
which use the analytic Z0 anchor and measured V/I. This separation
does not certify the V/I or reference impedance: shared record
contamination can affect both the fit and S. The
own-drive diagonal is exactly the provenance of every fitted
number this result carries (Z0[i, :] comes from port i's
own driven run). None while tracing.
Mixed-family S-matrix result (issue #488, lumped/wire + MSL v1).
S here is in the Kurokawa
power-wave convention (every wave amplitude divided by
sqrt(Re(Z0_port))): with unequal reference impedances across
families a pseudo-wave b/a ratio is off by sqrt(Z_j/Z_i)
(issue #460), so the mixed lane normalizes to power waves by
construction. Off-diagonal PHASE across families is provisional —
the lumped/wire cell and the MSL de-embedded plane are different
reference-plane conventions (magnitude is the validated observable).
Attributes
S : (n_ports, n_ports, n_freqs) complex
Power-wave S-matrix; S[i, j] = response at port i driving
port j. Port order: lumped/wire ports (registration order),
then MSL ports (registration order).
freqs : (n_freqs,) float
Frequency grid in Hz.
port_names : tuple of str
Combined port names (lumped/wire ports are auto-named
"lw<k>"; MSL ports keep their registered names).
port_families : tuple of str
"lumped" / "wire" / "msl" per port, same order.
z0_ref : (n_ports,) float
Power-wave reference impedance per port: the registered port
impedance for lumped/wire, analytic Hammerstad-Jensen Z0 for MSL.
settling_db : (n_ports,) float, optional
Per-drive pole-tail amplitude share in dB over recorded S channels
and witness probes at the extraction bins.
s21_power_witness : np.ndarray | None
(n_msl, n_lw, n_freqs) real — extractor-independent |S21|
cross-check for lumped/wire-driven columns: the MSL arriving
power-wave magnitude over |a_drive| reconstructed from the
delivered power P_del = 0.5*Re(Z_in)*|I|^2 and
(1 - |S_jj|^2) (does not trust the port-cell a-wave
magnitude; issue #313 triangulation).
reliable : np.ndarray | None
(n_msl, n_freqs) bool — relative low-signal mask from each MSL
port's own driven run. A False entry is not by itself proof of
an incorrect S-matrix; see MSLSMatrixResult.
probe_clearance : tuple[MSLProbeClearance, ...], optional
MSL-only downstream layout diagnoses, in MSL registration order.
Lumped/wire ports have no entries here, as with reliable.
Same contract as MSLSMatrixResult.probe_clearance, including
what the insufficient status costs and what to gate on (#726):
the FITTED Z0/beta go unreadable, S carries a measured 0.009 dB
caveat, and reliable gates neither.
beta_railed : np.ndarray | None
(n_msl, n_freqs) bool — β-scan rail mask from each MSL port's
own driven run, same criterion as
MSLSMatrixResult.beta_railed (issue #681). In this lane
the N-probe fit is DIAGNOSTIC ONLY (the |Zc| deviation warning);
a True bin means that diagnostic's β/|Zc| are the scan-window
limit, not a measurement. Fitted beta/Z0 do not enter S, but
this does not certify their shared measured V/I.
S_raw / passivity_correction :
As in MSLSMatrixResult — set only when the passivity
projection touched at least one bin.
Reverse-mode AD memory estimate (issue #30 CHECK 4 / #39).
All sizes are in gigabytes. warning is populated when the
selected estimate exceeds 85% of available_gb.
ad_checkpointed_gb is the legacy jax.checkpoint(step_fn)
estimate; for FDTD on the non-uniform path this is not a
realistic memory number because the scan carry itself is not
rematerialised (see issue #31 VESSL 369367233490). Use
ad_segmented_gb for the runner-supported segmented paths:
checkpoint_every on the non-uniform scan-of-scan path and
checkpoint_segments on the uniform segmented-scan path. The
segmented model counts segment-boundary storage (carry + cotangent,
2 × active_segments) PLUS the live-segment rematerialization tape
(one segment's per-step field tape resident during backward replay,
issue #277); it is minimized near sqrt(2 × n_steps) steps per
segment. When present,
ad_segmented_active_segments is the actual number of active segment
boundaries/carry snapshots used by the segmented estimate after warmup.
evidence_class labels this artifact as a static estimate so downstream
tooling does not confuse it with observed profiler evidence or a bounded
compiled-executable certificate.
Create new instance of AD_MemoryEstimate(forward_gb, ad_checkpointed_gb, ad_full_gb, ntff_dft_gb, available_gb, warning, ad_segmented_gb, checkpoint_every, checkpoint_segments, ad_active_steps, ad_segmented_active_segments, grid_kind, grid_source, grid_shape, sheet_gb)
Checkpoint planning result for reverse-mode AD memory.
checkpoint_every is the non-uniform segmented-scan chunk length.
checkpoint_segments is the uniform segmented-scan segment count.
checkpoint_mode is the knob selector, not a runnable-fit verdict:
check full_ad_fits first, then require segmented_fits before wiring
the selected segmented knob. A non-fitting plan may still carry the
least-memory candidate knob for diagnostics.
fit_safety_factor records the conservative multiplier used before an
estimate is allowed to set full_ad_fits or segmented_fits.
evidence_class labels this artifact as a calibrated conservative plan,
not a certificate or observed runtime profile.
Create new instance of ADMemoryPlan(n_steps, available_memory_gb, target_fraction, target_memory_gb, checkpoint_every, selected_estimate, full_ad_fits, segmented_fits, recommendation, checkpoint_segments, checkpoint_mode, fit_safety_factor)
Named contribution to a reverse-mode AD memory explanation.
Create new instance of ADMemoryComponent(name, memory_gb, share_of_selected, kind, unit, count, bytes_per_unit_gb, explanation)
Actionable AD-memory preflight hint derived from planning evidence.
Create new instance of ADMemoryActionHint(code, severity, message, action, checkpoint_mode, checkpoint_every, checkpoint_segments, blocking)
Static reverse-mode AD memory explainability artifact.
The report decomposes the selected AD estimate into named components so users can see whether field tape, segment-boundary carries, CPML/material state, or monitor state dominates the planning artifact. It is still static planning evidence, not profiler evidence or a bounded certificate.
Create new instance of ADMemoryExplainabilityReport(n_steps, strategy, selected_memory_gb, selected_memory_field, estimate, components, dominant_component, recommendations)
Consolidated mesh/memory preflight summary.
This is a lightweight user-facing planning object for the "subgrid-like" non-uniform lane: it combines existing preflight advisories with cell-count and AD-memory estimates, including a uniform-fine comparator for non-uniform meshes.
Create new instance of MeshIntelligenceReport(grid_shape, cells, uniform_fine_shape, uniform_fine_cells, cell_savings_factor, min_cell_size, nominal_dx, uses_nonuniform, preflight_issues, ad_memory, recommendation)
Composite AD-memory preflight artifact.
This report composes static memory planning, static explainability, optional mesh advisories, and optional trace-time JAX saved-residual diagnostics. It is not runtime profiler evidence, XLA memory analysis, a peak-memory guarantee, a certificate, or RF validation.
Create new instance of ADMemoryPreflightReport(n_steps, available_memory_gb, target_fraction, target_memory_gb, fit_safety_factor, status, supported_checkpoint_mode, checkpoint_every, checkpoint_segments, full_ad_fits, checkpointing_fits, memory_plan, explainability, mesh_report, residual_diagnostic, action_hints, evidence_boundaries, recommendation)
Bounded compiler-memory certificate for one exact compiled executable.
The report is derived from a caller-supplied JAX compiled object's
memory_analysis() result plus complete exact-scope metadata. It is not a
universal peak-memory predictor, runtime profiler artifact, RF validation
result, or proof that source/config digests correspond to an opaque
executable.
Create new instance of ADCompiledMemoryCertificate(status, status_reason, available_memory_gb, target_fraction, target_memory_gb, compiler_reported_required_bytes, compiler_reported_required_gb, temp_size_in_bytes, argument_size_in_bytes, output_size_in_bytes, alias_size_in_bytes, temp_gb, argument_gb, output_gb, alias_gb, exact_scope, scope_status, scope_status_reason, scope_digest, config_digest, environment_digest, memory_analysis_status, memory_analysis_status_reason, jax_version, evidence_boundaries, recommendations, source_preflight)
Whether the JAX compiler memory estimate fits the target budget.
This reflects Compiled.memory_analysis(), a compiler estimate — not
a runtime guarantee. It does not model allocator fragmentation or
runtime scratch, so True does not promise the run avoids OOM.
Returns None when compiler evidence or exact scope is incomplete.
Parser health summary for saved-residual diagnostics.
Grouped summary of saved residual records.
Create new instance of ADResidualGroup(key, source_kind, name, dtype, record_count, known_count, unknown_count, known_estimated_bytes, line_indices)
Structured artifact for JAX saved-residual inspection.
This artifact is produced from jax.ad_checkpoint.print_saved_residuals.
It explains what JAX says would be saved for reverse-mode AD under the given
Python function and sample arguments. It is not a runtime profile, not an
XLA memory report, and not a peak-memory certificate.
Create new instance of ADResidualInspection(records, raw_lines, total_estimated_bytes, total_estimated_gb, unknown_estimate_count)
One line from JAX saved-residual inspection.
estimated_bytes is derived from the printed abstract value when shape
and dtype are parseable. It is a static byte count for the residual value,
not allocator overhead, fragmentation, or measured peak memory.
Create new instance of ADResidualRecord(aval, source, dtype, shape, size, estimated_bytes, source_kind, name, raw_line, line_index)
rfx saved-residual diagnostic report.
The report summarizes JAX saved-residual evidence for one differentiated callable and sample argument set. It is trace-time explainability, not a runtime profiler, XLA memory report, peak-memory guarantee, certificate, or RF/gradient validation artifact.
Create new instance of ADSavedResidualDiagnosticReport(inspection, top_residuals, groups, parser_health, known_estimated_bytes, known_estimated_gb, total_estimated_bytes, total_estimated_gb, jax_version, workflow, context, artifact_snapshots, recommendations)
Return an rfx diagnostic report for JAX saved residuals.
The report layers rfx-owned summaries and recommendations on top of
inspect_ad_saved_residuals. It inspects fun(*args, **kwargs) and
preserves exceptions from tracing the callable. Optional context and
artifacts are serialized as snapshots only; they are not used to infer
runtime memory fit or physics validity.
Inspect JAX saved residuals for fun(*args, **kwargs).
The implementation captures jax.ad_checkpoint.print_saved_residuals and
parses the printed abstract values into a structured, JSON-serializable
artifact. Exceptions from tracing fun propagate to the caller.
Caveat: capture uses a process-global contextlib.redirect_stdout while
tracing fun. It is therefore not thread-safe, and any stray print()
executed by fun during tracing is captured and parsed as a residual
line (it degrades to an unknown record that nulls the byte total rather
than raising). Avoid printing inside fun when using this helper.
Parse one line printed by JAX saved-residual inspection.
Unknown future JAX formats are preserved as raw text with parseable fields
set to None instead of being silently discarded.
Pareto frontier artifact for a finite set of objective vectors.
Create new instance of ParetoFront(points, minimize, objective_names, source_count, front_indices, dominated_indices)
One candidate in a Pareto front.
Build a JSON-serializable Pareto-front artifact.
Return a boolean mask selecting non-dominated objective rows.
minimize may be one bool for all objectives or one bool per objective.
Objectives marked False are treated as maximization objectives. atol
raises the bar for strict improvement: after orientation a point p
dominates q only if p is no worse than q on every objective
(exact <=) and better by more than atol on at least one. This keeps
the relation a strict partial order (transitive, no dominance cycles), so a
larger atol retains more points and the front is never empty for
non-empty finite input.
Return lower-is-better weighted scalarization scores.
Maximization objectives are sign-flipped before scoring. When normalize
is true, each oriented objective is min-max normalized; constant objectives
contribute zero after normalization.
Return points satisfying raw epsilon constraints per objective.
For minimization objectives the constraint is objective <= epsilon; for
maximization objectives it is objective >= epsilon. Objective indices not
present in epsilons are unconstrained.
Select the best feasible point by one primary objective.
Returns None when no point satisfies the epsilon constraints.
Return a checkified wrapper for user and optional automatic checks.
User checks are always enabled because the helpers below emit
checkify.check predicates. float_checks additionally catches NaN and
divide-by-zero errors inserted by JAX checkify. index_checks is opt-in
because it can be noisier for code that intentionally uses masked indexing.
Assert that all entries of value are finite inside JAX tracing.
Assert that all entries of value are positive or non-negative.
Assert lower/upper bounds for an array-like value inside JAX tracing.
Assert a finite positive Courant-like number at or below limit.
The check is inclusive (courant <= limit), matching the textbook CFL
bound. The default limit=1.0 is the 1-D ceiling; pass the
dimensionality-appropriate bound for the target engine (e.g. 1/sqrt(3)
~= 0.577 for 3-D FDTD) rather than relying on the default.
Axis-aligned box defined by two corners (meters).
Rasterization convention (read before drawing a PEC obstacle).
On each axis the volume branch is half-open [lo, hi) over
node coordinates: node j at y_j belongs to the box
iff lo - tol <= y_j < hi - tol, with tol = NODE_TIE_REL *
node_widths[j] (#1138), using each node's forward primal cell width
and repeating the last width at the final node. The convention is
deliberate and several paths depend on it (see _axis_mask for the
issue history), but it has two consequences when a box is drawn to a
nominal physical size:
The
hiface contributes no cell. A box whose corners both land on node planes,lo = i*dxandhi = k*dx, occupies nodesi .. k-1. Its realized extent between the first and last occupied node plane is therefore(hi - lo) - dx, one cell short of the drawn extent, and the shortfall is entirely at thehiface, so a single box is also displaced bydx/2towardlo. Note the two faces are NOT interchangeable:lois inclusive andhiis exclusive. Whether that per-box displacement survives in a multi-object structure depends on which face of each object is the load-bearing one — see the facing-pair discussion below, where it cancels in one case and not the other.Float arithmetic within the on-lattice band keeps the face on its node. Concrete coordinates and comparisons remain host float64, independent of
jax_enable_x64(#802). A corner drawn throughm*dx,lo + N*dx, repeated addition or a graded cumulative sum can differ from its node by a few ULP. The shared1e-9-local-cell band includes that node atloand excludes it athi(#1138), preserving the half-open cell count.Historical (pre-#802, kept because committed values were pinned to it): masks compared float32 coordinates, themselves double-rounded
f32(f32(i) * f32(dx)), so a WR-90 f64 node reconstruction disagreed with production on 30 of 31 nodes by up to 1.1e-9 m and the SAME drawing recipe gaved + dxat one aperture andd + 2*dxat another, unpredictably. That scatter is gone: drawing fins to the nominal opening now costs exactly one cell (thehi-face retreat), at every aperture.
For a PEC obstacle the occupied nodes are where the tangential E
is zeroed, so consequence (1) is an electrical dimension error, not a
sub-cell cosmetic one. Transverse to the propagation direction — an
aperture width, a guide width, anything that sets a cutoff — the
electrical dimension is the span between the innermost ZEROED node
planes, (n_open + 1) * dx, the same measure that reproduces the
guide's own width a = cells * dx exactly (counting open nodes alone
would call WR-90 22.098 mm instead of 22.86 at a/30). An independent
refit of 16 committed WR-90 single-iris configurations across two meshes
pins the realized transverse aperture offset to within 1/20 of a cell of
that identity (measured during the stage-S3 / issue #499 review; no
committed record carries the refit yet, so like the longitudinal caution
below it is a recorded observation, not a number to build on).
Do not carry that identity into the propagation direction. The
electrical thickness of an obstacle (an iris's extent along the guide
axis) is not the zeroed-plane span: it is set by how the fields interact
with the discontinuity, not by a cutoff condition. Measured on a
single-iris geometry at four drawn thicknesses, the effective thickness
lands consistently between t_cells * dx and
(t_cells - 1) * dx, so neither integer rule is right and the
residual is a fixed per-face offset rather than a discretization error
that shrinks with dx. The longitudinal convention is not settled;
treat it as an unknown of order half a cell, and if a comparator needs a
thickness, make the sensitivity to that half cell part of the reported
envelope rather than picking a rule. (Measured during the stage-S3 /
issue #499 review; no committed record carries it yet, so it is stated
here as a caution, not as a number to build on.) This measured
effective thickness is a different quantity from a cascade
comparator's electrical-length bookkeeping — issue #499's comparator
deliberately draws t_c = round(t/dx) + 1 so that (t_c - 1)*dx
conserves the cascade's total electrical length; that bookkeeping choice
answers a different question and is not contradicted by this caution.
A facing pair is not symmetric under this rule, because its two
interior faces are different kinds of corner. For fins drawn from each
wall inward, the lo fin's interior face is a hi corner, which
half-openness always drops, so that fin always retreats one cell.
The hi fin's interior face is a lo corner, which the banded window
keeps, including float dust within the on-lattice band. Historically,
before that band, the realized opening was:
d + dxwhen only the lo fin retreated. The opening is then asymmetric: its centre sitsdx/2below the guide centre.d + 2*dxwhen rounding made the hi fin retreat as well. The two retreats cancel and the opening is symmetric.
Since #802 the nominal drawing landed on the FIRST case at every
aperture (measured across the WR-90 table at a/30 and a/60: excess is
uniformly one cell, asymmetric): the hi fin's lo corner, being an
exact node value, captured its node. #1138 also keeps a lo face a few
ULP above that node from retreating. Historical (pre-#802, float32): which case you got
was unpredictable — d = 12.192 mm gave d + 2*dx while 7.620
and 18.288 mm gave d + dx, and over ~99k (guide, mesh, aperture)
combinations the split was 82% / 9% / 8% across d + dx /
d + 2*dx / d at even parity.
Shifting each interior face half a cell the wrong way (toward the
metal) retreats both faces by construction rather than by luck, giving
d + 2*dx deterministically at every aperture — that is the drawing
case 18's blocked revision used, and it is why re-comparing that
revision against oracle(d + 2*dx) collapsed every row.
In PR #480's WR-90 single-iris lane this inflated the |S11| error
against an analytic mode-matching oracle by 4-6x, and because it scales
with dx it is easy to misread as first-order convergence. For a
resonant structure (multi-iris filter, cavity) it shifts the
passband rather than widening a magnitude tolerance, so it will not look
like a discretization error at all.
Recipe, two conditions, both needed:
- Put interior corners on cell midpoints,
(j + 0.5) * dx, to make nodejthe innermost occupied plane. Every corner value in a one-cell-wide interval selects the same footprint ((j*dx, (j+1)*dx]for ahicorner,[j*dx, (j+1)*dx)for alocorner), and the midpoint is that interval's centre — not merely a safe nudge away from the node. That is precisely why it is immune to the float32 effects in (2), which only ever perturb a corner by a fraction of a cell. - Keep the metal depth an exact number of cells, i.e. for a symmetric
obstacle keep
(cells - d_cells)even. When it is odd,fin_depth = (cells - d_cells)//2truncates and a symmetric opening of that width is simply not representable on the grid: the opening is one cell wider however it is drawn. That is a representability limit, not a rasterization defect.
Under both conditions the realized opening equals the nominal one exactly (measured, 100% of ~50k even-parity combinations). Then still assert the realized footprint (count the occupied node planes) against the intended one.
Odd parity is a fork, not a dead end. Symmetric fins can only realize
apertures whose cell count has the parity of cells, so when the
aperture you want has the wrong parity you must choose, and both options
cost something:
- change
dx(or the aperture) so the parity works, which moves every other dimension on that axis; or - place the fins asymmetrically on purpose, accepting a known half-cell offset of the opening rather than rounding the aperture to the wrong parity — which would change the aperture itself, the quantity that sets the cutoff.
This docstring does not recommend one over the other: how much the half-cell offset costs has not been measured here, so treat it as an open trade rather than a cheap escape. What is not optional is that the offset be recorded and representable by whatever you compare against — an off-centre aperture only stays a known quantity if the oracle can model it. If your comparator assumes a centred obstacle, an intentional offset silently becomes comparator error, which is the failure mode this whole docstring exists to prevent.
A box thinner than one local cell takes a separate thin-sheet
branch (a single node); the notes above apply to the volume branch
only. The thin branch's node is chosen by the volume rule whenever
that rule is decisive: a thin box whose half-open [lo, hi) window
selects exactly one node realizes on THAT node — so a face-registered
one-cell box lands on its lo-face node deterministically, not on
whichever side of the exact half-cell tie the last f64 ulp of
(lo+hi)*0.5 favours (#802 follow-up). Only a window with zero
nodes (a sub-cell box straddling none) or several falls back to the
nearest-node rule.
Sphere defined by center and radius.
Cylinder along a given axis.
Wire defined by a polyline path with constant circular cross-section.
Voxelizes by computing the distance from each grid point to the nearest
line segment of the polyline. Grid points within radius of any
segment are marked as inside.
Parameters
points : tuple of tuple[float, float, float] Ordered vertices in metres, e.g. ((x0,y0,z0), (x1,y1,z1), ...). radius : float Wire radius in metres. For PEC realization, a positive radius must be at least half the smallest local cell at the vertices (a volume wire). Smaller positive radii refuse; refine the mesh to resolve the wire. Zero retains the legacy lattice filament without a declared physical radius.
A rectangular patch curved along one axis with a given radius.
The patch is approximated as a series of flat Box segments (staircase approximation). The number of segments depends on the grid resolution dx.
Parameters
center : tuple (x, y, z) Center of the patch in 3D space. length : float Patch length along the curvature axis. width : float Patch width perpendicular to curvature. radius : float Curvature radius (larger = flatter). axis : str Axis along which the patch is curved ("x" or "y").
Via through-hole for multi-layer PCB structures.
Parameters
center : tuple (x, y)
Position in the xy-plane (meters).
drill_radius : float
Drill hole radius (meters). Determines the width of the
vertical conductor.
pad_radius : float
Copper pad radius (meters) on each layer boundary.
layers : list of (z_bottom, z_top)
PCB layer boundaries from bottom to top (meters).
Each tuple defines one dielectric layer.
material : str, optional
Material name assigned to all generated shapes.
Defaults to "pec".
Return a list of (Box, material_name) tuples.
Creates:
- One vertical conductor (Box) spanning from the lowest layer bottom to the highest layer top.
- One pad (thin Box) at each unique layer boundary z-coordinate.
Pads at shared boundaries (e.g. z_top of layer 0 == z_bot of layer 1) are emitted only once.
Alias for to_shapes() -- ready for Simulation.add().
Occupancy Shape backed by a watertight triangle mesh (point-in-mesh).
Construct from a file with from_file() (the common path) or wrap an
already-loaded trimesh.Trimesh directly with MeshShape(mesh).
Parameters
mesh : trimesh.Trimesh
An already-loaded, already-scaled/positioned watertight mesh (metres).
validate : bool, default True
Raise ValueError if the mesh is not watertight.
Load a mesh/CAD file and return a validated MeshShape.
Supported: STL / OBJ / PLY (native trimesh) and STEP / STP (via the optional
cascadio OpenCASCADE backend, part of the cad extra — the issue-#358 Stage-2
"optional helper"; no native STEP kernel in rfx). STEP assemblies of several solids are
concatenated into one occupancy body.
Parameters
path : str Mesh/CAD file; the extension selects the loader. scale : float REQUIRED (no unit guessing). Multiplies the LOADED vertex units to metres:
- STL/OBJ/PLY are UNITLESS (as authored) ⇒ pass your unit→m, e.g. mm ``scale=1e-3``.
- STEP/STP carry units; cascadio converts them to METRES on load, so the loaded mesh
is already in metres ⇒ ``scale=1.0`` (unless deliberately rescaling).
translate : tuple[float, float, float] Rigid offset in metres applied AFTER scaling. Default origin.
Thinnest bounding-box extent (metres) — a tessellation-INDEPENDENT proxy for the smallest resolvable dimension (e.g. a thin wall/plate thickness), used by the preflight under-resolution advisory. Deliberately NOT the shortest triangle edge: that tracks mesh DENSITY, not geometry, so a finely-tessellated smooth surface would cry wolf even when the part is well resolved.
(Nx, Ny, Nz) bool occupancy at cell centres via point-in-mesh containment.
Only cell centres inside the mesh bounding box are ray-tested (the rest are trivially outside), so cost scales with the object's footprint, not the whole domain.
Host-side only: rasterisation runs through trimesh.contains, so this cannot be
traced/jitted or used as a differentiable mesh — a traced coordinate raises a clear
error rather than a cryptic JAX array-conversion failure.
Single PCB layer (copper or dielectric).
Parameters
thickness : float
Layer thickness in metres.
material : str
Material identifier — must be a key in MATERIAL_LIBRARY
(e.g. "copper", "fr4", "rogers4003c") or a known
alias ("prepreg"). Users may also register custom materials
on the Simulation object.
name : str or None
Optional human-readable label (e.g. "top", "core").
Named layers can be looked up with Stackup.get_layer_z().
PCB stackup builder.
Layers are specified bottom-to-top (index 0 is the lowest z layer).
The stackup is centred at z = 0 so that
z_bottom = -total_thickness / 2 and
z_top = +total_thickness / 2.
Parameters
layers : sequence of PCBLayer
Ordered from bottom to top.
Return (z_lo, z_hi) of the named layer.
The stackup is centred at z = 0.
Raises
KeyError If no layer with name exists.
Generate Box shapes for each layer.
Parameters
center_xy : (cx, cy) Centre of the board in the xy-plane (metres). size_xy : (sx, sy) Full extent of the board in x and y (metres).
Returns
list of (Box, material_name) tuples
Each tuple contains the layer geometry and the resolved
material name suitable for sim.add(shape, material=...).
Conductor layers are SHEETS (#931 §1.5). A 35 µm copper foil is
thinner than any cell a board simulation uses, and a PEC Box with
0 < extent < one local cell is refused by the rasterizer — it
is a volume declaration the lattice cannot honour. So a conductor
layer is emitted as a ZERO-THICKNESS Box, the canonical sheet
declaration.
The plane is the foil's DIELECTRIC INTERFACE, not its mid-plane
(§4 rule 2): the face it shares with the neighbouring dielectric
layer — z_hi when the dielectric is above, z_lo when it is
below (z_lo when both, and the mid-plane only when neither
neighbour is a dielectric). On the mid-plane the sheet sits half
a foil thickness OFF the laminate, and a mesh that puts a line at
both the interface and the sheet gets a 17.5 µm vacuum cell in
series with the board — the #702 slot geometry, and a ~23x dt
collapse on a standard 2-layer stack-up. Dielectric layers keep
their finite extent, so the stack-up's z arithmetic
(get_layer_z, total_thickness) is unchanged.
Standard 2-layer PCB (top copper / substrate / bottom copper).
Parameters
substrate_thickness : float
Dielectric core thickness (default 1.6 mm).
substrate_material : str
Dielectric material name (default "fr4").
copper_thickness : float
Copper foil thickness (default 35 um / 1 oz).
Standard 4-layer PCB stackup.
Layer order (bottom to top)::
bottom copper | prepreg | inner2 copper | core (FR-4) |
inner1 copper | prepreg | top copper
Parameters
core_thickness : float Central dielectric core thickness (default 1.0 mm). prepreg_thickness : float Pre-impregnated dielectric thickness (default 0.2 mm). copper_thickness : float Copper foil thickness for all layers (default 35 um / 1 oz).
Characteristic impedance and eps_eff of a microstrip line (analysis).
Parameters
width : float Conductor (trace) width in metres. height : float Substrate (dielectric) thickness in metres. eps_r : float Substrate relative permittivity (dimensionless, >= 1).
Returns
(z0, eps_eff) : tuple of float
z0 is the characteristic impedance in ohms; eps_eff is the
effective relative permittivity.
Notes
Two-regime Wheeler/Hammerstad expression (Pozar §3.8, eq. 3.196):
w/h <= 1:z0 = eta_0 / (2*pi*sqrt(eps_eff)) <ul> <li>ln(8 h/w + w/(4 h))
w/h >= 1: z0 = eta_0 / (sqrt(eps_eff)
<ul>
<li>(w/h + 1.393 + 0.667 ln(w/h + 1.444)))Effective relative permittivity of a microstrip line (Hammerstad).
Parameters
width : float Conductor (trace) width in metres. height : float Substrate (dielectric) thickness in metres. eps_r : float Substrate relative permittivity (dimensionless, >= 1).
Returns
float
Effective relative permittivity eps_eff seen by the quasi-TEM
mode (1 <= eps_eff <= eps_r).
Notes
Uses the Hammerstad form (Pozar §3.8, eq. 3.195); the
(w/h < 1) branch adds the (1 - w/h)^2 thin-line correction.
Trace width that yields a target characteristic impedance (synthesis).
Solves the inverse problem: given a desired z0 (e.g. 50 ohm), a
substrate thickness, and a substrate permittivity, return the
microstrip conductor width.
Parameters
z0 : float Target characteristic impedance in ohms (> 0). height : float Substrate (dielectric) thickness in metres. eps_r : float Substrate relative permittivity (dimensionless, >= 1).
Returns
float Conductor (trace) width in metres.
Notes
Closed-form Wheeler/Hammerstad synthesis (Pozar §3.8, eq. 3.197).
A trial w/h is computed for both the narrow and wide regimes and
the branch consistent with its own validity condition is selected.
Differentiated Gaussian pulse with center frequency f0.
s(t) = -2 * ((t - t0) / tau) * exp(-((t - t0) / tau)^2)
The onset truncation at t=0 leaves a nonzero time integral: over the
sampled table, S = sum s(t_n)*dt ~ -amplitude * tau * exp(-cutoff^2).
A soft current source deposits exactly that integral as a net static
end-charge q = S/((1+loss)*dz_src) (issue #388), so the deposited DC
scales as e^(-cutoff^2): raising cutoff from 3.0 to 4.5 cuts the
deposited charge by e^(4.5^2 - 3^2) ~ 7.7e4 — about 5 decades.
Use a higher cutoff when a static source remnant matters (e.g. the
until_decay interior-energy stop on absorbing boundaries).
Parameters
f0 : float Center frequency (Hz). bandwidth : float Fractional bandwidth (0–1). Default 0.5. amplitude : float Peak amplitude (V/m). Default 1.0. cutoff : float Number of tau between t=0 and the pulse peak: t0 = cutoff * tau. Default 3.0 (historical value; waveform values are unchanged at the default). Higher values buy an e^(-cutoff^2) reduction of the deposited-DC integral at the cost of a longer onset (issue #388).
Gaussian-envelope modulated carrier (Meep-style source).
s(t) = amplitude * sin(2π·f0·t) * exp(-((t - t0) / tau)^2)
The carrier-centered spectrum generally reduces low-frequency/DC content relative to a differentiated Gaussian. A finite sampled pulse does not in general have an exactly zero integral, so DC-sensitive calculations should inspect the sampled spectrum and fields explicitly.
DC suppression is set by the envelope width: the spectrum is a Gaussian
centered at f0, and its value at DC relative to the carrier peak is
e^(-1/bandwidth^2) — about 0.5 at bandwidth=1.2 but 0.018 at
bandwidth=0.5. Wide fractional bandwidths therefore keep substantial DC
drive; when the net deposited charge of a soft source matters (e.g. the
until_decay interior-energy stop, issue #388), use bandwidth <= 0.4
(measured relative DC 4.0e-4 at the issue-#388 demo scale — below the
run() warning's 1e-3 threshold, which bandwidth=0.5 at ~2.6e-2 still
exceeds) or a GaussianPulse with a higher cutoff.
Parameters
f0 : float Center/carrier frequency (Hz). bandwidth : float Fractional bandwidth (0–1). Default 0.5. amplitude : float Peak amplitude. Default 1.0. cutoff : float Number of tau before the envelope peak. Controls how gradually the source turns on. Default 5.0 (Meep convention: source starts at t=0 and peaks at t = cutoff * tau). Use 3.0 for a faster onset with a slightly non-zero initial amplitude.
Continuous-wave sinusoidal source with smooth ramp-up.
Parameters
f0 : float Frequency in Hz. amplitude : float Peak amplitude. ramp_steps : int Number of source-frequency cycles over which to apply a cosine taper onset. 0 means instant full-amplitude.
User-defined waveform wrapper.
Parameters
func : callable
Function f(t: float) -> float returning the source amplitude
at time t.
Waveguide port definition.
x_index : int
Plane index along the port normal axis (legacy name kept for
backwards compatibility).
y_slice : (y_lo, y_hi) or None
Legacy x-normal first transverse slice (y) when normal_axis='x'.
z_slice : (z_lo, z_hi) or None
Legacy x-normal second transverse slice (z) when normal_axis='x'.
a : float
Waveguide width in meters along the first local transverse axis.
b : float
Waveguide height in meters along the second local transverse axis.
mode : (m, n)
Mode indices. (1, 0) for TE10 dominant mode.
mode_type : str
"TE" or "TM". Default "TE".
direction : str
Propagation direction when the port is driven. "+x" for a left
port launching into the guide, "-x" for a right port launching
back toward the guide interior.
x_position : float | None
Physical location (metres) of the source plane along the port normal
axis. If omitted, helpers fall back to x_index * dx.
normal_axis : {"x","y","z"}
Port-normal axis. Default "x" for the original straight-guide model.
u_slice, v_slice : tuple[int, int] or None
Generic transverse aperture slices used for non-x-normal ports.
CELL spans, not node spans: these index the FIELD ARRAYS through
`_plane_indexer()`, so ``hi - lo`` is the number of aperture cells
the transverse mode operator is built on and ``sum(widths)`` over the
slice must equal ``a`` (resp. ``b``). A node span — which is what
``rfx/api/_compile.py::_range_to_slice`` and
``rfx/runners/nonuniform.py::_range_to_slice_nu`` produce, both
reporting the physical aperture as ``(hi - lo - 1) * d`` — has ONE
ENTRY TOO MANY and must be converted at the call site (issue #868:
unconverted, it put ``f_cutoff`` on a guide one cell wider than the
walls make, 10 % low at ``dx = a/9`` and 2.7 % low at ``dx = a/36``).
Create new instance of WaveguidePort(x_index, y_slice, z_slice, a, b, mode, mode_type, direction, x_position, normal_axis, u_slice, v_slice)
Compiled waveguide port config for time-stepping.
Create new instance of WaveguidePortConfig(x_index, ref_x, probe_x, y_lo, y_hi, z_lo, z_hi, normal_axis, u_lo, u_hi, v_lo, v_hi, e_u_component, e_v_component, h_u_component, h_v_component, ey_profile, ez_profile, hy_profile, hz_profile, mode_type, direction, f_cutoff, a, b, dx, dt, u_widths, v_widths, source_x_m, reference_x_m, probe_x_m, dft_total_steps, src_amp, src_t0, src_tau, src_fcen, waveform, freqs, v_probe_t, v_ref_t, i_probe_t, i_ref_t, v_inc_t, n_steps_recorded, e_inc_table, h_inc_table, aperture_dA, h_offset, mode_indices)
SMA-style coaxial probe port for cavity coupling.
Models: outer conductor (PEC cylinder), PTFE dielectric fill, center pin (PEC rod), lumped excitation gap at the base.
Parameters
position : (x, y, z) in metres Center of port on the cavity wall (gap cell location). face : str Which wall the port passes through: "top", "bottom", "front", "back", "left", or "right". pin_length : float Protrusion of center pin into the cavity (metres). Default 5 mm. pin_radius : float Center pin radius (metres). Default 0.635 mm (SMA standard). outer_radius : float Outer conductor radius (metres). Default 2.055 mm (SMA standard). impedance : float Port reference impedance in ohms. Default 50. excitation : object Source waveform (GaussianPulse, ModulatedGaussian, etc.).
Floquet port for periodic structure excitation and mode extraction.
For a unit cell with periodic BC, the Floquet port injects a plane wave at a specified scan angle and extracts reflected and transmitted Floquet modes.
Parameters
position : float Position along the propagation axis (z typically), in metres. axis : str Normal axis of the port plane ("x", "y", or "z"). scan_theta : float Scan angle theta from broadside (degrees). Default 0 (broadside). scan_phi : float Scan angle phi in the xy-plane (degrees). Default 0. polarization : str Polarization: "te" or "tm". Default "te". n_modes : int Number of Floquet modes to extract (default 1 = specular only). freqs : array-like or None Analysis frequencies (Hz). If None, auto-generated. n_freqs : int Number of frequencies if freqs is None. f0 : float or None Source center frequency. If None, uses freq_max / 2. bandwidth : float Source bandwidth. Default 0.5. amplitude : float Source amplitude. Default 1.0.
RIS unit cell simulation wrapper.
Automates: periodic BC + Floquet port + sweep bias/geometry to produce reflection phase / amplitude curves.
Parameters
cell_size : (Lx, Ly) in metres
Unit cell lateral dimensions.
substrate_thickness : float
Substrate thickness in metres.
substrate_material : str
Substrate material name (must be in MATERIAL_LIBRARY or
registered as custom). Default "fr4".
freq_range : (f_min, f_max) in Hz
Frequency range for analysis.
n_freqs : int
Number of frequency points. Default 21.
scan_angles : list of (theta, phi) or None
Default scan angles for characterisation. If None, broadside only.
polarization : str
"te" or "tm". Default "te".
n_steps : int
Number of FDTD timesteps per simulation. Default 300.
dx : float or None
Uniform cell size. Required when a varactor is added; otherwise
auto-computed if None.
cpml_layers : int
CPML layers on the port-normal axis. Default 8.
ground_plane : bool
Whether to include a PEC ground plane below the substrate.
Default True (standard RIS backed by metal).
Add a conducting element (patch, cross, etc.) to the unit cell.
Parameters
shape : Box
Geometry of the element. Coordinates are in the unit cell
frame: x in [0, Lx], y in [0, Ly], z relative to the
substrate top surface.
material : str
Material name. Default "pec".
Add a tunable varactor for reconfigurability.
The varactor is modelled as a lumped capacitor bridging a gap at the given (x, y) position on the substrate surface.
Parameters
position : (x, y) in metres
Location in the unit cell plane.
capacitance_range : (C_min, C_max) in farads
Tuning range of the varactor diode.
component : str
E-field component the varactor bridges. Default "ez".
Sweep varactor capacitance and extract reflection phase/amplitude.
Parameters
values : sequence of float Capacitance values in farads to sweep over. freq : array or None Frequency points. If None, auto-generated from freq_range.
Returns
RISSweepResult
Sweep scan angle and extract reflection vs angle.
Parameters
theta_values : sequence of float Scan angles theta in degrees to sweep over. freq : array or None Frequency points. If None, auto-generated from freq_range. phi : float Azimuth angle in degrees. Default 0.
Returns
RISSweepResult
Plot reflection phase vs frequency vs bias state.
Parameters
result : RISSweepResult
Output of sweep_capacitance or sweep_angle.
Returns
matplotlib Figure
Structured result from an RIS parameter sweep.
Attributes
phases : (n_params, n_freqs) ndarray Reflection phase in degrees. amplitudes : (n_params, n_freqs) ndarray Reflection amplitude (linear, 0-1). freqs : (n_freqs,) ndarray Frequency points in Hz. capacitances : (n_params,) ndarray or None Swept capacitance values (None for angle sweeps). angles : (n_params,) ndarray or None Swept scan angles in degrees (None for capacitance sweeps).
Assemble an x-directed waveguide S-matrix via one-driven-port-at-a-time runs.
field_dtype is forwarded to every core run; None uses float32.
Modal records preserve their precision and widen for field storage.
Spectra and S promote the record and frequency dtypes.
PEC realization (#931 §1.7): interior Box(material='pec') walls
reach this lane as the realized edge masks pec_edge_masks
(rfx.boundaries.pec.realized_pec_edge_masks), applied per step by
the shared apply_pec_edges. Until #931 the caller folded the PEC
CELL mask into sigma = 1e10 instead — a fourth realization of the
same geometry, and one that damped only the components indexed by the
occupied cell, so a one-cell-thick wall or iris got its lower face and
never its far one. An empty-guide REFERENCE run gets no interior edge
masks; per-port reference geometry supplies its own.
return_settling=True (issue #538) additionally returns the
per-driven-run pole-tail amplitude witness: (S, settling_db) with
settling_db shape (n_ports,) from
settling_db_from_port_records() (NaN per run under tracing).
Default False keeps the legacy single-array return for existing
callers, and the S values are identical either way — the witness is
post-processing of records the scan already produces.
checkpoint_segments : int or None
When set, enables segmented gradient checkpointing on each per-port
FDTD run (forwarded to rfx.simulation.run as
checkpoint=True, checkpoint_segments=K). See
Simulation.compute_waveguide_s_matrix() for full semantics.
Extract (S11, S21) with optional reference-plane shifts.
The Phase 2 cleanup (2026-04-25) removed the in-scan DFT accumulators
(cfg.v_ref_dft etc.) from WaveguidePortConfig. Callers that
used to read those fields off the config now must either:
- Pass V/I DFT spectra explicitly via the
*_dftkeyword args (used by direct unit-test mock paths and by callers that have precomputed the spectra outside the FDTD scan); OR - Use
extract_waveguide_port_waves/extract_waveguide_s_matrixwhich run the canonical post-scan rectangular full-record DFT oncfg.v_ref_t/cfg.i_ref_t.
When the explicit DFTs are not provided this function falls back to the post-scan rect-DFT path on the recorded time series.
Parameters
ref_shift : float
Metres to shift the reference-plane reporting location relative to the
stored reference probe. Positive is downstream (+x), negative upstream.
probe_shift : float
Metres to shift the probe-plane reporting location relative to the
stored probe plane. Positive is downstream (+x), negative upstream.
v_ref_dft, i_ref_dft, v_probe_dft, i_probe_dft : array or None
Optional precomputed V/I spectra at the reference and probe
planes. When all are given the wave decomposition runs against
them directly; otherwise the recorded time-series on cfg is
used.
Extract S11 from backward/forward modal amplitudes with optional de-embedding.
Extract S21 from forward-wave modal amplitudes with optional de-embedding.
Extract full N-port S-parameter matrix for WirePort objects.
Runs N simulations (one per excitation port). All port impedances are active in every run so that port loading is consistent.
Parameters
grid : Grid
materials : MaterialArrays
Base materials without port impedances folded in.
ports : list of WirePort
freqs : (n_freqs,) Hz
n_steps : int or None
Defaults to grid.num_timesteps(num_periods=30).
boundary : "pec" or "cpml"
cpml_axes : str or None: the grid's absorbing axes when omitted; subsets accepted
debye_spec, lorentz_spec : optional dispersion specs
return_vi_dump : bool
When True, return a WirePortVIReplayBundle containing the
production S-matrix plus raw midpoint-cell V/I phasors for independent
replay of the current wire-port convention.
Returns
S or WirePortVIReplayBundle
By default, returns an S-matrix. With return_vi_dump=True, the
bundle also carries raw phasors and per-port wire cell counts.
Compute Floquet S-parameters from DFT accumulators.
Experimental. This pipeline carries an E0 contract test
(tests/unit/ports/test_floquet_s_params_contract.py) — the first real pytest
caller, a passivity check on pre-isolated pure-wave input, and a
finite-grad AD smoke (the path is jnp-pure and AD-traceable) — but no
accuracy validation against an external periodic-cell solver. It also
presumes the input accumulators already isolate the incident /
reflected / transmitted waves: a naive single-run soft-source drive
does NOT (a bidirectional source leaves no pure-forward monitor plane),
so |S11| from such a drive can exceed 1. Do not build accuracy
claims on it. Tracking: https://github.com/bk-squared/rfx/issues/141
Drive-isolation scoping (2026-07-05, R2-STOP): the obvious fix — a
two-run empty-vs-device reference subtraction (E_scat = E_dev −
E_empty at a +z-side plane, the pattern that works for waveguide/coax
ports) — does NOT resolve this for a periodic Floquet cell. On an εr=4
slab vs the exact analytic Airy S11, the two-run extraction is
non-physical: |S11|≈1.5 at band-center 6 GHz where Airy=0.09 (the ~1.5
extraction-error floor swamps the small true reflection) and larger
blow-ups at band edges (small-incident normalization noise). TFSF
one-way plane-wave sources — the clean isolation route — are explicitly
barred with Floquet ports (see add_floquet_port). A real fix needs
dedicated numerics (clean incident normalization + higher-order-mode
filtering, or a Floquet-compatible one-way source), NOT a parameter
flip; and drive-isolation is the PREREQUISITE to any external (RCWA /
Meep-Bloch) accuracy validation — the external reference is moot until
FDTD yields a physical |S11|≤1 on a real periodic structure.
Uses incident, reflected, and (optionally) transmitted plane DFT data to compute S11 (reflection) and S21 (transmission).
Parameters
acc_inc : FloquetDFTAccumulator DFT at the source/reference plane (incident side). acc_ref : FloquetDFTAccumulator DFT at the reflection measurement plane. acc_trans : FloquetDFTAccumulator or None DFT at the transmission measurement plane (if present). dx : float Cell size. Lx, Ly : float Unit cell periods. freqs : (n_freqs,) array Frequencies. theta_deg, phi_deg : float Scan angles.
Returns
dict with 'S11', 'S21' (if transmission plane exists), 'freqs'.
Compute radar cross section of the scatterer defined in materials.
Parameters
grid : Grid
Simulation grid (must already include CPML padding).
materials : MaterialArrays
Material arrays with the scatterer defined (e.g., PEC regions
with very high conductivity or high eps_r).
n_steps : int
Number of FDTD timesteps.
f0 : float
Center frequency of the Gaussian pulse (Hz).
bandwidth : float
Fractional bandwidth of the pulse.
theta_inc : float
Incident angle in degrees (0 = +x propagation). Non-zero values tilt the
illumination in the x-y plane and route through the open-domain oblique
Method-B TFSF (2.5-D: open transverse y, thin-periodic z). Only the
far-field PATTERN / specular direction is validated at oblique incidence
(specular peak phi = 180 - theta_inc at theta_obs = 90 deg). Measured
envelope on the committed gate configuration (2.2-lambda plate, 700
steps, public path, corner-inclusive kernels): peak error 0 deg at
theta_inc=20 and 3 deg at theta_inc=40 (4 deg on the pre-fix exclusive
kernels), gated at +/-6 deg, with a 3-7 deg peak-azimuth
sensitivity to domain size at fixed dx/plate/CPML (PR #461 audit,
measured on the PRE-fix exclusive kernels; not re-measured in-tree
on the inclusive kernels) —
i.e. the argmax azimuth of the broad specular lobe is NOT converged to
~1 deg. See tests/oracle/test_oblique_rcs_specular.py.
ABSOLUTE oblique sigma (issue #471 F7, all numbers measured): the
normalization is the MEASURED 1D-aux incident spectrum (see
``tfsf_oblique_open.measure_incident_spectrum``), which removed a
+6.58/+4.51 dB (theta 20/40 deg) inflation the analytic normal-path
spectrum caused. Against a PO uniform-aperture oracle on the gate
grid (dx=2 mm = lambda/30): specular-peak Delta +0.86/+0.85 dB
(theta 20/40), lobe-flat over +/-12 deg. The returned sigma is that
of an aperture of height ``h_eff = (k_hi - k_lo)*dx`` of the NTFF
z-box (span-doubling ratio 4.000 measured — sigma scales as
h_eff^2); the oblique path's hardcoded 2-cell midplane box means
h_eff = 2*dx, so scale by ``(L_z/h_eff)^2`` for a physical strip
height ``L_z``. CAVEAT (measured, pinned-geometry): halving dx to
lambda/60 moves Delta to -1.55 dB — a 2.4 dB resolution
sensitivity, so treat absolute oblique sigma as a +/-2 dB-class
number at lambda/30-lambda/60, NOT sub-dB (the committed test gates
|Delta| <= 1.5 dB at the fixed gate grid as a regression lock, not
an accuracy claim). Requires ``polarization='ez'`` and a uniform
grid; other combos raise ``NotImplementedError`` (issue #404 arc).
phi_inc : float
Incident azimuth in degrees (reserved for future oblique support).
polarization : str
Electric field polarization: "ez" or "ey".
theta_obs : array or None
Observation elevation angles in radians. Default: 0 to pi, 37 points.
phi_obs : array or None
Observation azimuth angles in radians. Default: [0, pi/2].
freqs : array or None
Frequencies at which to compute RCS (Hz). Default: [f0].
boundary : str
Boundary condition type ("cpml" or "pec").
cpml_layers : int
Number of CPML layers (must match grid.cpml_layers).
tfsf_margin : int
Cells between CPML edge and TFSF boundary.
ntff_offset : int
Cells between TFSF boundary and NTFF box (NTFF must be in the
scattered-field region, i.e., outside the TFSF box). The default
(1) places the Huygens surface deep in the reactive near field.
See "Bistatic pattern caveat" below: this default is validated
for the backscatter/monostatic bin, but is NOT a validated
bistatic setup, and increasing it does not close the oblique gap
at test scale.
subtract_incident_reference : bool
If True (default False), run a second vacuum (no-scatterer) pass
with the identical TFSF+NTFF setup and subtract its far-field from
the target's at the COMPLEX level (E_scat = E_far[target] -
E_far[vacuum]) before forming the RCS. This is the standard
total-field/scattered-field normalization; it removes the full incident
field read by NTFF side faces inside the total-field slab, which
otherwise produces a spurious forward-oblique lobe (issue #280).
Doubles the solve cost. Default False keeps the validated
monostatic path byte-identical; opt in for the bistatic pattern.
Without phase_reference, monostatic_rcs is computed from the raw
(unsubtracted) run regardless of this flag, preserving the separately
checked monostatic extraction.
phase_reference : (x, y, z) in metres, optional
Opt in to a ScatteringResponse with complex scattering amplitudes
referenced to this physical point. Requires normal +x incidence, a
uniform 3-D grid with symmetric CPML, and
subtract_incident_reference=True. The x coordinate must be on an
E-node plane inside the total-field slab; it is never silently
snapped. The normal incident wave is uniform in y/z. Only finite,
positive frequencies with a nonzero incident spectrum are admitted;
the caller must additionally check adequate source bandwidth and
record/mesh convergence. Without this option the return is unchanged.
Returns
RCSResult
freqs, theta, phi, rcs_dbsm (dBsm), rcs_linear (m^2),
monostatic_rcs (dBsm, evaluated exactly at the true backscatter
direction — for +x incidence that is (theta=pi/2, phi=pi) under
the farfield r_hat convention; it does not depend on
theta_obs/phi_obs).
With phase_reference set, ScatteringResponse.rcs holds the
RCS normalized by the incident measured at that reference, consistent
with its complex F components. Frequencies are the realized NTFF bins.
Bistatic pattern caveat
RCSResult.monostatic_rcs is evaluated from the raw target run at
exact backscatter, independently of the observation grid. Its current
Mie comparison is in tests/fixtures/rcs_sphere_mie/.
The default unsubtracted rcs_dbsm / rcs_linear bistatic pattern
is NOT VALIDATED. The source's total-field slab is infinite transversely,
so the side faces of a closed NTFF box record the full incident field.
Increasing ntff_offset does not remove that contribution; off-axis
bins cannot be interpreted as the scatterer's bistatic response.
Set subtract_incident_reference=True for the reference-subtracted
path. It doubles the solve cost and subtracts a matching vacuum run at
the complex far-field level. The committed sphere comparison is in
tests/fixtures/rcs280_reference_subtraction/; its scope is limited
to that sphere, frequency, polarization, angle cut and discretization.
After subtraction, refine curved surfaces, repeat with a longer run, vary NTFF placement and CPML thickness, and enlarge the domain when deep pattern nulls matter. Compare each new target and configuration with an analytic or independent reference before treating the bistatic values as quantitative.
JAX-differentiable RCS(θ,φ) from NTFF data — the AD counterpart of compute_rcs.
compute_rcs orchestrates (TFSF setup → run → far-field → RCS) with a numpy
post-processor, so it is NOT on the AD tape. This function is the differentiable
POST-PROCESSING half: given ntff_data accumulated by a differentiable
run(..., tfsf=…, ntff=…) (whose accumulators flow through the lax.scan AD
tape w.r.t. the scatterer materials), it returns σ as a jnp array so
jax.grad flows scatterer-ε → RCS. It is the primitive behind differentiable
RCS-reduction / -shaping inverse design.
Same formula as compute_rcs (σ = 4π·|E_far|²/|E_inc|², compute_far_field's
E_θ/E_φ already carry the jk/4π factor so |E_θ|²+|E_φ|² is the
r-scaled scattered power), evaluated with compute_far_field_jax. For the
monostatic (backscatter) bin at normal +x incidence pass theta=[π/2], phi=[π].
Parameters
ntff_data : NTFFData
NTFF surface accumulators from run(..., tfsf=…, ntff=…) (JAX arrays, on tape).
box : NTFFBox
grid : Grid
theta, phi : arrays in radians
Observation directions. Backscatter for +x incidence is (π/2, π).
e_inc_amplitude : (n_freqs,) complex array
Incident plane-wave spectral amplitude for normalization — source-only, hence a
CONSTANT for the gradient. At normal incidence use
rfx.sources.tfsf.measure_normal_incident_spectrum()
(cfg, state, n_steps, freqs, dt) on the same init_tfsf config the run
uses — the incident the grid carries, as compute_rcs does. The source
waveform's own DFT (_incident_spectrum_amplitude) is about 1.1 dB larger
than that incident and makes σ low by the same 1.1 dB (issue #820).
Returns
jnp.ndarray
rcs_linear (n_freqs, n_theta, n_phi) in m², differentiable in ntff_data
(hence in the scatterer permittivity upstream). Take 10·log10 for dBsm.
See Also
compute_rcs : the numpy orchestrator (Mie-validated monostatic bin). compute_far_field_jax : the differentiable far-field this builds on.
Radar cross section computation result.
freqs : (n_freqs,) Hz theta : (n_theta,) radians phi : (n_phi,) radians rcs_dbsm : (n_freqs, n_theta, n_phi) in dBsm rcs_linear : (n_freqs, n_theta, n_phi) in m^2 monostatic_rcs : (n_freqs,) backscatter RCS in dBsm, evaluated exactly at the backscatter direction (opposite the incident propagation vector), independent of the theta/phi observation grid. For normal-incidence +x propagation this is (theta=pi/2, phi=pi), i.e. -x.
VALIDATION SCOPE (read before trusting the full pattern)
monostatic_rcs is compared with exact Mie in the committed
tests/fixtures/rcs_sphere_mie/ PEC-sphere case. The default unsubtracted
rcs_dbsm / rcs_linear bistatic pattern is NOT validated: the NTFF side faces
record the full incident field inside the total-field slab, and
increasing ntff_offset alone does not remove it. Results produced by
compute_rcs(..., subtract_incident_reference=True) use two-run complex
far-field subtraction; that path is validated only for the documented
ka~1 sphere comparison. RCSResult does not record which option produced
it, so retain the call configuration with archived results. See
compute_rcs ("Bistatic pattern caveat") for limits and convergence
checks.
Create new instance of RCSResult(freqs, theta, phi, rcs_dbsm, rcs_linear, monostatic_rcs, settling_db, settling_witness)
One incident-polarization column of a phase-referenced scattering matrix.
With the exp(+j omega t) convention, the outgoing field at
reference_position + R * s_hat is exp(-jkR) / R times
(F_theta, F_phi) * E_inc(reference_position). F has units of metres,
shape (n_freqs, n_theta, n_phi), and uses the spherical output basis
of rcs.theta/rcs.phi. Run both ey and ez for two columns.
incident_spectrum is the actual E-field DFT at that reference, not
the source waveform. Grid/record convergence still bounds accuracy.
Create new instance of ScatteringResponse(rcs, F_theta, F_phi, incident_spectrum, reference_position, polarization)
Single Debye pole parameters.
delta_eps : float Permittivity contribution Δε (dimensionless). tau : float Relaxation time in seconds.
Single Lorentz oscillator pole.
omega_0 : float Resonant angular frequency (rad/s). Set to 0 for Drude. delta : float Damping coefficient (rad/s). kappa : float Coupling strength = Δε · ω₀² for Lorentz, ω_p² for Drude.
Create a Drude pole from plasma frequency and collision rate.
Parameters
omega_p : float Plasma frequency (rad/s). gamma : float Collision rate (rad/s).
Create a Lorentz pole from physical parameters.
Parameters
delta_eps : float Permittivity contribution Δε (dimensionless). omega_0 : float Resonant angular frequency (rad/s). delta : float Damping coefficient (rad/s).
Thin conductor specification.
Parameters
shape : Shape
Geometric region of the thin conductor.
sigma_bulk : float
Bulk conductivity (S/m). e.g. copper = 5.8e7.
When >= 1e6, treated as PEC thin sheet (added to PEC mask).
thickness : float
Physical thickness in metres.
eps_r : float
Relative permittivity (default 1.0).
surface_impedance_f0 : float | Array | None
Band-centre frequency (Hz) for the opt-in Leontovich surface-
impedance loss model (issue #669). None (default) keeps the
exact legacy semantics: metal -> lossless PEC sheet, sub-threshold
-> DC lossy fold sigma_bulk*t/d. When set, the sheet is a
resistive sheet of sheet resistance Rs0 = leontovich_rs(f0,
sigma_bulk) for ANY sigma_bulk > 0, realized as
sigma_eff = 1/(Rs0*d_norm).
Whether this thin conductor should be treated as PEC.
ORDER IS LOAD-BEARING: the None-check short-circuits BEFORE the
float compare, so a traced (JAX tracer) sigma_bulk never reaches
>= when surface_impedance_f0 is set — that is what makes
sigma_bulk a legal differentiable DoF in f0 mode. Pinned by
tests/unit/materials/test_thin_conductor.py (is_pec-order pin).
The one function that turns conductor geometry into PEC E edges.
Parameters
cell_mask : (nx, ny, nz) bool array or None
Primal-cell occupancy of every PEC VOLUME (Box / Sphere / Cylinder
via sim.add, and thick PolylineWire). None when the run
has no volume conductor.
sheets : iterable of SheetSpec
PEC sheets (add_thin_conductor with PEC conductivity).
wires : iterable of WireSpec
PEC filaments.
periodic : (bool, bool, bool)
The run's per-axis periodic flags (#689). Callers on a periodic
lane MUST pass their own; the default is the non-periodic
convention.
Returns
(Mx, My, Mz) boolean arrays, grid shape: True where that E component
is zeroed. Consequences the contract test pins: a Box drawn
z_a -> z_b on node planes realizes walls at BOTH planes and shorts
every normal edge between them; a 1-cell Box is a filled slab with two
faces on every axis; a sheet is one plane with its normal edge live;
the rule is invariant under mirror and axis permutation.
Sorted node-plane indices along axis where a tangential wall exists.
A plane k counts when some E component tangential to axis (the
two components other than axis) is PEC on that plane, inside the
selection:
region— a tuple of three slices over the grid;Noneis the whole grid. Any tangential edge whose index falls in the region.ij— the two in-plane node indices of ONE column (in axis order, skippingaxis). A wall counts on the column when any tangential edge INCIDENT to the node(ij, k)is PEC, i.e.E_tat the node's own index or at the backward index alongt— so a node on the hi rim of a footprint, whose incident edges are stored ati-1, is found. The backward neighbour follows the run's #689 convention: on a periodic (or length-1) in-plane axis the backward neighbour of index 0 isn-1, so a wall on the seam node is found; on a non-periodic axis index 0 has no backward edge.
periodic is the run's per-axis flags; callers on a periodic lane
MUST pass their own (the default is the non-periodic convention). It
only affects the ij= column form — region= already scans every
stored index.
ij and region are exclusive. Consumers: preflight cavity /
guide-width checks, cv15 assert_realized_stack, MSL trace
detection, oracles.
EVERY plane, not just the two faces. A body three cells thick has a
tangential wall on all four of its node planes, because the interior
is shorted too, so this returns four indices and not two. max -
min is therefore the body's OUTER span and over-reads a guide or a
cavity bounded by thick walls: a 40-cell guide inside 3-cell walls
gives [0, 1, 2, 3] and [43, 44, 45], and 45 - 0 is 45, not
- A consumer that wants the clear opening brackets the aperture
between the hi plane of one wall group and the lo plane of the next
(
_port_transverse_spansdoes, and reads 40).
True iff the E edge component at index (i, j, k) is PEC.
component is "ex"/"ey"/"ez", "x"/"y"/"z" or
0/1/2. Wire-port live-cell logic (#556 end gap, #929 port_in_pec),
probes and sources read this instead of scanning a cell mask.
Load measured material data from a CSV file.
Supports two input formats:
- Complex permittivity: columns for frequency, real part, and imaginary part of permittivity.
- Loss tangent: columns for frequency, real part, and tan(delta).
The imaginary part is computed as
eps_r * tan_delta.
Parameters
path : str
Path to the CSV file.
freq_col : str
Column name for frequency in Hz.
eps_real_col : str
Column name for the real part of permittivity.
eps_imag_col : str
Column name for the imaginary part of permittivity.
tan_delta_col : str or None
Column name for loss tangent. If provided, eps_imag_col is ignored
and the imaginary part is derived from eps_r * tan_delta.
Returns
freqs : ndarray, shape (N,)
Frequencies in Hz.
eps_complex : ndarray, shape (N,), dtype complex128
Complex relative permittivity eps' - j*eps''.
Fit Debye relaxation model to measured permittivity data.
Uses scipy.optimize.minimize (L-BFGS-B) to find optimal Debye pole
parameters that minimize the relative RMS error.
Model: eps(f) = eps_inf + sum_k delta_eps_k / (1 + j2piftau_k)
Parameters
freqs : (N,) array Measurement frequencies in Hz. eps_measured : (N,) complex array Measured complex permittivity. n_poles : int Number of Debye poles to fit. eps_inf : float or None If provided, fix the high-frequency permittivity. If None, it is included as a free parameter (initialized from the highest-frequency measurement).
Returns
DebyeFitResult
Named tuple with eps_inf, poles (list of DebyePole), and
fit_error (relative RMS).
Fit Lorentz oscillator model to measured permittivity data.
Uses scipy.optimize.minimize (L-BFGS-B) to find optimal Lorentz
pole parameters.
Model: eps(f) = eps_inf + sum_k delta_eps_k * omega_k^2 / (omega_k^2 - omega^2 + jomegagamma_k)
Parameters
freqs : (N,) array Measurement frequencies in Hz. eps_measured : (N,) complex array Measured complex permittivity. n_poles : int Number of Lorentz poles to fit. eps_inf : float or None If provided, fix the high-frequency permittivity. If None, it is included as a free parameter.
Returns
LorentzFitResult
Named tuple with eps_inf, poles (list of LorentzPole), and
fit_error (relative RMS).
Evaluate a Debye model at given frequencies.
Parameters
freqs : (N,) array in Hz eps_inf : high-frequency permittivity poles : list of DebyePole
Returns
eps_complex : (N,) complex array
Evaluate a Lorentz or Drude pole model at given frequencies.
Parameters
freqs : (N,) array in Hz
eps_inf : high-frequency permittivity
poles : list of LorentzPole (lorentz_pole or drude_pole; a Drude
pole has omega_0 == 0 and gives eps_inf - omega_p^2 / (w^2 - jgammaw))
Returns
eps_complex : (N,) complex array
Plot measured vs fitted complex permittivity.
Creates a two-panel plot showing real and imaginary parts of permittivity as a function of frequency.
Parameters
freqs : (N,) array Frequencies in Hz. eps_measured : (N,) complex array Measured permittivity. eps_fitted : (N,) complex array Fitted permittivity from the model. ax : matplotlib Axes or None If None, a new figure with two subplots is created.
Returns
fig : matplotlib Figure
Result of a Debye pole fit.
eps_inf : float High-frequency permittivity (dimensionless). poles : list[DebyePole] Fitted Debye poles. fit_error : float Relative RMS fit error.
Result of a Lorentz pole fit.
eps_inf : float High-frequency permittivity (dimensionless). poles : list[LorentzPole] Fitted Lorentz poles. fit_error : float Relative RMS fit error.
Fit dispersive material poles to S-parameter data via jax.grad through FDTD.
Parameters
sim_factory : callable(eps_inf, debye_poles, lorentz_poles) -> Simulation
Factory that builds a Simulation with the given material
parameters. The fixture geometry, ports, and probes are set
inside this callable. It is called once outside the gradient
tape for static setup and again with traced poles for each loss
evaluation. Declarations not carried by the fit's assembly are
refused before FDTD (#1290), including wire/passive/MSL/modal
ports, plain/TFSF sources, lumped RLC, periodic overrides, Kerr,
conformal walls, point ports on PEC edges, and non-default solver,
precision, stencil or interface settings. Refinements, non-uniform
meshes, and surface-impedance sheets retain their existing refusals.
s_measured : (n_ports, n_ports, n_freqs) complex array
Measured S-parameter matrix.
freqs : (n_freqs,) array in Hz
Frequency points matching s_measured.
n_debye_poles : int
Number of Debye poles to fit.
n_lorentz_poles : int
Number of Lorentz poles to fit.
n_iterations : int
Number of Adam optimization iterations.
learning_rate : float
Adam learning rate.
initial_guess : DebyeFitResult, LorentzFitResult, or None
Initial pole parameters from fit_debye / fit_lorentz.
If None, a frequency-band-based default is used.
weight_mag, weight_phase : float
Weights for magnitude and phase in the S-parameter loss.
checkpoint : bool
Use jax.checkpoint in the FDTD loop (recommended).
verbose : bool
Print progress.
normalization : {"per_probe_max", "reference_probe"}
Loss-proxy normalization mode (issue #580; see the module
docstring's ACCURACY CAVEAT for when each is appropriate).
"per_probe_max" (default, legacy) is shape-only and BLIND to
magnitude-acting parameters such as conductivity;
"reference_probe" divides every probe spectrum, complex and
per-frequency, by the designated reference probe's spectrum, so
magnitude and inter-probe level enter the loss while the shared
drive amplitude cancels.
reference_probe : int or None
Required (and only used) with normalization="reference_probe":
index into the fixture's registered probe list of the probe that
records the reference (incident-side) signal. Must not be one of
the first n_ports probes — those are the response probes the
proxy is built from, and self-referencing would reintroduce the
degeneracy this mode exists to remove.
Returns
MaterialFitResult
final_s_params carries the LAST iteration's model-side proxy
spectrum (same normalization as the loss) — previously this field
was always None.
Result of differentiable material fitting.
Attributes
eps_inf : float High-frequency permittivity. debye_poles : list[DebyePole] Fitted Debye poles (may be empty). lorentz_poles : list[LorentzPole] Fitted Lorentz poles (may be empty). loss_history : list[float] Loss value at each iteration. final_s_params : np.ndarray or None S-parameters from the final simulation. freqs : np.ndarray Frequency array in Hz. converged : bool Whether the optimizer converged (loss plateau).
Weighted MSE loss between simulated and measured S-parameters.
Parameters
s_sim, s_meas : (..., n_freqs) complex arrays S-parameter arrays (any leading shape, typically (n_ports, n_ports, n_freqs)). weight_mag : float Weight on magnitude error. weight_phase : float Weight on phase error (wrapped to [-pi, pi]).
Returns
Scalar loss value.
Near-to-far-field transform surface (Huygens box).
Grid indices defining a closed rectangular box. Must be inside the computational domain and outside any PEC or source regions.
Create new instance of NTFFBox(i_lo, i_hi, j_lo, j_hi, k_lo, k_hi, freqs, cpml_lo_x, cpml_hi_x, cpml_lo_y, cpml_hi_y, cpml_lo_z, cpml_hi_z, face_centre, w_x_lo, w_x_hi, w_y_lo, w_y_hi, w_z_lo, w_z_hi)
Build an NTFFBox with the grid's realized per-face padding.
Physical zero is the inner edge of each lower-face pad, including a zero-cell pad. Symmetric padding keeps the legacy coordinates.
The box is built for face-centre collocation by default (the
second-order layout); pass collocation="node" to reproduce the
pre-second-order geometry.
Accumulated DFT of tangential fields on 6 faces.
x faces store [ey, ez, hy, hz], y faces [ex, ez, hx, hz], z faces [ex, ey, hx, hy]. Shape: (n_freqs, face_n1, face_n2, 4).
Kahan compensation arrays (c_*) maintain near-float64 precision in
complex64 accumulation over thousands of timesteps. The value read by the
far-field transform is x_lo (the compensated running sum); c_x_lo
is the running Kahan residual carried to the next add, not added at read.
Create new instance of NTFFData(x_lo, x_hi, y_lo, y_hi, z_lo, z_hi, c_x_lo, c_x_hi, c_y_lo, c_y_hi, c_z_lo, c_z_hi)
Far-field radiation result.
E_theta, E_phi : (n_freqs, n_theta, n_phi) complex Angular far-field components (V·s: the accumulator weights each sample by dt; the 1/r factor is omitted). theta : (n_theta,) radians phi : (n_phi,) radians freqs : (n_freqs,) Hz
Create an NTFF box from physical coordinates.
The box is built for face-centre collocation (the second-order layout);
pass collocation="node" for the pre-second-order geometry.
Compute far-field radiation pattern from NTFF DFT data.
Every face cell contributes one sample of the equivalent surface
currents J = n x H and M = -n x E, weighted by the cell's area and
phased by its position. Where that sample is taken is what decides the
order of the rule: with box.collocation == "face_centre" it sits at
the centre of the cell, matching what accumulate_ntff stored there,
and the sum is a midpoint rule — second order in the cell size. With
"node" (the legacy layout, and the default for a hand-built box) it
sits at the cell's lower-corner node, which is a left-endpoint
rectangle rule — first order.
Automatically dispatches to the JAX-differentiable implementation
when called inside jax.grad or other JAX tracing contexts.
Use this function for both post-processing and optimization objectives.
Parameters
ntff_data : NTFFData Accumulated frequency-domain tangential fields. box : NTFFBox grid : Grid theta : (n_theta,) array in radians [0, pi] phi : (n_phi,) array in radians [0, 2*pi]
Returns
FarFieldResult
JAX-differentiable far-field computation for use inside jax.grad.
Same physics as compute_far_field — including its face-cell sample
placement, which is the midpoint rule when the box says
collocation="face_centre" and the legacy corner-node rule when it
says "node" — but uses jnp throughout, enabling end-to-end
differentiation for far-field optimization.
max_phase_bytes bounds the (n_freqs, n_directions, n_cells) phase
array the transform materializes per face; the direction grid is
split to stay under it. Splitting theta is EXACT — the sum over
surface cells is independent per direction — and the chunked result
is bit-identical to the single-shot one. Pass float("inf") to
force a single pass.
Normalized radiation pattern in dB.
Returns (n_freqs, n_theta, n_phi) array.
Directivity in dBi for each frequency.
Integrates radiated power over the full sphere using gradient weights and computes D = 4π U_max / P_rad. Requires full-sphere coverage; a single phi cut assumes an axisymmetric pattern.
Returns (n_freqs,) array.
Axial ratio (AR) of far-field polarization.
AR = |E_major| / |E_minor| (≥ 1). AR = 1 for circular, ∞ for linear.
Returns (n_freqs, n_theta, n_phi) array.
Axial ratio in dB. 0 dB = circular, large = linear.
Polarization tilt angle (orientation of major axis) in radians.
Returns (n_freqs, n_theta, n_phi) array.
Polarization sense: +1 = RHCP, -1 = LHCP, 0 = linear.
Returns (n_freqs, n_theta, n_phi) array of integers.
Antenna gain G(theta, phi) for each frequency.
G = 4*pi * U(theta, phi) / P_ref
where P_ref = P_rad (IEEE gain) when input_power is None,
or P_ref = P_in (realized gain) when input_power is given.
P_rad requires full-sphere coverage (a single phi cut assumes
axisymmetry). Realized gain accepts partial angular patterns.
Parameters
ff : FarFieldResult input_power : array-like (n_freqs,) or scalar, optional Total input power in watts. When provided the result is realized gain which accounts for mismatch loss.
Returns
gain : (n_freqs, n_theta, n_phi) array (linear, not dB)
Antenna gain in dBi. Same interface as antenna_gain().
Radiation efficiency eta = P_rad / P_in.
P_rad requires full-sphere coverage (a single phi cut assumes axisymmetry), even though input_power is supplied.
Parameters
ff : FarFieldResult input_power : array-like (n_freqs,) or scalar Total input power in watts.
Returns
eta : (n_freqs,) array, values in [0, inf) theoretically in (0, 1].
Half-power (3 dB) beamwidth in degrees.
Parameters
ff : FarFieldResult
plane : "E" (phi=0 cut) or "H" (phi=pi/2 cut).
freq_idx : frequency index.
Returns
hpbw : float, degrees. Returns nan if the 3 dB points
cannot be found (e.g. omnidirectional in the cut plane).
Front-to-back ratio in dB.
F/B = max gain in forward hemisphere (theta < 90 deg) - max gain in backward hemisphere (theta > 90 deg)
Parameters
ff : FarFieldResult freq_idx : frequency index.
Returns
fb_dB : float
Impedance bandwidth where |S11| < threshold_db.
Finds the widest contiguous frequency range below the threshold.
Parameters
s11 : (n_freqs,) complex or real. If complex, magnitude is computed. If real, assumed to be in dB. freqs : (n_freqs,) Hz. threshold_db : float Threshold in dB (default -10 dB).
Returns
BandwidthResult
Impedance bandwidth extraction result.
center_freq : float, Hz bandwidth_hz : float, Hz fractional_bandwidth : float (ratio, e.g. 0.10 = 10 %) freq_lo : float, Hz — lower edge freq_hi : float, Hz — upper edge
Multi-panel antenna summary figure.
Panels:
- Radiation pattern (polar, E-plane and H-plane)
- Gain vs theta (rectangular)
- S11 vs frequency (if provided)
- Efficiency vs frequency or gain statistics
Parameters
ff : FarFieldResult s11 : (n_freqs,) complex or real, optional freqs : (n_freqs,) Hz for the S11 data, optional freq_idx : frequency index for pattern plots. input_power : for realized gain computation, optional.
Returns
matplotlib Figure
Rectangular region where material parameters are optimizable.
Parameters
corner_lo, corner_hi : (x, y, z) in metres Bounding box of the design region. eps_range : (eps_min, eps_max) Permittivity bounds. The optimizer works in a continuous latent space and projects to this range via sigmoid.
Result of an optimization run.
Attributes
eps_design : (nx_d, ny_d, nz_d) float array Optimized permittivity in the design region. loss_history : list of float Objective value at each iteration. latent : jnp.ndarray Final latent parameters (pre-sigmoid).
Run gradient-based optimization on a design region.
Refuses simulations with refinement, as the underlying forward()
path has no subgridded solve.
Parameters
sim : Simulation
Base simulation (the design region overrides eps_r in its box).
region : DesignRegion
Design region specification.
objective : callable(Result) -> scalar
Objective function to minimize. Must be JAX-differentiable
through the simulation.
n_iters : int
Number of optimization iterations.
lr : float
Learning rate (Adam).
init_latent : array or None
Initial latent parameters. If None, initialized to zeros
(maps to midpoint of eps_range).
n_steps : int or None
Number of simulation timesteps per iteration. Controls memory
usage — fewer steps = less memory for jax.grad. If None,
auto-computed from num_periods.
num_periods : float
Periods at freq_max for auto n_steps (default 20). Reduce to
10 for lower memory usage with minimal accuracy loss.
verbose : bool
Print progress every 10 iterations.
n_warmup : int
Forwarded verbatim to sim.forward(n_warmup=...) at every
iteration (issue #40). On a non-uniform / distributed-non-uniform
sim, this trades an APPROXIMATE (truncated) gradient for
reduced reverse-mode memory/compute — see Simulation.forward's
own n_warmup docstring for the measured error curve (issue
#626 part 2); a design loop built on this objective inherits that
bias, not just a single forward call, so verify the bias is
acceptable for your loss window before using a nonzero value here.
On a uniform-mesh sim it raises NotImplementedError
(issue #626 — previously a silent no-op). Default 0 (no
warmup split, full gradient).
port_s11_freqs : array-like or None
Frequencies (Hz) at which to accumulate per-port V/I DFTs in the
JIT scan body so that result.s_params is populated with
wave-decomposition |S11| values. Required when the objective is
minimize_s11_at_freq_wave_decomp() (issue #72).
checkpoint_segments : int or None
If set, route sim.forward through the segmented-checkpoint
path (issue #73) which trades ≈2× compute for ≈√n_steps memory.
Must divide n_steps; useful when n_steps is large enough
that the linear-memory scan would OOM. None (default)
preserves the legacy per-step jax.checkpoint behaviour.
Currently wired only on the uniform single-device forward path.
Non-uniform meshes and ``distributed=True`` will raise
``NotImplementedError``; NU support is tracked as a follow-up
on issue #73.
n_starts : int
Number of random-initialized restarts (opt-in; default 1 = the
legacy single run). Start 0 always uses init_latent (so
n_starts=1 is byte-identical to before this parameter existed);
starts 1..N-1 draw i.i.d. standard-normal latents from seed.
The best restart by final loss (or best-visited loss when
best_iterate=True) is returned. Promoted from the MSL
open-stub multimodal example (issue #171); the surface must be
multimodal for extra starts to help.
best_iterate : bool
If True, return the lowest-loss iterate visited during the run
rather than the final iterate (default False = final iterate =
legacy behaviour). Guards the sharp-null overshoot the #171 GPU
run caught. Selection among n_starts restarts uses the same
best-visited loss when this is True.
step_clamp : float or None
If set (>0), clamp the L2 norm of each Adam latent update to this
value, rescaling the whole step (direction preserved) when it
overshoots. Default None = unclamped = legacy behaviour. Generic
analogue of the example's physical-length clamp.
seed : int
PRNG seed for the extra n_starts restart inits (deterministic /
reproducible). Unused when n_starts=1.
jit : bool
Compile the loss and its gradient once per optimize() call and
reuse the program in every iteration and every start (#1225).
Default False runs jax.value_and_grad eagerly, which compiles
the whole FDTD solve again on every iteration — on a board-sized
model most of the iteration's wall time.
With ``jit=True`` the objective is traced, not run, so it must not
read a traced value on the host (``float(...)``, ``np.asarray(...)``,
a Python ``if`` on an array). Wire ports on either mesh and lumped
ports on a graded mesh trace since #1225. Known exception: a model
with an MSL port —
its port set-up reads the realized PEC mask on the host
(``rfx/sources/msl_port.py``) and the MSL wave-decomposition
extractor converts a grid spacing with ``float()``
(``rfx/sparams/_common.py:143``); under ``jit=True`` the call stops
with ``TracerArrayConversionError`` or ``ConcretizationTypeError``.
Use the default there.
The jitted loss and gradient can differ from the eager ones in the
last float32 bits, because XLA compiles the whole step as one
program. Measured on CPU in float32 ULP at the peak of each array
(``max|eager - jitted|`` over the spacing at ``max|eager|``): 0 to 9
in the loss and up to ~20 in a design-region gradient on the models
tested, and ~150 in the loss of a waveguide-port model. These are
measurements, not a bound: on a wire-port model where two parts of
the objective's gradient partly cancel at the port cell, the
whole-grid permittivity gradient differed by 12 to 56 ULP at the
peak. The optimisation path can therefore drift from an eager run's
at that level.
Returns
OptimizeResult
For n_starts>1 this is the winning restart's result
(loss_history is that restart's own trajectory).
Result of AD-vs-FD gradient comparison.
Attributes
ad_grad : jnp.ndarray Gradient from reverse-mode autodiff. fd_grad : jnp.ndarray Gradient from central finite differences. relative_error : float max |ad - fd| / (max |ad| + max |fd| + 1e-30).
Compare AD gradient with finite-difference gradient.
Runs the simulation via sim.forward() with permittivity
overrides derived from design_params, computes the gradient
of objective_fn with respect to design_params using both
reverse-mode AD and central finite differences, and returns
the comparison.
Parameters
sim : Simulation
Configured simulation (ports, probes already added).
design_params : jnp.ndarray
Flat array of design parameters. These are added element-wise
to the baseline eps_r from sim._assemble_materials().
Shape must broadcast with grid.shape.
objective_fn : callable(SimResult) -> scalar
Differentiable objective function.
eps : float
Finite-difference perturbation (default 1e-3).
n_steps : int or None
Number of timesteps. Passed to sim.forward().
num_periods : float
Periods at freq_max if n_steps is None.
Returns
GradientCheckResult Contains ad_grad, fd_grad, and relative_error.
One resolution stage in a progressive_optimize schedule.
Attributes
dx : float
Target cell size for this stage (metres). Passed to sim_factory.
n_iters : int
Number of Adam iterations at this resolution.
lr : float
Learning rate for this stage (defaults to 0.01).
num_periods : float
Periods at freq_max for auto n_steps computation (default 20).
n_steps : int or None
Explicit step count override. None -> auto from num_periods.
Result of a progressive_optimize run.
Attributes
stages : list of OptimizeResult
Per-stage optimize() output, in schedule order.
final_eps_design : jnp.ndarray
Optimized permittivity at the finest stage's design-region shape.
final_latent : jnp.ndarray
Final latent at the finest stage's shape.
loss_history : list of float
Concatenated loss across all stages.
stage_boundaries : list of int
Iteration index where each stage starts (cumulative sum of
n_iters). Useful for plotting.
Progressive multi-resolution inverse design (issue #42).
Runs optimize() at each stage in schedule, upsampling the
latent between stages. Coarse stages produce a fast loss-landscape
scan; finer stages refine on top.
Parameters
sim_factory : callable(dx) -> Simulation
Builds a Simulation at the given dx. Called once per stage.
region : DesignRegion
Design region in physical coordinates (resolution-independent).
objective : callable(Result) -> scalar
JAX-differentiable loss.
schedule : list of ProgressiveStage
Must be non-empty. Stages usually go coarse -> fine.
init_latent : jnp.ndarray or None
Initial latent at the FIRST stage's design-region shape.
If None, optimize() initialises to zeros.
interp_method : str
Latent-upsample interpolation ("linear", "nearest", "cubic").
verbose : bool
Print per-stage progress banner.
n_warmup : int
Forwarded to each stage's optimize(..., n_warmup=...) call —
see optimize()'s own n_warmup entry for the full
behaviour (issue #40/#626): an approximation on non-uniform
meshes, a fail-loud NotImplementedError on uniform ones.
Returns
ProgressiveOptimizeResult
Notes
The design variable basis is the per-stage latent (not a stage-
independent per-wavelength grid). jax.image.resize bridges
stages, which is adequate when the mesh refinement is modest.
For dramatic resolution jumps, consider a dedicated per-wavelength
basis.
Density-based design region for topology optimization.
Each cell in the region has a continuous density rho in [0, 1]. rho=0 corresponds to material_bg (background) and rho=1 to material_fg (foreground).
Parameters
corner_lo, corner_hi : (x, y, z) in metres Bounding box of the design region. material_bg : str Background material name (from MATERIAL_LIBRARY). material_fg : str Foreground material name. filter_radius : float or None Radius (in metres) for the cone density filter. If None, no filtering is applied. beta_projection : float Initial sharpness of the threshold projection. beta=1 is gentle; beta>=64 is near-binary. min_feature_size : float or None Alias for filter_radius (diameter = 2 * filter_radius). If provided, overrides filter_radius.
Result of a topology optimization run.
Attributes
density : jnp.ndarray Final (raw) density field in the design region. density_projected : jnp.ndarray Final density after filtering and projection. eps_design : jnp.ndarray Final permittivity distribution in the design region. history : list of float Objective value at each iteration. beta_history : list of float Projection sharpness at each iteration. final_result : object Simulation result from the last iteration.
Run density-based topology optimization.
Optimizes a continuous density field within the design region to
minimize an objective function, using gradient information from
jax.value_and_grad through the full FDTD pipeline.
Uses optax.adam for the optimizer.
Parameters
sim : Simulation Base simulation (the design region overrides eps_r in its box). design_region : TopologyDesignRegion Region and material specification. objective : callable(Result) -> scalar Objective function to minimize. Must be JAX-differentiable through the simulation. n_iterations : int Number of optimization iterations. learning_rate : float Learning rate for Adam optimizer. beta_schedule : list of (iteration, beta) or None Schedule for increasing projection sharpness. Default: [(0, 1), (30, 4), (60, 16), (80, 64)] init_density : array or None Initial density field. If None, initialized to 0.5 everywhere (uniform mixture of bg and fg materials). verbose : bool Print progress every 10 iterations.
Returns
TopologyResult Contains final density, permittivity, loss history, and beta history.
Backward-compatible density -> (eps, sigma) helper.
Minimize |S11|² over specified frequencies.
Returns a callable objective(result) -> scalar that computes the
mean |S11|² across the nearest frequency bins. If the mean S11 is
already below target_db the loss is clamped to zero.
Parameters
freqs : array-like, shape (n,) Target frequencies (Hz) over which to minimize S11. target_db : float Threshold in dB. When mean |S11|² is below this, loss = 0.
Returns
callable(Result) -> scalar (JAX-differentiable)
Maximize |S21|² (transmission) over specified frequencies.
Returns -mean|S21|² so that minimizing this objective
maximizes transmission.
Parameters
freqs : array-like, shape (n,) Target frequencies (Hz).
Returns
callable(Result) -> scalar (JAX-differentiable)
Minimize |Z_in − z_target|² at a specific frequency.
Computes input impedance from S11: Z_in = Z0 * (1 + S11) / (1 − S11)
where Z0 = 50 Ω (standard reference impedance).
Parameters
freq : float Target frequency (Hz). z_target : float Desired input impedance (Ω).
Returns
callable(Result) -> scalar (JAX-differentiable)
Maximize the bandwidth where |S11| < threshold.
Evaluates S11 over a frequency band [f_center − f_bw/2, f_center + f_bw/2]
and returns a soft loss that is lower when more frequency bins satisfy
|S11|_dB < s11_threshold.
The loss is computed as the mean of max(|S11|²_dB − threshold, 0)
across the band, so it drives the optimizer to push all bins below
threshold simultaneously, effectively maximizing matched bandwidth.
Parameters
f_center : float Center frequency (Hz). f_bw : float Bandwidth span (Hz). s11_threshold : float Threshold in dB (default −10 dB).
Returns
callable(Result) -> scalar (JAX-differentiable)
Maximize directivity in a target direction (ratio-based, scale-invariant).
Computes the directivity ratio U(θ_target, φ_target) / P_rad where
U = |E_θ|² + |E_φ|² is the radiation intensity and P_rad is
the total radiated power integrated over the FULL sphere (the same
coverage as farfield.directivity() and antenna._total_radiated_power,
retaining this objective's JAX trapezoid endpoint weights — so the
optimized quantity is the true directivity, not an upper-hemisphere proxy
that would inflate D for antennas with any back radiation). The ratio is
scale-invariant, so the absolute magnitude of the NTFF spectral integral
drops out — gradients reflect pattern shape only.
Prior versions used absolute |E|² at the target direction, which
is ~1e-27 in rfx's spectral NTFF convention and produces zero
gradients in topology_optimize (GitHub issue #32).
Parameters
theta_target : float Polar angle in radians [0, π]. phi_target : float Azimuthal angle in radians [0, 2π]. n_theta : int, optional Number of polar samples in [0, π] for the full-sphere P_rad integration (default 37 → 5° spacing). n_phi : int, optional Number of azimuthal samples in [0, 2π] (default 73 → 5° spacing).
Returns
callable(Result) -> scalar (JAX-differentiable)
Parameters (continued)
log_ratio : bool, optional
Default True: optimize -(log U - log P) — the full, sign-correct
quotient gradient U'/U - P'/P that is right for every degree of
freedom (including power-changing ones). log_ratio=False is refused
because it gives wrong-sign gradients for power-changing variables.
eps : float, optional
Floor for the log_ratio log arguments (default 1e-37). It only
guards log(0); the log-ratio gradient U'/U is scale-invariant, so
eps must be BELOW the working U/P magnitude (rfx spectral NTFF U,P are
~1e-27 in full antenna runs, ~1e-32 in small/early sims). A floor at or
above U/P (e.g. the old 1e-30 on a 1e-32 sim) clamps the log argument and
zeros the gradient — keep eps well below min(U, P).
Notes
The removed log_ratio=False mode omitted the denominator derivative,
giving wrong-sign gradients for power-changing variables. Use the default.
Time-domain S11 proxy: minimize late-time reflected energy at port.
Computes the ratio of energy in the second half of the probe time series (dominated by reflections) to energy in the first half (dominated by the incident pulse). Minimizing this ratio drives the optimizer toward better impedance matching.
This objective works with optimize() and topology_optimize()
because it uses only result.time_series (no S-parameters needed).
The "late half = reflection" split is only valid when the round trip
from the port to the reflecting feature and back arrives after the
split (1 - late_fraction of the series) and before the run ends.
On a short round-trip geometry (thin substrate, feature near the port)
the reflection lands in the early window and the late window is nearly
empty, so this ratio collapses toward 0 and its gradient is numerical
noise -- with no error raised. Sanity-check the returned magnitude (a
meaningfully reflecting design sits near 1e-2--1e-1, not ~1e-7), then fix
an empty window by its mode: enlarge n_steps if the reflection
arrives after the run ends; RAISE late_fraction (which moves the
split earlier) if it arrives before the split; if the incident pulse and
reflection overlap in time on a very short round trip, no split separates
them -- narrow the source bandwidth or use an impedance-referenced port.
An AD-vs-finite-difference check does NOT catch this: both differentiate
the same empty window and agree.
Parameters
port_probe_idx : int
Index into result.time_series columns identifying the probe
co-located with the excitation port (default 0).
late_fraction : float
Fraction of the time series considered "late" (default 0.5).
A value of 0.5 means the second half is treated as reflection.
Returns
callable(Result) -> scalar (JAX-differentiable)
Time-domain S21 proxy: maximize energy at an output probe.
Returns the negated total squared energy at the output probe, so that minimizing this objective maximizes transmission.
This objective works with optimize() and topology_optimize()
because it uses only result.time_series (no S-parameters needed).
Parameters
output_probe_idx : int
Index into result.time_series columns for the output probe.
Default -1 (last probe).
Returns
callable(Result) -> scalar (JAX-differentiable)
Steer radiation toward target_probe_idx and away from suppress_probe_idx.
Uses a near-field probe array as a differentiable surrogate for far-field beam steering. Place probes at different spatial positions around the antenna (e.g., above-left and above-right), then this objective maximizes the power ratio between target and suppressed probe.
This avoids NTFF DFT entirely and works reliably in float32.
Setup example::
# Probes at different angles in the near field
sim.add_probe((x_center - 0.02, y_center, z_above), "ez") # probe 0: left
sim.add_probe((x_center + 0.02, y_center, z_above), "ez") # probe 1: right
# Steer toward probe 1 (right), suppress probe 0 (left)
obj = steer_probe_array(target_probe_idx=1, suppress_probe_idx=0)
Parameters
target_probe_idx : int
Column index in result.time_series to maximize.
suppress_probe_idx : int
Column index to suppress (default 0).
late_fraction : float
Fraction of time series to use (late portion, after source decays).
Returns
callable(Result) -> scalar (JAX-differentiable)
Result accessor for one or more registered DFT-plane probes.
Parameters
names : str or sequence of str
The name= (or auto-generated default) given to
add_dft_plane_probe(...). A single name returns the raw
(n_freqs, n1, n2) complex accumulator directly (no dict
wrapping -- there is nothing to disambiguate). A sequence of names
returns either a stacked array or a dict, controlled by stack.
stack : bool
When names is a sequence and stack=True (default), the
accessor stacks all requested planes into one
(n_names, n_freqs, n1, n2) array ALONG A NEW LEADING AXIS --
but only when every requested plane has the same
(n_freqs, n1, n2) shape (planes at different axes/coordinates
commonly do not, e.g. a yz-plane vs an xz-plane monitor). When the
shapes differ, stacking is impossible and the accessor RAISES
ValueError naming the mismatched shapes and pointing at
stack=False; it never silently truncates or broadcasts. Pass
stack=False explicitly to opt into the per-name
dict[name] -> array form instead (also the unconditional return
form when names is a sequence and stack=False, even if the
shapes happen to match). Mixed-dtype stacking (e.g. a complex64
plane alongside a complex128 one, possible if planes were
registered under different jax_enable_x64 states) follows
ordinary jnp.stack dtype promotion -- the result takes the
wider dtype.
Returns
callable(result) -> jnp.ndarray | dict[str, jnp.ndarray]
JAX-differentiable: the returned array(s) are views/stacks of the
same accumulator that was carried through the FDTD scan, so
jax.grad flows through them unchanged.
Raises
ValueError
If result carries no dft_planes at all, is missing one of the
requested names, or (list form, stack=True) the requested
planes' shapes do not match -- naming the fix in each case.
Objective factory: sum(|field|**2) over one or more DFT planes.
Sums squared magnitude over every element (space AND frequency) of
every requested plane. Trivially smooth (a sum of squares is C-infinity
in the accumulator), so this is a good first objective to sanity-check
a new DFT-plane setup with before reaching for field_softmax().
Parameters
names : str or sequence of str
Plane name(s) to sum over -- see dft_field(). Shapes need not
match between planes (unlike dft_field(..., stack=True)): each
plane's energy is summed independently, so mixing e.g. a yz-plane
and an xz-plane monitor is fine.
weights : dict[str, float] or None
Optional per-plane scalar weight (default 1.0 for every plane not
listed), applied before summing -- e.g. to down-weight a monitor
plane closer to the source.
Returns
callable(Result) -> scalar (JAX-differentiable)
Objective factory: smooth (soft) max of |field|**2 over space+frequency.
Uses the standard log-sum-exp soft-max, AUTO-SCALED to the field's own
magnitude each call: beta_eff = beta / stop_gradient(max(vals)),
softmax = logsumexp(beta_eff * vals) / beta_eff, computed with
jax.nn.logsumexp for numerical stability. As beta (equivalently
beta_eff * max(vals), which the normalization pins to exactly
beta by construction -- see "Auto-scaling", below) grows this
converges to the true max(|field|**2); a smaller beta trades a
looser max approximation for a smoother, better-conditioned gradient
(useful early in an optimization run, e.g. the shielding case: minimize
the worst-case leaked field anywhere behind a barrier, over all
monitored frequencies, rather than the mean leaked field).
Parameters
names : str or sequence of str
Plane name(s) to pool over -- see dft_field(). All requested
planes' elements (space AND frequency, across every plane) are
pooled into ONE soft-max, so this composes the register-N-planes
pattern: register one plane per physical monitor location, then
soft-max over all of them at once for a single worst-case scalar.
beta : float
Soft-max sharpness (default 1.0), now DIMENSIONLESS -- see
"Auto-scaling" below. Must be positive.
Auto-scaling (fixes a measured footgun -- read before raising beta
"to be safe")
Earlier versions of this function computed logsumexp(beta*vals)/beta
directly against the RAW |field|**2 values, so beta had to be
hand-matched to whatever physical units/magnitude the accumulator
happened to carry (rfx's own DFT-plane accumulators commonly sit
around |field|**2 ~1e-10 to 1e-22 depending on
geometry/normalization). At the then-default beta=1.0 the
objective sat at ~100.000002% of the design-independent constant
log(count)/beta (measured, #619): both the value and its
gradient were differentiating ROUNDING NOISE in that constant, not
physics, and it could coincidentally still pass a single
finite-difference check while failing at a different step size (a
feedback_gate_can_bind_artifact-class trap).
This function now computes beta_eff = beta / stop_gradient(max(vals))
and uses beta_eff in place of a raw beta everywhere above.
Because vals' own current max scales out of the product exactly,
beta_eff * max(vals) == beta ALWAYS (BEFORE any overflow -- see
the next paragraph), regardless of the field's physical units -- the
log(count)/beta swallow above cannot recur at any REPRESENTABLE
(nonzero) field magnitude, at any beta, including the default. This
is scoped deliberately: below the working dtype's smallest
representable magnitude (~1.4e-45 for float32 subnormals) vals
itself underflows to a literal 0 array, the degenerate all-zero
fallback below kicks in (beta_eff = beta, i.e. UNSCALED), and the
original swallow pattern can in principle recur there -- rfx's own
realistic DFT-plane field magnitudes (|field|**2 ~1e-10 to
1e-22) sit ~23-35 orders of magnitude above that floor, so this is a
dtype-representability edge, not a practical concern at any field
magnitude this simulator actually produces. stop_gradient on
the normalizer is deliberate: without it, the returned scalar would
differentiate the (physically meaningless) rescaling itself rather
than approximating max; with it, the gradient is the standard
softmax-weighted average at the CURRENT call's beta_eff (a real,
directionally-correct signal, recomputed fresh -- and re-normalized --
every call).
What beta now controls (and does not control): a larger beta
tightens the approximation -- the softmax value can exceed the true
max(vals) by a factor of at most 1 + log(count)/beta (default
beta=1.0 is therefore LOOSE for a field with many samples: e.g.
~14.8x at count=1e6), so raise beta (5-50 is a reasonable range
for most plane sizes) for a tighter approximation once you know you
need one, exactly as before -- and the DEFAULT is now safe at any
field magnitude, merely loose, never rounding-noise.
The top-end overflow footgun MOVED, it did not disappear --
and it now has TWO thresholds, not one. Auto-scaling divides by
max(vals) instead of multiplying by the raw field magnitude,
so a large enough beta against a physically tiny field can
still overflow -- now in computing beta_eff = beta /
max(vals) itself, before vals is even touched. Measured at
max(vals) ~2.24e-22 (this repo's realistic DFT-plane range):
- Value overflow (
beta_effitself overflows).betaat or above ~1e17 makesbeta_effexceed float32's range, previously returningvalue=nan, grad=nansilently, no exception. This function guards it: whenbeta_effwould be non-finite, it is clipped to the largest finite value the working dtype represents (jnp.finfo(vals.dtype).max) rather than propagatingnan. The clipped VALUE is accurate (still converges to the truemax(vals), verified atbetaup to 1e38). - Gradient collapse, measured to start EARLIER (~1e15-1e17)
and NOT covered by the value clip above. Even where the
value stays finite and correct, the returned gradient
silently drops to EXACTLY
0.0somewhere in this range -- the precise threshold is FIXTURE-DEPENDENT (measured to shift by up to 10x between two fixtures differing only in element values, so no single number is trustworthy across geometries) -- a sharp cliff, not a gradual falloff, correct to machine precision on one side and exactly0.0on the other. A reciprocal-of-a-huge-number precision limit in the VJP's1/beta_effterm, distinct from (and tighter than) the value-overflow threshold above. This is NOT fixed by the value clip (it happens at a lower beta than clipping even engages) and is not attempted here: it would need a different numerical formulation (e.g. normalizingvalsbyscalebefore scaling by beta, which trades this gradient cliff for a symmetric one on the VALUE side at extreme beta / tiny-field combinations -- no formulation tried avoids both).
Practical guidance: both thresholds sit many orders of magnitude above the 5-50 dimensionless range recommended above, and above the 100-1e6 range this repo's own re-calibrated tests and examples actually use -- treat beta past ~1e6 as unverified, not merely "less sharp than useful."
Batched forward-mode Jacobian of sim_fn at params (issue #577).
jax.jvp / jax.jacfwd / jax.vmap(jax.jvp) already run
end-to-end through sim.forward(eps_override=...) ->
dft_field() / field_energy() / field_softmax() today,
with zero solver changes -- this function is a thin, gated PACKAGING
layer over that stock-JAX capability (tangent-basis construction,
dtype/shape validation, primal de-duplication, sequential-vs-batched
dispatch), not a new AD engine. It computes
``value, jacobian = jacobian_fwd(sim_fn, params)``
where value = sim_fn(params) (bit-identical to calling sim_fn
directly -- see the API trap note below) and jacobian has the SAME
pytree structure as value (sim_fn's OUTPUT, not params -- this
is what jax.jvp's own tangent output mirrors, and this function
does not reshape it into a params-keyed structure the way
jax.jacfwd does), with every leaf gaining one new LEADING axis of
size n_t: jacobian (or jacobian[k] for an output pytree
with leaf k) has shape (n_t,) + value.shape, row i holding
the directional derivative of value along tangent direction i.
With the default tangents="identity", direction i corresponds
to params' i-th flattened leaf (jax.tree_util.tree_flatten
order) -- i.e. jacobian[i] is d(value)/d(params_leaf[i]).
Mechanism: jax.vmap over the TANGENT argument of jax.jvp (NOT
jax.linearize). Measured on the real uniform-lane FDTD scan: the
vmap(jvp) jaxpr contains exactly ONE lax.scan (the FDTD time
loop) whose carry holds one UNBATCHED copy of the field state (the
primal sweep) plus n_t BATCHED tangent copies -- the primal sweep
really is shared across every tangent direction, which is the premise
577 asked to verify. jax.linearize was measured to cost 24x more
peak memory on the same fixture (8654 MB vs 355 MB at n_steps=600, n_t=10) because it materialises the whole scan's primal residuals -- i.e. the reverse-mode tape -- which forward mode never needs.
API TRAP (verify this, do not assume it): a naive
jax.vmap(jax.jvp(...)) ALSO stacks the primal, returning a
(n_t,) + value.shape array of n_t copies of the same value.
This function passes out_axes=(None, 0) to jax.vmap so the
returned value is the single, unstacked primal -- but that only
works because the primal computation genuinely does not depend on
which tangent direction is being evaluated. If you change sim_fn
in a way that breaks that invariant, jax.vmap raises loudly
(out_axes=None on an output that DOES depend on the mapped axis is
a vmap error, not a silent wrong answer). batch_tangents=False
(the sequential path) has NO jax.vmap of its own to inherit that
check from -- it just calls jax.jvp n_t times in a plain
Python loop and returns values[0], so without an explicit guard a
sim_fn that violates the invariant would silently return one
arbitrary tangent row's primal on that path instead of raising. This
function closes that gap on the sequential path via two guards, so
the memory-vs-speed knob is not also a safety-vs-speed knob: (1) a
jax.eval_shape abstract trace of the SAME out_axes=(None, 0)
vmap the batched path actually runs -- shape-only, zero FLOPs, zero
extra memory (verified: reproduces the identical
ValueError a real violating vmap call raises, including when this
whole call is itself wrapped in an outer jax.jit by the caller,
since eval_shape is trace-time like vmap's own check); (2)
when running eagerly (params are not themselves tracers, so the
computed primal values are concrete), an exact pairwise comparison of
all n_t sequentially-computed primal values, raising ValueError
on any mismatch -- a strictly stronger, numeric confirmation of the
same invariant, skipped only when it is not expressible (under an
enclosing jax.jit, where guard (1) is the check in effect).
Cost characterization: NOT "a few times one solve" -- see
scripts/benchmark_jacobian_fwd.py (regenerable; run it for current
numbers on your hardware/grid) and the CHANGELOG entry for this issue
for measured wall-time/flops/memory ratios. The honest summary: wall
time and flops scale roughly as (1 + n_t) plain solves and that
ratio MOVES with grid size (do not trust one ratio across problem
sizes); memory is the argument FOR this mode, not speed -- forward-mode
peak is O(state) * (1 + n_t) and independent of n_steps, while
reverse mode without segmented checkpointing is tens of times one
plain solve even for a single scalar output, and jax.jacrev refuses
a complex-valued observable outright (raises TypeError without
holomorphic=True). Numbers are never published here -- see the
anti-rot rule in the module/CHANGELOG instead of trusting a docstring
number, which rots.
Parameters
sim_fn : Callable
params -> value, JAX-differentiable (typically
lambda p: dft_field(name)(sim.forward(eps_override=eps(p), ...))
or the same composed with field_energy() /
field_softmax()). Called under jax.jvp, so it must be
traceable the same way any jax.grad/jax.jvp target is.
params : Any
A JAX pytree of the design variables to differentiate with respect
to. With the default tangents="identity", every leaf must be a
floating-dtype SCALAR (shape ()) -- see _identity_tangent_batch().
A flat (n_p,) array (one non-scalar leaf) needs the explicit
tangents= escape hatch below.
tangents : "identity" or a pytree, optional
"identity" (default): build one one-hot tangent direction per
scalar leaf of params (n_t = number of leaves); fails loud
(ValueError) if any leaf is non-scalar or non-floating -- see
_identity_tangent_batch()'s docstring for the measured trap
this avoids. Otherwise: an explicit tangent BATCH -- a pytree with
the same structure as params, every leaf carrying one extra
LEADING axis of a common size n_t (e.g. tangents=jnp.eye(n_p)
when params is a flat (n_p,) array, for the full Jacobian, or
any (n_t, n_p) slice of it for a subset of columns).
batch_tangents : bool, optional
True (default): run all n_t tangent directions as ONE
jax.vmap(jax.jvp(...)) call -- lower wall time (measured 2.3-3.1x
faster than the sequential control at n_t in {4, 10}), higher peak
memory (O(state) * (1 + n_t), all n_t tangent copies live
at once). False: run the n_t directions as independent
sequential jax.jvp calls in a plain Python loop -- the
memory-vs-speed knob, trading the wall-time win for a peak that
scales with ONE tangent copy at a time instead of n_t
(measured: primal-sharing still holds sequentially, at ~1 + n_t
compute but with a lower peak than the batched path scales to as
n_t grows). CORRECTION: this function does not call
jax.jit itself on EITHER path -- there is no jax.jit
CALL anywhere in this module (verify:
grep -nE '@jax\.jit|jax\.jit\(' rfx/observables.py returns
nothing -- a plain grep jax.jit also matches this sentence's
own prose, so it is not a useful self-check). batch_tangents=False runs the n_t
sequential jax.jvp calls exactly as written, at whatever JIT
boundary the CALLER supplies (eager if the caller does not
jax.jit anything; compiled as part of one program if the
caller wraps its own outer function, as
scripts/benchmark_jacobian_fwd.py and
tests/unit/autodiff/test_jacobian_fwd.py both do for compiled/jaxpr
measurement) -- an earlier version of this docstring claimed
jax.jit wrapping was part of this function's own behaviour,
which was never true. The two paths (batched vs. sequential) are
DIFFERENT XLA programs and may disagree at float32 reassociation
scale (~1e-6 relative); they are not bit-identical. See "Fail-loud
fences" below for how the two paths' SAFETY differs, not just
their performance.
Returns
(value, jacobian) : tuple
value: the single primal, sim_fn(params) bit-identical
(verify this in your own test if you change sim_fn's structure --
see the API trap above). jacobian: a pytree mirroring value
(sim_fn's output, NOT params -- see the paragraph above the
signature), each leaf an array of shape (n_t,) + value.shape.
Complex-Jacobian convention
For a complex-valued value (e.g. dft_field()'s output), the
returned Jacobian is dy/dx UNCONJUGATED -- exactly what forward-mode
AD produces for a real input tangent and a complex output. This
DIFFERS BY A CONJUGATE from anything derived through jax.vjp /
jax.grad on the same computation (measured: 0.02% relative error
against an unconjugated central-difference reference, 198% against the
conjugated one). If you compare this Jacobian against anything built
from reverse-mode AD, conjugate one side first. This is also why
jax.jacrev cannot be used as a drop-in alternative here: it refuses
a complex-dtype output outright (TypeError, needs
holomorphic=True) -- forward mode is the only stock-JAX mode that
hands back this Jacobian without a holomorphy declaration.
Fail-loud fences
Three configurations are unsafe to combine with jacobian_fwd on
sim_fn bodies built on Simulation.forward(...). This function
is intentionally generic over sim_fn (it never sees forward()'s
own keyword arguments -- they live inside your closure), so the first
two are enforced by a RAISE that propagates up through jax.jvp
from forward()'s own pre-existing checks; the remaining one has no
raise to inherit and cannot be intercepted from outside the closure,
so it is a DOCUMENTED trap instead. Read the RAISES/DOCS tag on each
before assuming "it didn't raise" means "it's fine":
- [RAISES, inherited] non-uniform +
distributed=Truewith a registered DFT-plane probe:forward()itself raisesNotImplementedError(the #619 fence,rfx/api/_execute.py) -- neither sharded runner accumulates DFT-plane fields.jacobian_fwdscope is the uniform single-device lane only; a distributedsim_fnfails loud before this function's own machinery runs. - [RAISES, inherited]
n_warmup:forward()itself raisesNotImplementedErrorfor a nonzeron_warmupon the uniform lane (issue #626) -- so onjacobian_fwd's supported (uniform) lane, an_warmup-usingsim_fnalready fails loud. It used to be a measured SILENT NO-OP (bit-identical value AND tangent atn_warmup=0vsn_warmup=60of 80 steps) before the fence was added. Do not read this as "n_warmup is safe elsewhere": on the non-uniform lane where it IS implemented,n_warmuptruncation error DEPENDS ON DISTANCE FROM THE SOURCE TO THE DESIGN REGION, not merely onn_warmup's value -- it is (near-)exact whilen_warmupstays below the wavefront's arrival time at the design region (K_safe ~= floor(min_distance(source, design_region) / (C0 * dt))) and grows sharply beyond it, up to ~6-7% at half the pre-loss-window length, 58% at the loss-window boundary itself, exactly zero once far enough beyond it, FOR A NEAR-SOURCE placement specifically (issue #626 part 2 / addendum -- a far-from-source placement stays within this repo's ~1.5% AD-vs-FD noise floor for everyn_warmup <= K_safe(growing smoothly, not a sharp cliff, from <0.01% well belowK_safeto ~1.2% ATK_safeitself), seerfx/nonuniform.py'sn_warmup splitcomment for the full measured curves and formula) -- it is not fenced there (truncated-BPTT is a legitimate, if placement-dependent, construction), and any future non-uniform extension of this function must account forK_safe, not treatn_warmupas uniformly lossy OR uniformly free. - [DOCS ONLY -- inert, not a raise]
checkpoint/checkpoint_segments: measured EXACTLY NEUTRAL under forward mode (flops/temp bytes identical acrosscheckpoint=False,checkpoint=True(theforward()default), andcheckpoint_segments=K) -- remat only pays off under reverse-mode transposition, and forward mode builds no tape to transpose. Asim_fnbuilt for use withjacobian_fwdshould passcheckpoint=Falseexplicitly (forward()defaults tocheckpoint=True) to avoid inheriting dead HLO pluscheckpoint_segments'n_steps % K == 0divisibility raise for no forward-mode benefit. These are reverse-mode knobs; document them as such, do not reach for them here.
Scope limits
- Uniform single-device lane only (see the fences above).
- "Geometry parameter" is NOT a parametric dimension here. A traced
Boxcorner raisesConcretizationTypeError(rfx/geometry/csg.py) andforward()'s material rasterisation is a binaryjnp.wheremask with zero gradient almost everywhere. The only continuous geometry design channels this package has are topology density (viarfx.topology.density_to_material_fields()),pec_occupancy_override, and the non-uniformdz_profile. If you wantd/d(patch width in metres), this function cannot supply that column -- rfx does not have it to give, on any AD mode. - Nonlinear (Kerr chi3) tangents ran clean under
vmap(jvp)in testing but were numerically indistinguishable from the linear baseline at the tested chi3 -- that is NOT a validation of the nonlinear tangent path. Treat it as unverified until measured at a chi3 large enough to move the Jacobian outside noise.
Run multiple simulations with different parameter values.
Parameters
sim_factory : callable
sim_factory(param_value) -> Simulation. Called once per value
in param_values to create a fully configured Simulation object.
param_name : str
Human-readable name of the parameter being swept (used for labels).
param_values : array-like
1-D sequence of parameter values to sweep over.
n_steps : int or None
If given, passed as n_steps to each sim.run().
until_decay : float or None
If given, passed as until_decay to each sim.run().
run_kwargs : dict or None
Additional keyword arguments forwarded to sim.run().
Returns
SweepResult
Container with .results, .param_values, and .param_name.
Structured result from a parametric sweep.
Attributes
results : list
One rfx.api.Result (or equivalent) per parameter value.
param_name : str
Label for the swept parameter.
param_values : ndarray
1-D array of parameter values that were swept.
Return the minimum S11 (dB) for each sweep point.
Works with results that have s_params (n_ports, n_ports, n_freqs).
Return peak absolute value of time-series for each sweep point.
Build a Pareto-front artifact from sweep metrics.
metrics may be a mapping from objective name to one value per sweep
point, or a sequence of SweepResult metric method names such as
("s11_min_db", "peak_field"). Maximization objectives are declared
through minimize=False for that objective position.
Visualize how a metric varies across the swept parameter.
Parameters
sweep_result : SweepResult
Output of parametric_sweep.
metric : str or callable
Built-in metric name ("s11_min_db" or "peak_field"), or a
callable metric(result) -> float applied to each result.
title : str or None
Plot title. Defaults to "<param_name> sweep" .
ylabel : str or None
Y-axis label. Defaults to the metric name.
marker : str
Matplotlib line/marker style (default "o-").
Returns
matplotlib Figure
Batch-evaluate multiple material parameter values using jax.vmap.
Only works for material parameters (eps_r, sigma, mu_r)
where the grid shape stays constant. For geometry sweeps, use
parametric_sweep().
Parameters
sim : Simulation
Base simulation (all geometry already added).
param_name : str
Material parameter to sweep: "eps_r", "sigma", "mu_r",
or a material-specific name like "substrate.eps_r".
param_values : array-like, shape (n_batch,)
Values to evaluate.
n_steps : int or None
Timesteps. If None, auto-computed from num_periods.
num_periods : float
Periods at freq_max for auto timestep count (default 20).
return_fields : bool
Must be False (the default). return_fields=True was documented
here since this function's introduction but never implemented —
neither the fast path nor the sequential fallback ever populated
VmapSweepResult.final_fields, so it silently returned None
instead of the promised snapshot. Passing True now raises
ValueError (fail loud beats silent None, #578). Use
sim.run()/sim.forward() for a final-field snapshot of one
configuration, or parametric_sweep() for full per-value
Result objects across a swept parameter.
Returns
VmapSweepResult
Result with .time_series of shape
(n_batch, n_steps, n_probes), .dft_planes (dict of
(n_batch, n_freqs, n1, n2) complex arrays keyed by
add_dft_plane_probe name, or None if no plane was
registered), and .param_values.
Notes
The entire FDTD time loop is vmapped so all simulations execute in a
single fused GPU kernel. Memory scales as
n_batch * grid_size + n_batch * sum(n_freqs_p * n1_p * n2_p for each
registered DFT plane p) (complex accumulators; no remat on this
path). For large grids/plane counts, reduce batch size or plane
frequency count to avoid OOM.
Features supported in vmap path: PEC boundaries, CPML absorbing
boundaries, soft sources (incl. amplitude_kind='current', #571),
point probes, and DFT plane probes (add_dft_plane_probe, #578).
Lumped ports, MSL/coaxial/floquet ports, TFSF, dispersion, waveguide
ports, flux monitors, and NTFF are not supported in the vmap fast
path and trigger a sequential fallback. As of #578 that fallback also
populates .dft_planes (by stacking each swept value's
Result.dft_planes accumulators), so registering a DFT plane never
silently loses data on either path — it is only the FAST path that is
conditional on eligibility, not the DFT-plane output.
TFSF sweeps — angle batching is intentionally out of scope, not just
unimplemented: it is a structural mismatch with this function's
material-only batching model, not a missing feature. The TFSF incident
field is itself an auxiliary FDTD solution advanced inside the scan
(not closed-form); Method B's auxiliary-grid size is angle-dependent
(a batch of angles would need a batch of scan shapes, which
jax.vmap cannot express); and the oblique 2D-aux Bloch path
concretizes its transverse phase factors from angle_deg at trace
time (rfx/sources/tfsf_2d.py). More basically, rfx cannot even
express a general incidence triple (theta, phi, psi) today — only
angle_deg + a binary direction + a two-way polarization
choice — so there is no batchable angle parameter to expose in the
first place. Route illumination/angle sweeps to parametric_sweep()
instead: it already returns full Result objects including
dft_planes for each swept value. Caveat there too: oblique-Bloch
TFSF (angle_deg != 0, method='bloch', the default) combined
with DFT planes/flux monitors/NTFF raises NotImplementedError by
design (rfx/simulation.py:728-745) because the accumulated envelope is
not the physical spectrum — DFT-plane tensors from a TFSF-illuminated
parametric_sweep are obtainable only at normal incidence
(angle_deg=0) or with method='methodB' (open-domain, real
fields). A MATERIAL sweep on a sim carrying a FIXED (non-swept) TFSF
source still benefits from #578: it takes this function's sequential
fallback (TFSF is not fast-path-eligible) but that fallback now
carries dft_planes through, so per-material DFT tensors at fixed
incidence ARE obtainable via vmap_material_sweep today.
Point-frequency-domain probes are not a separate case here: rfx has no
public builder for a point DFT probe (only plane DFT probes and flux
monitors), and point time series — the actual point-probe primitive
— are already fully batched via add_probe + .time_series on
both paths.
Result from a vmapped material sweep.
Attributes
time_series : ndarray, shape (n_batch, n_steps, n_probes)
Probe recordings for every sweep value.
param_name : str
Name of the swept parameter.
param_values : ndarray, shape (n_batch,)
The parameter values that were swept.
final_fields : dict or None
Never populated (return_fields=True raises ValueError
instead of silently returning None here — see
vmap_material_sweep). Reserved for a future final-field-state
implementation; use sim.run()/sim.forward() or
parametric_sweep() for final fields today.
dft_planes : dict[str, ndarray] or None
DFT plane accumulators, keyed by the add_dft_plane_probe name,
each shaped (n_batch, n_freqs, n1, n2) complex — a leading
batch axis over the same accumulator Result.dft_planes[name]
exposes for a single run (#578). Populated on BOTH the vmap fast
path and the sequential fallback (the fallback stacks each
swept-value Result.dft_planes[name].accumulator), so the field
is uniform regardless of which path a given Simulation takes.
None if no add_dft_plane_probe was registered. Appended
AFTER final_fields (not inserted before it) to preserve the
positional-construction meaning of this publicly exported
dataclass for any existing caller using positional args.
Define a multi-dimensional parameter sweep.
Parameters are given as keyword arguments, each mapping to an array of values. The sweep iterates over the full Cartesian product.
Example
>>> sweep = ParameterSweep(width=[0.01, 0.02], eps_r=[2.0, 4.0])
>>> sweep.total
4
>>> list(sweep.combinations())
[{'width': 0.01, 'eps_r': 2.0}, {'width': 0.01, 'eps_r': 4.0}, ...]
Result record returned by run_batch_with_manifest().
result is None for cases skipped through resume.
Run simulations for all parameter combinations.
Parameters
sim_factory : callable Function that takes keyword args from the sweep and returns a configured Simulation object (or any object with a .run() method). sweep : ParameterSweep Parameter sweep definition. run_kwargs : dict or None Extra keyword arguments passed to sim.run().
Returns
list of (params_dict, result) tuples
Run a sequential sweep with durable per-case provenance.
The runner records stable case ids, parameters, status, timings, metrics,
and optional artifact paths. artifact_fn may return paths relative to
the case directory, paths relative to the output root, or absolute paths
inside the output root.
The runner automatically fingerprints run_kwargs so changing run
settings (for example n_steps) invalidates resume. Pass
run_fingerprint when simulation factory, metric semantics, or external
inputs changed but run_kwargs did not. When resume=True, a case is
skipped only if the manifest record is completed, has the expected run
fingerprint, contains at least one metric, and all declared artifact paths
still exist. With continue_on_error=True, failed cases are recorded and
later cases continue; the default preserves the fail-fast behavior.
Return an inspectable status summary for a batch manifest.
manifest_or_path may be a manifest dictionary or a path to a manifest
JSON file. The returned summary is detached from the input so callers can
use it for reports without mutating the manifest.
Voltage at the WirePort LIVE-run midpoint cell.
Uses a single-cell measurement at the live-run midpoint (same location
as wire_port_current) for balanced V/I wave decomposition. The midpoint
is that of the LIVE run (issue #764; pec_edge_masks=None or no dead cells
is bit-identical to the historical all-extent midpoint).
Parameters
state : FDTDState grid : Grid port : WirePort pec_edge_masks : realized PEC edge masks (Mx, My, Mz) for the #318 live split
Returns
float scalar
Current through a WirePort via Ampere's law at the LIVE-run midpoint.
Uses the H-field loop integral at the center cell of the LIVE wire run
(issue #764; with no dead cells this is the historical all-extent
midpoint), the same _ampere_loop() port_current() uses.
Out-of-domain H is ZERO (#692) — see _bwd_h() for the rule and
for the two axis kinds that keep the wrap.
Parameters
state : FDTDState grid : Grid port : WirePort periodic : (bool, bool, bool) Per-axis periodic flags for the run; default non-periodic. pec_edge_masks : realized PEC edge masks (Mx, My, Mz) for the #318 live split
Returns
float scalar
Running DFT accumulator for Poynting flux through a plane.
Accumulates frequency-domain E and H tangential components on a (possibly finite-size) region of the plane to compute ∫ Re(E × H*) · n̂ dA at each frequency.
For x-normal plane: flux = Re(Ey·Hz* - Ez·Hy*) integrated over y,z. For y-normal plane: flux = Re(Ez·Hx* - Ex·Hz*) integrated over x,z. For z-normal plane: flux = Re(Ex·Hy* - Ey·Hx*) integrated over x,y.
Create new instance of FluxMonitor(e1_dft, e2_dft, h1_dft, h2_dft, freqs, axis, index, dA, total_steps, window, window_alpha, lo1, hi1, lo2, hi2, dA2)
Compute Poynting flux spectrum from accumulated DFT fields.
Returns
flux : (n_freqs,) float ∫ Re(E × H*) · n̂ dA at each frequency. Positive = power flowing in +axis direction.
Notes
With x64 disabled the accumulators are complex64, and per-cell E×H* products below the float32 minimum normal (~1.18e-38) are flushed to zero by XLA — a physically tiny-but-nonzero flux then returns EXACTLY 0.0 at every frequency with healthy field accumulators (issue #304; single-cell fixed-amplitude sources radiate P ~ dx^6, so fine grids hit this readily). Eager calls detect that state with a float64 NumPy recompute of the identical sum and emit a UserWarning; under jit/grad the check is skipped entirely (tracer-safe, off the AD tape).
exact_f64=True performs that float64 NumPy recompute AS the result
(eager-only; raises under tracing): the per-cell products and the sum
run in complex128 from the (healthy) float32 accumulators, which is
the #304 warning's own sanctioned remedy. Absolute-scale energy
consumers (the #589 flux-adjudication witness channel) use this path —
partial subnormal flushing corrupts small-magnitude sums well before
they reach exact zero, so the warning alone is not sufficient there.
The default path is unchanged.
Remove a reference (incident) field from a flux monitor, field-level.
The two-run reference subtraction of
docs/agent/recipe-rt-measurement.mdx (critical detail (a)): pass the
monitor from the run WITH the scatterer and the monitor from the otherwise
identical run WITHOUT it, and call flux_spectrum() on the result to
get the scattered flux.
Parameters
sample : FluxMonitor Monitor from the run with the scatterer present. ref : FluxMonitor Monitor from the reference run — same plane, same frequencies.
Returns
FluxMonitor
sample with each of the four tangential DFT accumulators replaced
by sample.<field> - ref.<field>. Every other field (freqs,
axis, index, dA, total_steps, window,
window_alpha, lo1/hi1/lo2/hi2) is carried over
from sample unchanged.
Raises
TypeError
If either argument is not a FluxMonitor.
ValueError
If the four accumulators disagree in shape, or if any non-accumulator
field differs. Nothing is broadcast: two monitors that do not describe
the same plane and the same frequencies have no meaningful difference,
and a silent broadcast would return a plausible-looking number instead
of an error.
Notes
The subtraction has to happen on the FIELDS, never on the fluxes. Poynting
flux is bilinear in E and H, so writing the sample fields as incident plus
scattered (E_s = E_i + E_r, H_s = H_i + H_r) gives
``flux(sample) - flux(ref) = flux(scattered)
+ Re(E_i x H_r* + E_r x H_i*) dA``
and those cross terms are the same order as the scattered flux itself —
they vanish only for a null scatterer. Maxwell's equations are linear, so
differencing the accumulators first and forming the Poynting product
afterwards is exact. flux_spectrum(sample) - flux_spectrum(ref) is not
an approximation of this; it is a different quantity.
Tracer-safe: the subtraction is plain array arithmetic and stays on the AD
tape, so jax.grad through
flux_spectrum(subtract_flux_monitors(...)) works. Shape checks run on
tracers too; the equality check on array-valued metadata is undecidable
under jit/grad and is skipped there.
Extract resonant modes via the Matrix Pencil Method.
Lossless closed cavities have infinite physical Q — do not read a Q
off a finite window. In a PEC-bounded domain with no material loss
(an empty or lossless-filled cavity), a resonance never decays, so the
Q returned here is a pure windowing artefact: it swings by orders of
magnitude with the run length rather than tracking any physics. The
resonant frequency is still meaningful; the Q is not. To measure a
meaningful Q, add a realistic loss (a finite sigma / tan_delta
on the fill, or a lossy load) so the cavity has a finite physical Q, or
use an open (CPML) boundary so radiation sets the Q.
Parameters
signal : 1D real or complex array
Time-domain signal.
dt : float
Timestep in seconds.
f_min, f_max : float
Frequency search range in Hz.
pencil_parameter : float
Fraction of signal length for pencil size L (0.2-0.5).
min_Q : float
Discard modes with Q < min_Q.
max_modes : int
Maximum complex-pole rank and maximum modes to return. Both members
of a real signal's conjugate pair consume rank before reporting.
sv_threshold : float
Relative singular value threshold for rank determination (relative
to the largest singular value). The same threshold is used after
resampling; it is not a calibrated noise-confidence bound.
decimate : {"auto", False}
Automatically reduce oversampled, band-limited inputs before matrix
pencil analysis. With the default "auto", decimation is applied
when 1 / dt > 8 * f_max using a target factor of
int(1 / dt / (4 * f_max)). Anti-aliased, zero-phase FIR stages of
at most 13 are used, and the effective timestep is passed to the core
algorithm. At each stage, outputs touching the filter's zero-padded
boundary are excluded: filtering an interior exponential preserves
its pole, while zero padding introduces transients. This SHORTENS
the usable record; harminv_record_duration reports its duration.
A stage is omitted if fewer than 10 interior samples would remain or
its pencil could not hold max_modes poles plus one singular
direction for rank selection. If the original pencil is smaller,
its existing capacity is preserved instead. This is a dimension
safeguard, not a guarantee that noisy/nearby modes are identifiable.
Set to False to retain every input sample. Decimation avoids
redundant work because the estimator scales approximately as
\( O(N^{2.7}) \).
Returns
list of HarminvMode, sorted by amplitude (strongest first). Amplitude and phase describe jointly fitted complex coefficients at the original input's first sample. For a real cosine of peak A the positive-frequency coefficient has magnitude A/2; its conjugate is fitted too, then omitted from the returned list. Relative weights depend on source/probe placement and are not modal energies.
Extract resonances from a simulation probe time series.
Automatically windows the signal to use only the ring-down portion (after source has decayed).
A lossless closed (PEC) cavity has infinite physical Q, so the Q
returned here is a window-length artefact, not physics — add a
realistic loss or use an open (CPML) boundary before trusting a Q.
See harminv() for the full note. (The frequency is fine.)
Parameters
time_series : 1D array dt : float freq_range : (f_min, f_max) in Hz source_decay_time : float Time at which source has decayed (seconds).
A single resonant mode extracted by Harminv.
A single waveguide eigenmode on the cross-section grid.
Attributes
ey_profile : ndarray, shape (nu, nv)
Transverse E-field, first local axis component.
ez_profile : ndarray, shape (nu, nv)
Transverse E-field, second local axis component.
hy_profile : ndarray, shape (nu, nv)
Transverse H-field, first local axis component.
hz_profile : ndarray, shape (nu, nv)
Transverse H-field, second local axis component.
beta : ndarray, shape (n_freqs,)
Propagation constant at each frequency. Real above cutoff,
imaginary (stored as complex) below cutoff.
f_cutoff : float
Cutoff frequency in Hz.
mode_type : str
"TE" or "TM".
mode_indices : tuple[int, int]
(m, n) mode indices.
Create new instance of WaveguideMode(ey_profile, ez_profile, hy_profile, hz_profile, beta, f_cutoff, mode_type, mode_indices)
Solve for the lowest eigenmodes of a rectangular waveguide cross-section.
Parameters
a : float
Waveguide width (metres), along the first transverse axis.
b : float
Waveguide height (metres), along the second transverse axis.
dx : float
Cell size (metres).
freqs : array_like, shape (n_freqs,)
Frequency points at which to evaluate the propagation constant.
n_modes : int
Number of lowest-cutoff modes to return.
eps_cross : ndarray, shape (ny, nz), optional
Relative permittivity on the cross-section grid. None means
uniform vacuum (eps_r = 1).
mu_cross : ndarray, shape (ny, nz), optional
Relative permeability on the cross-section grid. None means
uniform (mu_r = 1).
Returns
modes : list[WaveguideMode]
The n_modes lowest-cutoff eigenmodes, sorted by cutoff frequency.
Lumped RLC element specification.
Parameters
R : float Resistance in ohms. 0 = no resistive component. L : float Inductance in henries. 0 = no inductive component. C : float Capacitance in farads. 0 = no capacitive component. topology : str "series" or "parallel". Series topology tracks a shared current through R, L, C via ADE (true series RLC). Parallel topology folds R/C into material properties independently and uses an ADE only for L. position : (x, y, z) in metres component : str E-field component ("ex", "ey", or "ez").
Derive all simulation parameters from geometry + frequency range.
Parameters
geometry : list of (Shape, material_name) tuples
freq_range : (f_min, f_max) in Hz
materials : dict of material definitions (name -> {eps_r, sigma, ...})
accuracy : "draft", "standard", or "high"
draft: 10 cells/λ, 0.15λ margin (fast, ~10% error)
standard: 20 cells/λ, 0.25λ margin (~3% error)
high: 40 cells/λ, 0.50λ margin (~1% error)
boundary : "cpml" or "pec"
Boundary condition type. Determines source type recommendation:
- "cpml": J-source with Cb/dV normalization (Meep convention)
- "pec": raw E-field source (no Cb normalization needed)
dx_override : force specific cell size
margin_override : force specific margin
n_steps_override : force specific step count
max_memory_mb : float or None
GPU memory budget in megabytes. When set, dx is
automatically coarsened (increased) until
estimated_memory_mb <= max_memory_mb. Useful for
keeping reverse-mode AD within a 24 GB GPU. Ignored when
dx_override is provided.
Practical limits (approximate, with gradient checkpointing):
- 8 GB GPU: ``max_memory_mb=6000``
- 24 GB GPU: ``max_memory_mb=18000``
- 48 GB GPU: ``max_memory_mb=36000``
waveform : source waveform or None
Used only for the source-time term of the n_steps estimate:
when the waveform exposes t0 (GaussianPulse /
ModulatedGaussian: t0 = cutoff*tau), the source window is
2*t0 so a longer-onset pulse (e.g. cutoff=4.5 →
9*tau) is fully covered. Without a waveform the historical
cutoff=3 envelope (6*tau at bw=0.8) is used, bitwise-identical
to the previous hardcode.
thin_conductors : iterable of ThinConductor
Declared sheets. Their planes (and any zero-thickness PEC box in
geometry) become z FEATURES: the non-uniform z profile puts a
mesh line on each, so a sheet is realized where it was declared
(#931 §1.9).
Returns
SimConfig with all derived parameters.
Auto-derived simulation configuration.
Estimated grid shape (nx, ny, nz) including CPML padding.
Issue #696: when dz_profile is set the z axis is counted from
the PROFILE, not from ceil(domain_z / dx). This property
described the UNIFORM grid even for a configuration whose whole
point is a graded z stackup, and a graded stackup normally
refines below dx — so the cell count, and every memory number
derived from it, came out LOW for exactly the meshes people size
GPUs for. The z term now matches NonUniformGrid's
len(profile) + pads + 1.
Coarse auto-configuration memory heuristic in megabytes.
This property is used while choosing a draft mesh from geometry
before a Simulation object exists. It intentionally remains a
conservative planning heuristic, not the calibrated AD-memory
contract for production runs.
For user-facing memory reports and inverse-design sizing, prefer
Simulation.estimate_ad_memory(...) or
Simulation.mesh_intelligence_report(...). Those APIs know the
final grid/profiles, NTFF state, and segmented scan-of-scan
checkpointing semantics from issue #31.
Accounts for:
- 6 field arrays (Ex, Ey, Ez, Hx, Hy, Hz)
- ~6 material coefficient arrays (eps_r, sigma, mu_r, Ca, Cb, etc.)
- CPML auxiliary fields (~24 psi arrays in absorbing region)
- NTFF DFT accumulators (6 faces × n_freqs × face_cells × 4 components)
- Debye/Lorentz auxiliary polarization fields (2 per pole)
- ~10x overhead for reverse-mode AD through jax.lax.scan (XLA compilation buffers, reverse-mode tape for scan unrolling, CPML auxiliary arrays replicated for AD, jax.checkpoint reduces but does not eliminate this)
Returns
float Estimated memory in megabytes.
Extract critical dimensions and material properties from geometry.
Parameters
geometry : list of (Shape, material_name) tuples
materials : dict of material_name -> MaterialSpec or dict
pec_threshold : sigma above which material is PEC
thin_conductors : iterable of ThinConductor
Declared sheets (sim.add_thin_conductor). Their planes join the
zero-thickness PEC boxes found in geometry as z FEATURES
(#931 §1.9): a sheet has no thickness to resolve, but the mesh must
carry a node at its plane.
Inspectable mesh-planning result built from existing rfx facts.
MeshPlan is an advisory artifact. It serializes the derived mesh,
CFL basis, memory estimate, support rows, and intended artifact paths
without writing files or claiming deterministic solver replay.
Return Simulation constructor kwargs when built by plan_mesh.
Plan a mesh from geometry and frequency inputs using auto_configure.
This function is a facade around auto_configure; it does not duplicate
the mesh preset math. The returned MeshPlan adds structured audit rows,
artifact declarations, and serialization helpers. artifact_root only
declares intended paths; it never creates directories or writes files.
Plan/audit a configured Simulation using existing preflight APIs.
artifact_root only declares intended paths; it never creates directories
or writes files.
sparameter_calculator="waveguide" is the one calculator whose preflight
is not free: it builds the grid and solves each waveguide port's mode so
the setup audits can read the runner's own plane indices and cutoffs. It
runs no FDTD, but on a fine mesh with several ports it is seconds rather
than milliseconds. Those audits also file an informational row per port
pair (the known E-plane offset), which is a status="info" row, not a
"warn" one.
Run a mesh convergence study.
Parameters
sim_factory : callable(dx) -> Simulation
Builds a simulation at the given cell size.
dx_values : list of float
Cell sizes to test (coarse to fine).
metric_fn : callable(Result) -> float
Extracts the metric of interest (e.g., resonance frequency).
n_steps : int or None
If given, passed as n_steps to each sim.run().
until_decay : float or None
If given, passed as until_decay to each sim.run().
run_kwargs : dict or None
Additional keyword arguments forwarded to sim.run().
expected_order : float
Initial guess for convergence order (default 2 for standard Yee).
Returns
ConvergenceResult Container with dx_values, metrics, extrapolated value, order, relative errors, and raw results.
Structured result from a mesh convergence study.
Attributes
dx_values : ndarray Cell sizes tested (coarse to fine). metrics : ndarray Metric value at each cell size. extrapolated : float Richardson-extrapolated grid-independent value. order : float Estimated convergence order. errors : ndarray Relative error of each metric w.r.t. the extrapolated value. results : list Raw simulation results (one per dx).
Plot convergence: metric vs dx and log-log error vs dx.
Parameters
ax : matplotlib Axes or None If provided, only the metric-vs-dx plot is drawn on this axes. If None, a two-panel figure is created. title : str Figure/axes title.
Returns
matplotlib Figure
Richardson extrapolation to estimate the grid-independent value.
Uses the three finest grid points to estimate both the converged value and the effective convergence order. Falls back to two-point extrapolation when only two values are available.
Parameters
values : array-like Metric values at each cell size (must have >= 2 elements). dx_values : array-like Cell sizes corresponding to values (same length). expected_order : float Initial guess for the convergence order (default 2 for Yee).
Returns
(extrapolated, order) : (float, float) Estimated grid-independent value and observed convergence order.
Notes
When three or more values are available the order p is estimated by solving the ratio of successive differences:
(f3 - f2) / (f2 - f1) = (h3^p - h2^p) / (h2^p - h1^p)
where h = dx and f = metric. A Newton iteration refines p from the initial guess. With only two values the expected_order is used directly.
Run a convergence study on an existing Simulation with dx scaling.
Creates copies of sim at different cell sizes by scaling the base dx
and re-running. This is a convenience wrapper around
convergence_study for quick checks.
Parameters
sim : rfx.api.Simulation
A fully configured simulation. Its _dx (or auto-computed dx)
is used as the reference cell size scaled by each factor.
metric : str or callable
"resonance" extracts the first resonance frequency.
"peak_field" extracts the peak probe amplitude.
A callable metric(Result) -> float is used directly.
dx_factors : list of float or None
Scaling factors applied to the base dx. Default [2.0, 1.5, 1.0, 0.75].
Values > 1 mean coarser; < 1 mean finer.
n_steps : int or None
Forwarded to sim.run().
until_decay : float or None
Forwarded to sim.run().
run_kwargs : dict or None
Additional keyword arguments forwarded to sim.run().
Returns
ConvergenceResult
Compute spatial error indicator from a simulation result.
Returns a 3-D array where high values indicate regions that would benefit from mesh refinement. The proxy is the magnitude of the spatial gradient of the final-state field component, normalized to [0, 1].
Parameters
result : Result or SimResult
A completed simulation result whose .state carries the final
field arrays (ex, ey, ez, hx, hy, hz).
component : str
Field component to analyze (default "ez").
Returns
error : ndarray, shape (Nx, Ny, Nz) Normalized error indicator in [0, 1]. Values near 1 mark cells with the steepest field gradients.
Suggest Box regions for SBP-SAT refinement based on error map.
Identifies connected regions where the error indicator exceeds threshold and returns axis-aligned bounding boxes for each region that is at least min_region_size cells in every used dimension.
Parameters
error_map : ndarray, shape (Nx, Ny, Nz)
Normalized error indicator (output of compute_error_indicator).
grid : Grid or None
If provided, box corners are in physical (metre) coordinates.
Otherwise they are in cell-index coordinates.
threshold : float
Cells with error_map > threshold are flagged (default 0.5).
min_region_size : int
Minimum bounding-box extent in cells per axis to keep a region
(default 4). Smaller regions are discarded as noise.
Returns
list of Box Suggested refinement bounding boxes.
Automatically add a refinement region based on the error indicator.
Computes the error indicator from result, finds the dominant high-
error region, and calls sim.add_refinement() with the
corresponding z-range.
Parameters
sim : Simulation
The simulation to add refinement to (mutated in-place).
result : Result
Completed coarse simulation result.
threshold : float
Error threshold for region suggestion (default 0.5).
component : str
Field component for the error indicator (default "ez").
min_region_size : int
Minimum cells per axis for a suggested region (default 2).
ratio : int
Subgrid refinement ratio passed to add_refinement() (default 2).
Returns
sim : Simulation The same simulation object (for chaining).
Remove port extension (transmission line) effects from S-parameters.
Shifts the reference plane inward by removing the phase delay of the feed lines at each port. Assumes lossless, matched feed lines (same impedance as the port reference impedance) so only a phase correction is needed.
Parameters
s_matrix : ndarray, shape (n_ports, n_ports, n_freqs)
Complex S-parameter matrix.
freqs : ndarray, shape (n_freqs,)
Frequency points in Hz.
port_lengths : list of float or ndarray, shape (n_ports,)
Physical length to de-embed from each port, in metres.
z0 : float
Port reference impedance (ohm). Not used in the phase-only
correction but kept for API consistency and future lossy extension.
eps_eff : float or ndarray
Effective relative permittivity of the feed line. A scalar
applies uniformly; an array of shape (n_freqs,) supports
dispersive media.
Returns
ndarray, shape (n_ports, n_ports, n_freqs) De-embedded S-parameter matrix.
De-embed a DUT using a thru-line measurement (TRL-lite).
Assumes a symmetric two-port fixture: the thru standard captures the combined effect of both fixture halves in cascade. The fixture half-network is extracted from the thru, inverted, and removed from each side of the measured DUT.
This is a simplified single-thru calibration; for full TRL accuracy, reflect and line standards would also be needed.
The algorithm:
- Convert the thru S-matrix to a transfer (T) matrix.
- Take the matrix square root to get the fixture half T-matrix.
- Invert the fixture half.
- Cascade: T_dut = T_fix_inv @ T_meas @ T_fix_inv.
- Convert back to S-parameters.
Parameters
s_measured : ndarray, shape (2, 2, n_freqs) Measured S-parameters of the DUT including fixture. s_thru : ndarray, shape (2, 2, n_freqs) Measured S-parameters of the thru standard (fixture back-to-back).
Returns
ndarray, shape (2, 2, n_freqs) De-embedded S-parameters of the DUT.
Raises
ValueError If inputs are not 2-port matrices or have mismatched frequency counts.
One validation diagnostic for an RF port observable.
Validation result returned by validate_port_smatrix().
Validate schema and selected invariants for an RF port observable.
Thresholds are caller-controlled. Defaults are intentionally strict for synthetic/unit checks; integration tests may pass looser tolerances that match their validated physics envelope.
Validate and raise AssertionError on failure.
Returns the report on success so tests can inspect metrics without running validation twice.
Normalize rfx S-matrix-like outputs into a canonical observable.
The function uses duck typing so it works for:
rfx.api.Result/Result.s_paramsrfx.api.WaveguideSMatrixResult/.s_paramsrfx.api.MSLSMatrixResult/.S- direct
s_params=...andfreqs=...arrays
It raises ValueError for schema problems. Use
validate_port_smatrix() when you want a non-raising report.
Compare an independent replay against production S-matrix output.
Replay an S-matrix from raw V/I phasors.
This is an independent post-processor for E3 evidence. It uses the power-wave split, role-selected per port to mirror the production lumped decomposer (issue #308):
a = (V + Z0 I) / (2 sqrt(Z0)) (incident, at the DRIVEN port),
b = (V - Z0 I) / (2 sqrt(Z0)) (reflected, at the DRIVEN port), and
b_recv = -(V + Z0 I) / (2 sqrt(Z0)) (arriving, at a PASSIVE
receive port — the production receive channel (V_fdtd - Z0 I)
expressed in this dump's into-DUT voltage convention V = -V_fdtd;
the overall sign is pinned empirically by the DC falsifier on the
canonical thru, S21(DC) -> +1)
diagonal_frame selects the DRIVEN diagonal. The default,
"port_branch", is b/a on the stored (V, I) — the standard
power-wave reflection for a pair that really is (voltage into the DUT,
current into the DUT), and this function's contract.
"driven_terminal" is (V_fdtd - Z0 I)/(V_fdtd + Z0 I) with
V_fdtd = -V, for a dump whose stored voltage sign makes the pair
inconsistent AT THE DRIVEN PORT while keeping the off-diagonal role
channel right — which is the case for rfx's own lumped-port dump since
2026-09-21 (scripts/diagnostics/lumped_port_known_load_line.py; on that
known-load line b/a at a driven port reads 4.757 where the closed
form is 0.333, the reciprocal of the physical reflection). Such a dump
says so in its metadata; see replay_smatrix_from_port_vi_dump().
The off-diagonal role channel is unchanged by this switch.
In the "driven_terminal" frame the whole column shares ONE incident
wave, the physical one at the driven port, so the diagonal and the
off-diagonals have the same denominator. That is the wire family's
whole-port frame, which rfx's lumped lane now evaluates at one live cell,
and it needs no channel beyond the stored V and I.
with current positive into the DUT by default. If the dump records
current positive out of the DUT, set current_convention="positive_out_of_dut"
and the sign is flipped before wave decomposition.
Optional reference-plane shifts are explicit metadata, not hidden magic:
reference_plane_offsets_m[i] > 0 means the reported reference plane
is farther into the DUT than the raw measurement plane in port i's
local coordinate. With propagation constant gamma this applies
a_ref = a_raw exp(-gamma d) and
b_ref = b_raw exp(+gamma d).
Metadata-aware Touchstone read result.
The legacy read_touchstone() API intentionally returns only
(s_params, freqs, z0). Use this dataclass through
read_touchstone_full() when Touchstone 2.0 metadata such as per-port
reference impedances or explicit matrix layout matters.
Read S-parameters from a Touchstone file.
This legacy compatibility API returns a scalar reference impedance. If a
Touchstone 2.0 file carries non-uniform per-port references, use
read_touchstone_full() instead.
Parameters
filepath : str or Path layout : str Auto-detect old unmarked rfx v1 exports; otherwise use standard order. An explicit layout overrides detection.
Returns
s_params : (n_ports, n_ports, n_freqs) complex array freqs : (n_freqs,) float in Hz z0 : float reference impedance
Read S-parameters and Touchstone metadata.
Supported subset:
- S-parameters only
- RI, MA, and DB numeric formats
- Touchstone 1-style data blocks
- Touchstone 2.0 metadata keywords used by rfx interop
- Touchstone 2.0 information blocks preserved as raw lines
- Full matrix data only
layout="auto" detects legacy column-major order for unmarked rfx
v1 exports with 3+ ports. Marked exports and external files use standard
order. Two-port and v2 files are unaffected; explicit layouts override.
Write S-parameters to a Touchstone file.
Parameters
filepath : str or Path
Output file (extension determines port count: .s1p, .s2p, etc.)
s_params : (n_ports, n_ports, n_freqs) complex array
freqs : (n_freqs,) float in Hz
z0 : reference impedance (ohm)
freq_unit : "Hz", "kHz", "MHz", or "GHz"
fmt : "RI" (real/imag), "MA" (mag/angle), or "DB" (dB/angle)
comments : optional comment lines
version : "1.0" or "2.0"
"1.0" preserves the historical rfx writer shape. "2.0"
writes Touchstone 2.0 metadata keywords for the supported S-parameter
subset.
layout : "legacy-rfx", "standard", or None
Multi-port order. None means standard layout for both
Touchstone 1.0 and 2.0 output.
The legacy rfx layout is column-major for all port counts. The
standard layout is row-major for 3+ ports. Two-port standard order is
controlled by two_port_order.
port_z0 : array-like or None
Optional per-port reference impedances. Requires version="2.0"
when non-uniform.
matrix_format : str
Touchstone 2.0 matrix format. Only "Full" is supported.
two_port_order : "21_12" or "12_21"
Explicit 2-port data order for standard Touchstone 2.0 files.
information : dict, list, tuple, or None
Optional Touchstone 2.0 information block. Dicts are written as
sorted "key value" lines; sequences are written verbatim.
Return common RF network quality metrics for an S-matrix cube.
The input must have shape (n_ports, n_ports, n_freqs). Metrics are
host-side diagnostics for reports and interop gates; they do not alter any
solver path.
Save OptimizeResult or TopologyResult to HDF5.
Parameters
path : str or Path result : OptimizeResult or TopologyResult metadata : dict or None Extra metadata (grid shape, freq_max, etc.) stored as HDF5 attrs. include_latent : bool Save latent/density arrays (default True). include_density : bool Save projected density and PEC occupancy (default True). include_history : bool Save loss/beta history (default True).
Load optimization result from HDF5.
Returns
dict with keys: eps_design, loss_history, latent, density, etc.
Save FarFieldResult to HDF5.
Parameters
path : str or Path ff_result : FarFieldResult metadata : dict or None
Export radiation pattern as CSV for measurement comparison.
Columns: theta_deg, phi_deg, E_theta_mag, E_theta_phase_deg, E_phi_mag, E_phi_phase_deg, gain_dBi
Parameters
path : str or Path
ff_result : FarFieldResult
freq_idx : int
Frequency index to export.
input_power : array-like (n_freqs,) or scalar, optional
Input power in watts, passed to rfx.antenna.antenna_gain_dB()
for realized gain. Without it, gain_dBi is absolute IEEE gain
normalized by total radiated power. That integral requires full-sphere
coverage, or one phi cut under an axisymmetric assumption. Realized
gain with input_power accepts partial angular patterns.
Export a lightweight geometry index as JSON (bounding boxes only).
Intended for dataset indexing and eyeball review. Each geometry entry is
recorded as its shape class name plus a bounding box, so a Cylinder and
a Box with the same bounds are indistinguishable here, and materials are
reduced to scalar eps_r/sigma/mu_r (dispersion poles are
dropped). Sources are recorded as position + component only.
This output therefore cannot round-trip and is not a design description:
do not drive an external solver from it. For a round-trip-complete design
document use rfx.interop.design_to_dict() /
rfx.interop.design_to_json(), which record constructor parameters and
refuse rather than degrade what they cannot represent.
Parameters
path : str or Path sim : Simulation
Save standardized experiment metadata as JSON.
Auto-collects: sim config, grid shape, timing, loss history.
Parameters
path : str or Path sim : Simulation result : Result, OptimizeResult, or TopologyResult extra : dict or None Additional metadata (GPU info, notes, etc.)
Save simulation input-output pair for surrogate model training.
Creates an HDF5 file with selectable sections:
- Input: simulation config and grid metadata
- Output: time series, S-parameters
- Fields: final 3D E/H snapshot (large, opt-in)
Parameters
path : str or Path sim : Simulation result : Result include_fields : bool Save full 3D E/H field arrays (default False — large). include_time_series : bool Save probe time series (default True). include_s_params : bool Save S-parameters and frequencies (default True). include_config : bool Save simulation config as input metadata (default True).
Paths written by export_artifact_bundle().
Write a runtime artifact bundle directory and return its paths.
Save FDTD state to an HDF5 file.
Parameters
path : str or Path Output file path (.h5). state : FDTDState Field state to save. grid : Grid or None If provided, grid metadata is stored as attributes.
Load FDTD state from an HDF5 file.
Returns
state : FDTDState metadata : dict Grid metadata if it was saved, otherwise empty dict.
Plot a 2D slice of a field component.
Parameters
state : FDTDState grid : Grid component : field name ("ex", "ey", "ez", "hx", "hy", "hz") axis : normal axis for the slice ("x", "y", or "z") index : grid index along axis (default: center) title : plot title cmap : colormap vmax : symmetric color range [-vmax, vmax]
Returns
matplotlib Figure
Plot S-parameter magnitudes vs frequency.
Parameters
s_params : (n_ports, n_ports, n_freqs) complex freqs : (n_freqs,) Hz db : plot in dB title : plot title
Returns
matplotlib Figure
Plot radiation pattern in polar coordinates.
Parameters
ff : FarFieldResult freq_idx : frequency index phi_idx : phi cut index db : plot in dB (normalized) title : plot title
Returns
matplotlib Figure
Plot probe time series.
Parameters
time_series : (n_steps, n_probes) array dt : timestep in seconds labels : probe labels title : plot title
Returns
matplotlib Figure
Plot RCS pattern in polar or rectangular coordinates.
Parameters
rcs_result : RCSResult
Output from compute_rcs().
freq_idx : int
Frequency index to plot.
phi_idx : int
Phi cut index to plot.
polar : bool
If True, plot in polar coordinates. If False, rectangular.
title : str or None
Plot title.
Returns
matplotlib Figure
Plot S11 data on a Smith chart.
Parameters
s11 : (n_freqs,) complex
Reflection coefficient (Gamma) values.
freqs : (n_freqs,) Hz
Corresponding frequencies.
z0 : float
Reference impedance (used only for annotation, not for
re-normalisation).
ax : matplotlib Axes, optional
Axes to plot on. If None, a new figure is created.
show_vswr : bool
If True, draw VSWR = 2 and VSWR = 3 circles.
markers : list of float, optional
Frequencies (in Hz) at which to place labelled markers.
title : str, optional
Plot title. Defaults to "Smith Chart".
Returns
matplotlib Axes
Render simulation geometry in 3D.
Parameters
sim : Simulation rfx Simulation object with geometry and ports defined. show_domain : bool Show the computational domain wireframe. show_ports : bool Show port locations as arrows. title : str or None Plot title. figsize : tuple Figure size. scale : float Coordinate scale factor (default 1e3 for mm). scale_label : str Unit label for axes. backend : str "auto" (pyvista if available, else matplotlib), "matplotlib", or "pyvista".
Returns
Figure (matplotlib) or Plotter (pyvista)
3D field visualization with isosurface or volume rendering.
Parameters
state : FDTDState grid : Grid component : field component name threshold : float Isosurface threshold as fraction of peak value (0-1). title : str or None figsize : tuple cmap : colormap name scale : coordinate scale factor scale_label : unit label backend : "auto", "matplotlib", or "pyvista"
Returns
Figure (matplotlib) or Plotter (pyvista)
Export field data to VTK format for ParaView.
Parameters
state : FDTDState grid : Grid filename : str Output filename (without extension). components : tuple of str Field components to export. scale : float Coordinate scale factor.
Returns
str : path to the written file
Save field animation from simulation snapshots.
Parameters
result : rfx.Result or dict
Simulation result with .snapshots attribute, or a raw
snapshots dict mapping component names to arrays of shape
(n_frames, nx, ny, nz) or (n_frames, nx, ny).
filename : str
Output file path. Extension determines format:
.gif uses PillowWriter, .mp4 uses FFMpegWriter
(falls back to GIF if ffmpeg is unavailable).
If no recognised extension, .gif is appended.
component : str
Field component to animate (default "ez").
slice_axis : str
Axis normal to the slice plane ("x", "y", or "z").
Only used when snapshot data is 3D per frame.
slice_index : int or None
Index along slice_axis. None selects the centre.
fps : int
Frames per second.
colormap : str
Matplotlib colormap name.
vmin, vmax : float or None
Colour-scale limits. None computes symmetric auto-range
from the data.
dpi : int
Resolution in dots per inch.
figsize : tuple
Figure size in inches (width, height).
interval : int
Frame stride — use every interval-th snapshot frame.
Default 1 (every frame). This is a stride over the RECORDED frames,
applied after the run, not SnapshotSpec.interval (which is in
time steps): a run recorded every 10 steps animated with
interval=3 shows every 30th step.
Returns
str Path to the saved animation file.
Raises
ValueError If result has no snapshots or the requested component is missing. ImportError If matplotlib or Pillow are not installed.
Report available JAX compute devices.
Benchmark FDTD throughput across grid sizes.
Returns a list of BenchResult sorted by grid size.
Export parametric sweep results as neural network training data.
From a SweepResult or VmapSweepResult, creates arrays:
- inputs : parameter values, shape
(n_samples, 1) - outputs : time-series probe data, shape
(n_samples, n_steps, n_probes)(SweepResult) or(n_samples, n_steps, n_probes)(VmapSweepResult). When S-parameters are available (SweepResultwith ports), ans_paramsarray is also stored. - metadata : parameter name and frequency array (when available).
Parameters
sweep_result : SweepResult or VmapSweepResult
Output of parametric_sweep or vmap_material_sweep.
output_path : str or Path
Destination file path.
format : "npz"
Output format (currently only NumPy .npz is supported).
Returns
Path The written file path.
Raises
ValueError If format is unsupported or sweep_result type is unrecognised.
Export simulation geometry as a signed distance field.
Builds a 3-D grid at the requested resolution covering the simulation domain and evaluates the geometry mask. The SDF is approximated from the binary mask via a distance transform:
- negative inside geometry (material regions)
- positive outside
- magnitude = approximate Euclidean distance to the nearest boundary (in metres)
This is suitable as input to geometry-conditioned neural operators (e.g., DeepONet, Fourier Neural Operator).
Sheets. A conductor declared as a sheet — add_thin_conductor,
or a zero-thickness PEC Box, which is the same declaration (lattice
ownership contract §1.5) — has no interior, so a containment test
finds it only if its plane happens to land exactly on a sample. Before
this it did not land: add_thin_conductor sheets are not in
sim._geometry at all and never reached the exporter, so every
sheet-declared ground plane, patch and trace was missing from the
exported training data with no error anywhere.
A zero-thickness region cannot be represented in a sampled occupancy
field as zero thickness. The convention here, stated rather than
inferred: a sheet occupies exactly ONE sample layer — the sample
plane nearest its declared mid-plane, with the drawn footprint sampled
closed in-plane — so the distance transform reads -resolution on
the sheet and the sheet's apparent thickness in the SDF is one sample,
not zero. That is the same realization rule the solve uses (a sheet is
one node plane); only the lattice differs. A sheet whose plane or
footprint cannot be placed on this lattice raises rather than
vanishing — refine resolution.
Parameters
sim : Simulation A configured simulation with geometry added. resolution : float Spatial resolution of the SDF grid (metres). Default 1 mm.
Returns
sdf : ndarray, shape (Nx, Ny, Nz) Signed distance field in metres.