# Agent Auto-Configuration Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/markdown/agent/auto-config.md Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/agent/auto-config.mdx Agents should use auto-configuration as a starting point, not as a substitute for RF judgement or support-matrix checks. ## Recommended sequence 1. Define the geometry, material, source/port family, and target frequency band. 2. Use `auto_configure(...)` or the high-level `Simulation(...)` builder to get a first mesh and time-step proposal. 3. Inspect preflight warnings and support-boundary status before running long jobs. 4. Run a short smoke simulation first. 5. Promote to a longer run only after the smoke output has sane field/probe amplitudes and the selected feature lane is documented. ## Gating on the preflight report Read the gate off the report, never off its truthiness. `sim.preflight()` returns a `PreflightReport`, which is a list of findings, so plain list truthiness says the opposite of what a gate means: a clean report is EMPTY and therefore falsy, while a report carrying nothing but advisories is truthy. `if not sim.preflight(): raise ...` and `assert sim.preflight()` both read as "stop unless this passed" and do the reverse — the assert passes exactly when there are problems and fails on a clean run. Boolean evaluation raises `TypeError` for that reason: the trap is loud at the call site instead of a run that quietly skipped its own gate. Use `report.raise_for_failure()` to stop on error-severity findings, `report.ok` or `report.errors` for a condition, and `len(report)` when you mean "did preflight find anything at all", which is the stricter gate that also rejects advisories. ## Python sketch ```python from rfx import Simulation, Box, GaussianPulse, auto_configure geometry = [(Box((0.0, 0.0, 0.0), (0.08, 0.06, 0.0016)), "fr4")] materials = {"fr4": {"eps_r": 4.4, "sigma": 0.025}} config = auto_configure( geometry=geometry, materials=materials, freq_range=(1e9, 4e9), accuracy="standard", ) sim = Simulation(**config.to_sim_kwargs()) sim.add_material("fr4", eps_r=4.4, sigma=0.025) for shape, material in geometry: sim.add(shape, material=material) sim.add_source((0.029, 0.03, 0.0008), "ez", waveform=GaussianPulse(2.4e9, 0.8), amplitude_kind="current") sim.add_probe((0.029, 0.03, 0.0008), "ez") result = sim.run(n_steps=min(config.n_steps, 200)) # smoke first; increase only after inspection ``` This smoke configuration emits preflight advisories about absorber thickness, mesh grading and the absorber runway. A successful smoke run does not establish that the proposed mesh is inside the validated envelope. ## What agents must report - mesh/domain/frequency assumptions - selected source or port family - support status for each advanced feature - smoke command and result artifact location - whether any generated report, plot, or Touchstone file is derived from a supported, shadow, or experimental lane --- # Agent Design Workflows Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/markdown/agent/design-workflows.md Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/agent/design-workflows.mdx Agent-driven design should move from small, inspectable RF runs to larger sweeps or optimization only after the selected lane is understood. ## Workflow ladder 1. **Smoke geometry:** build the smallest geometry that exercises the intended source/port/observable. 2. **Single-run metric:** extract one metric such as resonance frequency, `|S11|`, return loss, transmitted power, or a probe amplitude. 3. **Sweep:** vary one or two physical parameters with `ParameterSweep` or a small Python loop. 4. **Report:** export plots, tables, Touchstone files, or JSON summaries with the exact support status. 5. **Optimize:** only optimize a metric whose differentiability and physics support boundaries are documented for the selected lane. ## Sweep template Supply `make_simulation(width, eps_r)` for your device; it must return a configured `Simulation`. This block is not a standalone program. ```python from rfx.batch import ParameterSweep, run_batch sweep = ParameterSweep(width=[0.028, 0.030, 0.032], eps_r=[3.8, 4.2]) results = run_batch(make_simulation, sweep, run_kwargs={"n_steps": 500}) for params, result in results: print(params, result.time_series.shape) ``` ## Reporting checklist - Which public guide or example did the agent start from? - Which feature lane was used: supported, shadow, experimental, blocked, or unsupported? - What exact command ran? - Where are generated files stored? - Which metric decided pass/fail? - Does the result require an external solver, analytic oracle, or dump replay before it becomes a claims-bearing validation result? --- # AI Agent Overview Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/markdown/agent/overview.md Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/agent/overview.mdx rfx is useful with AI coding agents, but public RF/FDTD claims still need the same support-boundary checks as human-authored code. Treat this page as the agent-facing entry point for safe, reproducible rfx work. The reviewed external-agent pages are available as Markdown alternates in the [LLM index](https://remilab.ai/rfx/llms.txt), alongside the typed API inventory and support contracts. ## Agent operating rules 1. **Start from the supported lane.** Prefer uniform Cartesian Yee RF workflows unless the task explicitly requires a shadow or experimental feature. 2. **Check support before claiming physics.** Use the support matrix and S-parameter matrix before describing a feature as validated. 3. **Use examples before inventing loops.** Start with the closest file in `examples/` or the public guide page, then adapt it. 4. **Keep generated artifacts separate.** Store plots, Touchstone files, HDF5 snapshots, and reports in an output directory rather than committing runtime artifacts. 5. **Record uncertainty.** If a workflow uses non-uniform mesh, subgridding, distributed execution, Floquet/Bloch, coaxial-line reflection, older coaxial S-matrix paths, or advanced inverse design, say whether it is supported, shadow, experimental, deprecated, or blocked. ## Core references - [Quick Start](https://remilab.ai/rfx/start/first-simulation/) - [Sources and Ports](https://remilab.ai/rfx/model/ports/) - [Port and source selection](https://remilab.ai/rfx/markdown/agent/port-selection.md) - [Design loop](https://remilab.ai/rfx/markdown/agent/recipe-design-loop.md) - [Probes and S-Parameters](https://remilab.ai/rfx/measure/s-parameters/) - [Support Boundaries](https://remilab.ai/rfx/support/) - [Validation](https://remilab.ai/rfx/validation/) - `docs/guides/support_matrix.md` - `docs/guides/sparameter_support_matrix.md` - `docs/guides/physics_validation_evidence_rule.md` ## Safe prompt skeleton ```text Use rfx for a uniform Cartesian Yee RF/FDTD workflow unless the support matrix says another lane is supported. Build from the closest public example, run the smallest smoke check, and report support status, commands, and artifacts. Do not claim broad validation for experimental lanes. ``` ## Current API defaults Source amplitude defaults to `amplitude_kind="current"`; specify it explicitly when adapting older examples (witness: `tests/unit/sources/test_source_amplitude_kind.py`). `write_touchstone` uses standard Touchstone ordering (witness: `tests/unit/api/test_touchstone_interop.py`). The ADI default is `adi_cfl_factor=2.0` (source: `rfx/api/__init__.py::Simulation.__init__`). Matrix products use `jax.lax.Precision.HIGHEST` (witness: `tests/contracts/test_matmul_precision.py`). Use `BoundarySpec` (`from rfx.boundaries.spec import BoundarySpec, Boundary`) through `Simulation(boundary=...)` to select boundary faces. --- # Port & Source Selection Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/markdown/agent/port-selection.md Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/agent/port-selection.mdx Use this page as the decision layer before choosing a primitive. The full primitive reference lives in the [sources and ports guide](https://remilab.ai/rfx/model/ports/); this page adds the device-class decision layer on top of it. For evidence status and normative envelopes, the [S-parameter support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.md) is the support contract; this guide does not supersede it. ## Selection Table | Device class | Primitive | Compute path | Status | Hard limit / envelope | |---|---|---|---|---| | Rectangular waveguide | `add_waveguide_port` | `compute_waveguide_s_matrix` | E5-narrow (current port) | Multi-port S-matrix; requires at least two waveguide ports. Single-mode; validated empty-guide, PEC-short, slab analytic + external references including Meep/OpenEMS/Palace. Chain closure (theta to S to objective) is **chain-closed (v1.8)** for the uniform single-mode guide on the `normalize=False` and `normalize="flux"` lanes, after three pre-declared WR-90 runs of the chain battery — with contract criterion 1 (forward identity: the reverse-mode-traced result equals the untraced one) and 3(a) (AD-vs-FD) read with JAX 64-bit mode enabled on the flux lane, the forward default still float32. A float32 gradient pipeline on the flux lane, junctions, multimode and nonuniform meshes are outside that declaration; "chain-closed" is not "supported". See the waveguide row of the [support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/support_matrix.md) for the numbers and fences before relying on this path for a differentiable chain. | | Microstrip / MSL | `add_msl_port` | `compute_msl_s_matrix` | E5-narrow / eigenmode-blocked | Use only within the characterized-not-tight MSL envelope in the [support contract](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.md). The MSL open-stub notch filter is compared with frozen external solver data by `tests/crossval/msl_notch_filter/test_msl_notch_filter.py`; the retired script's committed run logs and the numbers quoted from them left with it on 2026-09-22. Eigenmode is unsupported (fenced); the non-uniform graded-mesh lane runs via the laplace feed (internally gated, external validation still owed). | | Coaxial (promoted path) | `add_coaxial_port` | `compute_coaxial_line_reflection` | **broad_e5_passed** | One-port transmission-line reflection path (forward AND `jax.grad` optimization via `eps_scale`). Broad-E5 physics (analytic Γ envelope over short/open/matched and resistive loads 25 Ω / 100 Ω, two characteristic impedances, mesh-resolution sweep; `max \|Γ\|` dev 0.037 ≤ 0.05) + independent Meep broad-E4 cross-check 4–12 GHz, both committed under `tests/fixtures/`, + end-to-end differentiable (composition AD-vs-FD gate). Clean-checkout auditor returns `coaxial_port` PASSED. | | Lumped / RLC excitation | `add_lumped_rlc` / `add_port` | `Simulation.run(compute_s_params=True, ...)` or `Simulation.forward(port_s11_freqs=...)` | E2/E3 partial (lumped), E0/E1 (forward S11) | Lumped-port envelope for V/I extraction. Nonuniform lumped-port S-parameter extraction is not within the current validated envelope. `compute_s_params` path is only for `add_port` simulations. | | Free-space scattering | `add_tfsf_source` | Field, flux, or NTFF observables (where separately supported) | Not a port | Plane-wave total-field/scattered-field drive. Not a port calibration path; no promoted S-parameter compute path. | | Periodic surfaces | `add_floquet_port` | No promoted API | E2/E3-modal/slab-analytic; no promoted S-matrix API | Experimental Floquet/Bloch excitation. Modal oracle and analytic slab checks pass; no RCWA or external crossval. No promoted multi-port S-matrix compute path. Use only within the documented analytic-slab envelope. | | Antennas / radiation | `add_source` / `add_polarized_source` + `add_ntff_box` | Near-to-far-field post-processing | Supported (NTFF separately validated) | Use source primitives for drive; use `add_ntff_box` for radiation observables (directivity, RCS, far-field patterns). Do not route radiation results into port S-parameters without a supported extraction row. | ## Decision Tree 1. **Rectangular waveguide** → `add_waveguide_port` + `compute_waveguide_s_matrix`. Needs two or more ports for a full S-matrix. 2. **Microstrip / MSL guided line** → `add_msl_port` + `compute_msl_s_matrix`. Use the default `mode="laplace"` feed (works on uniform and non-uniform meshes; the non-uniform lane is internally gated, externally unvalidated). `mode="eigenmode"` is unsupported (fenced). 3. **Coaxial** → `add_coaxial_port` + `compute_coaxial_line_reflection`. This is the one-port claims surface; for a through line use `compute_coaxial_two_port`. 4. **Lumped component or discrete terminal** → `add_lumped_rlc` or `add_port`. Keep results within the lumped-port interpretation; this is not a waveguide or coaxial S-matrix path. 5. **Free-space incident field** → `add_tfsf_source`. No port calibration applies. 6. **Periodic structure requiring modal excitation** → `add_floquet_port` only within the E2/E3 analytic-slab envelope. No promoted S-matrix API. 7. **Antenna or radiation observable** → `add_source` or `add_polarized_source` plus `add_ntff_box`. ## Coaxial Compute Path Caveat `compute_coaxial_line_reflection` is the coaxial transmission-line reflection path and is `broad_e5_passed`: broad-E5 physics (analytic Γ envelope) + independent Meep broad-E4 short/open, both with committed evidence (`tests/fixtures/coax_broad_e5/`, `tests/fixtures/coax_broad_e4/`), plus an end-to-end differentiable path (`grad(S11)` w.r.t. the dielectric via `eps_scale`; composition AD-vs-FD gate `tests/unit/autodiff/test_coax_end_to_end_ad.py`). The clean-checkout auditor returns `coaxial_port` PASSED. The [support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.md) coaxial rows document the current lanes. ## Compute-Path/Port Pairing Rules Keep these pairings exact. Most mispairings raise a `ValueError`; treat any combination that does not raise as undefined rather than validated. - `compute_waveguide_s_matrix` → only for `add_waveguide_port` simulations - `compute_msl_s_matrix` → only for `add_msl_port` simulations - `compute_coaxial_line_reflection` → only for the documented one-port coaxial envelope - `run(compute_s_params=True, ...)` → only for `add_port` lumped/wire ports - `forward(port_s11_freqs=...)` → only for `add_port` lumped/wire ports ## When No Port Family Fits Stop before improvising. If no row in the [S-parameter support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.md) covers the geometry, boundary conditions, source normalization, and observable you need: 1. Check the full [support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.md) and [support matrix JSON](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.json) for any partial or experimental row that covers part of the envelope. 2. Check [docs/guides/](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides) for evidence rules and methodology notes. 3. Do not invent a new extraction scheme and label it validated. Add an explicit evidence row first. 4. If the structure is close to a supported family (e.g., a variant waveguide geometry), confirm it is within the documented envelope before claiming the family's status. An unverified extraction that reports plausible-looking numbers is not a validated result. ## When a Gate Fails Do not redesign extractors from the failure symptom alone — the common obvious-looking fixes in this area are already falsified. Walk the [failed-gate triage recipe](https://remilab.ai/rfx/markdown/agent/recipe-failed-gate-triage.md) first. `compute_far_field` is jittable; this does not establish support for an NTFF adjoint objective (witness: `tests/unit/autodiff/test_forward_jit_contract.py`). --- # Agent Prompt Templates Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/markdown/agent/prompt-templates.md Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/agent/prompt-templates.mdx Use these templates when delegating rfx work to an AI agent. ## Build a small supported-lane simulation ```text Create a minimal rfx uniform Cartesian Yee RF/FDTD example for . Use the closest public example first. Run a short smoke test, save generated artifacts outside git, and report support status, command output, and known limitations. ``` ## Review a physics claim ```text Review this rfx result as a physics claim. Check docs/guides/support_matrix.md, docs/guides/sparameter_support_matrix.md, and physics_validation_evidence_rule.md. Classify the claim as supported, shadow, experimental, blocked, or unsupported, and list the missing evidence if it is not supported. ``` ## Update public docs ```text Update the rfx public docs for . Verify every API name against current code, run scripts/check_public_docs_manifest.py, run the public docs sync check, and avoid broad production wording unless the support matrix explicitly allows it. ``` ## Prepare artifacts for a report ```text Generate a reproducible rfx report bundle for . Include command lines, input parameters, output paths, support status, and exact metrics. Keep plots, Touchstone files, and HDF5 snapshots out of git unless they are intentionally curated docs assets. ``` --- # Recipe: End-to-End Design Loop Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/markdown/agent/recipe-design-loop.md Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/agent/recipe-design-loop.mdx Use this recipe to run a complete gradient-based design loop. It chains the pages that each cover one decision: device class → port family → AD-safe compute path → objective → optimizer → validation verdict. The [prompt templates](https://remilab.ai/rfx/markdown/agent/prompt-templates.md) stop before optimization; this page goes to the end and identifies paths that are not ready for closed-loop design. ## The chain 1. **Device class → port family.** Use the [port selection page](https://remilab.ai/rfx/markdown/agent/port-selection.md). The port family determines the compute path and therefore which optimization method below applies. 2. **Check the compute path is AD-safe.** The [support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.json) carries an `ad_traceable` column per family with the evidence test named in `ad_evidence`. Two verified nuances: - Coaxial is `yes` for the documented scalar `eps_scale` path through `compute_coaxial_line_reflection`. The end-to-end AD/finite-difference check is `tests/unit/autodiff/test_coax_end_to_end_ad.py::test_coax_reflection_grad_finite_and_fd_consistent`. This does not extend the result to arbitrary coaxial geometry parameters. - Waveguide `yes` covers BOTH the default extraction (`normalize=False`) and the power-flux extraction (`normalize='flux'`), which is AD-traceable (evidence: `tests/unit/autodiff/test_waveguide_flux_ad.py`) — you can optimize directly through the production-recommended flux path. 3. **Pick the optimization method.** 4. **Validate the final design — never ship the loss number alone.** ## `optimize()` / `forward(port_s11_freqs=...)` for lumped and wire ports For lumped and wire-port devices, this is the supported end-to-end loop, and it is CI-gated: `tests/unit/autodiff/test_minimize_s11_at_freq_physical.py` runs a physical S11 design objective through `forward()` on every PR. - Objectives come from `rfx.optimize_objectives` (`minimize_s11`, `maximize_s21`, `target_impedance`, `minimize_reflected_energy`, `minimize_s11_at_freq_wave_decomp`, …). - Drive it via `optimize(sim, region, objective, ...)` with a `DesignRegion`, or hand-roll `jax.value_and_grad` over `sim.forward(port_s11_freqs=...)`. - Memory sizing: `n_steps` / `num_periods` control the AD tape; for large step counts pass `checkpoint_segments` (≈2× compute for ≈√n_steps memory; uniform single-device path only — non-uniform and `distributed=True` raise). - Pass `port_s11_freqs=...` whenever the objective reads port S11. ## Direct `jax.grad` through a waveguide S-matrix calculation `jax.grad` flows end-to-end through `sim.compute_waveguide_s_matrix(..., eps_override=eps(theta))` and `compute_msl_s_matrix` — the gradient itself is accurate (AD↔FD relative error 9.21e-5 on the last run of `examples/inverse_design/differentiable_s11_design.py`, whose gate is `tests/unit/autodiff/test_sparam_ad_end_to_end.py`). The objective callables from `rfx.optimize_objectives` compose here too — they accept any result exposing `.s_params` / `.freqs`. **Watch for a run-length-growth trap in the waveguide source `f0` default.** If an explicitly supplied `f0` lands below the mode cutoff, the launch is evanescent and the extracted S grows with `n_steps`. `f0=None` defaults to the center of the requested band, and preflight flags `port_source_below_cutoff` / `port_freqs_below_cutoff`. Always pass measurement `freqs` (and ideally an explicit in-band `f0`), keep every bin above the mode cutoff, and check the final design at 2× `n_steps` (run-length invariance) before trusting it. The direct MSL path does not have an equivalent verified design example. ## Direct `jax.grad` through a registered DFT-plane field observable For an objective defined directly on a frequency-domain FIELD rather than a port S-parameter (e.g. "minimize the worst-case field leaking through a shield"), register one or more `add_dft_plane_probe(...)` planes and pull them off the result with `rfx.observables`: - `dft_field(names)` -- accessor: single name returns the raw `(n_freqs, n1, n2)` complex accumulator; a list stacks (same-shape planes) or returns a per-name dict. - `field_energy(names)` -- `sum(|field|**2)`, a trivially smooth first objective. - `field_softmax(names, beta=...)` -- a temperature-controlled soft-max over space + frequency, for a worst-case-bin (not average) objective. `beta` is auto-scaled internally (`beta_eff = beta / stop_gradient(max(vals))`) and DIMENSIONLESS — it no longer needs hand-matching to `|field|**2`'s physical magnitude the way an earlier version did (issue #619; rfx's DFT-accumulator units commonly sit far from O(1)). The default (`beta=1.0`) is safe at any field magnitude, merely loose; raise `beta` (5-50 is a reasonable range for most plane sizes) for a tighter max approximation — see the function's own docstring and `examples/inverse_design/field_observable_shielding.py`'s measured `BETA` for a worked example. `beta` values tuned under the OLD (pre-#619-fix) unscaled semantics do NOT carry over — see `field_softmax`'s docstring "Auto-scaling" section for the migration note. Composition is `lambda p: field_softmax(names)(sim.forward(eps_override=p))` — same factory-returns-`callable(Result)` style as `rfx.optimize_objectives`, plugging directly into a hand-rolled `jax.grad` descent loop (or `optimize()` with a `DesignRegion`). Lane-agnostic: works on `run()`/`forward()` results from both the uniform and non-uniform meshes; fenced fail-loud on the two distributed lanes (neither sharded runner accumulates DFT planes today). ## `jacobian_fwd` — a many-output Jacobian in one batched forward pass For a many-output objective (the full `dft_field` tensor, not a scalar reduction — e.g. training a gradient-augmented surrogate on the field itself), reverse mode needs one `jax.grad`/`jax.vjp` call PER output, and `jax.jacrev` refuses a complex-dtype output outright (`TypeError` without `holomorphic=True`). `rfx.jacobian_fwd(sim_fn, params)` computes the whole Jacobian in one batched forward-mode pass instead: ```python value, jacobian = rfx.jacobian_fwd( lambda p: dft_field(name)(sim.forward(eps_override=eps(p), checkpoint=False)), params, ) ``` with `params` a pytree of scalar design variables (the default `tangents="identity"` builds one tangent direction per scalar leaf — pass an explicit tangent pytree/matrix, same structure as `params` with a leading `n_t` axis, for a flat `(n_p,)` design vector or a subset of columns). `jacobian` mirrors `sim_fn`'s OUTPUT, not `params` — every leaf gains a new leading `n_t` axis holding the directional derivative along each tangent direction, in `tangents="identity"`'s case matching `params`' flattened leaf order. This is PACKAGING over stock `jax.vmap(jax.jvp(...))`, not new solver capability — forward mode already ran end-to-end through the same `forward()` -> `rfx.observables` path with zero solver changes. Read before reaching for it: - **Not "a few times one solve, not N times."** Measured cost scales roughly `(1 + n_t)` plain solves in both wall time and flops, and that ratio MOVES with grid size — regenerate `scripts/benchmark_jacobian_fwd.py` for a current number rather than trusting one written down elsewhere. The real argument for this mode is MEMORY: forward-mode peak is `O(state) * (1 + n_t)` and independent of `n_steps`, while reverse mode on a single scalar output is tens of times one plain solve on the same fixture and cannot produce a many-output Jacobian at all. - The returned Jacobian is `dy/dx` UNCONJUGATED for a complex `value` — differs by a conjugate from anything built through `jax.vjp`/`jax.grad`. - Uniform single-device lane only. `n_warmup` and non-uniform + `distributed=True` (with a registered DFT-plane probe) both raise `NotImplementedError` via `forward()`'s own pre-existing checks (`n_warmup` used to be a measured silent no-op on this lane — issue #626 turned that into a fail-loud fence instead). `checkpoint`/ `checkpoint_segments` are measured exactly neutral under forward mode — no raise to catch, so it stays a documented trap in `jacobian_fwd`'s own docstring rather than being plumbed through. - "Geometry parameter" still means the topology-density pixel / `pec_occupancy_override` / `dz_profile` design channels from the section above — not a parametric dimension (`Box` corners are not differentiable through `forward()`). Full contract, fence reasoning, and gates: `rfx/observables.py`'s `jacobian_fwd` docstring and `tests/unit/autodiff/test_jacobian_fwd.py`. ## Validate either method - `rfx.validate_port_smatrix(result)` — passivity/shape verdict. A `passivity_violation` issue means the *extraction* is not trustworthy at that configuration; stop and triage, don't re-tune the design. - Re-run the final design with preflight visible (`sim.preflight()`) and quote every warning — the loops above usually suppress warnings inside `jax.grad`. - Check run-length invariance: re-measure the final design at 2× n_steps. If the S-parameters move materially, the measurement was not converged and the "optimum" is suspect. - Check the GRADIENT's record length too, separately from the value: `rfx.gradient_record_length_witness(objective, params, n_steps, tol=...)` differentiates `objective(params, n_steps)` at `n_steps` and at `factor * n_steps` (default 2) and reports whether the gradient moved. A record whose value has settled can still give a gradient off by tens of percent, and agreement between AD and finite differences does not show this, because both differentiate the same truncated record. `tol` has no default; its docstring lists measured cases to choose it from. Report its verdict fields by their own names: `passed`, `worst` (the largest relative change of the gradient vector over the bins), `n_steps` and `n_steps_long`. `grad_rel_change` is a per-element table for locating a failure, not the verdict. The pattern, as used in [`examples/inverse_design/multilayer_ar_coating.py`](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/examples/inverse_design/multilayer_ar_coating.py): ```python from rfx import gradient_record_length_witness def objective(params, n_steps): # the observable from a record of n_steps result = sim.forward(eps_override=render(params), n_steps=n_steps) return figure_of_merit(result, n_steps) witness = gradient_record_length_witness(objective, params, N_STEPS, tol=0.05) print(f"passed={witness.passed} worst={witness.worst:.3g} " f"n_steps={witness.n_steps} n_steps_long={witness.n_steps_long}") ``` For a gradient through the ring-down completion, use `rfx.ringdown.gradient_witness` instead ([resonance recipe](https://remilab.ai/rfx/markdown/agent/recipe-resonance-extraction.md)). - Check the full |S11(f)| curve, not just the objective bin. ## Verified pitfalls - **Below-cutoff port frequencies NaN the gradient.** Every waveguide-port frequency must be above the guide cutoff (WR-90 TE10: ~6.56 GHz). The forward |S| stays finite; only the gradient NaNs — a silent trap. Pass explicit `freqs=` to `add_waveguide_port`. - **Ports inside the CPML.** `cpml_layers=8` at dx=2mm is a 16mm absorber per side — easy to park a port inside it. `sim.preflight()` flags this exactly; never optimize with warnings suppressed before reading them once. - **`compute_waveguide_s_matrix` takes no `freqs` argument** — the result carries its own grid (`res.freqs`); select bins from it. - **An improving loss is not an improving design.** In one design toy, the optimizer "improved" the loss 100× by saturating the design variable — the underlying extraction was the thing moving. The validation step above is what catches this. ## Related - [Design workflows](https://remilab.ai/rfx/markdown/agent/design-workflows.md) — broader optimization context. - [Parameter sweeps](https://remilab.ai/rfx/markdown/agent/recipe-parameter-sweeps.md) — scanning instead of optimizing. - [Failed-gate triage](https://remilab.ai/rfx/markdown/agent/recipe-failed-gate-triage.md) — when the validation verdict fails. ## JIT and the adjoint path `forward()` works inside an outer `jax.jit` for every single-device model (witness: `tests/unit/autodiff/test_forward_jit_contract.py`). For design permittivity on the uniform path with E-DFT objectives, `forward(gradient="adjoint")` is available (witness: `tests/unit/autodiff/test_reciprocity_adjoint.py`); NTFF objectives are outside this adjoint contract. `maximize_directivity` requires its default `log_ratio=True`; passing `log_ratio=False` raises (source: `rfx/optimize_objectives.py::maximize_directivity`). --- # Recipe: Failed-Gate Triage Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/markdown/agent/recipe-failed-gate-triage.md Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/agent/recipe-failed-gate-triage.mdx Use this recipe when a validation gate fails — non-passive `|S11| > 1`, a resonance that looks wrong, an R/T mismatch, or a mesh-convergence miss. Walk the tree before designing a fix. Terminal **STOP** nodes mark paths already implemented and falsified — do not re-attempt one without new evidence. Symptoms of this kind have consistently resolved to a comparator or extractor bug, not an FDTD solver residual. ## Step 0 — never triage from the headline metric Dump the full per-frequency curve and at least one independent witness (signs, magnitudes vs an analytic expectation, a sibling run) before believing a scalar pass/fail. Canonical trap: the `|S11|` magnitude dip of a directly edge-fed patch is easy to read as "the resonance". It is not — for a high-impedance edge-fed patch the dip is the off-resonance match point. The true resonance comes from a source-off probe trace (`Result.find_resonances()`, see the [resonance extraction recipe](https://remilab.ai/rfx/markdown/agent/recipe-resonance-extraction.md)) and agreed with the analytic value all along. The gate was re-specified, not the solver fixed. ## Step 1 — validate the metric, not the physics 1. Run `rfx.validate_port_smatrix(result)` — passivity, reciprocity, and shape sanity on any port result. 2. If raw V/I dumps exist, replay them through the production assembly with `rfx.replay_smatrix_from_vi_dump(...)`. If the replay disagrees with the production number, the bug is in a diagnostic or comparator, not in the solver. 3. Quote every preflight warning verbatim before reporting numbers — `sim.preflight()` surfaces real setup mistakes. ## Step 2 — branch by symptom ### A. Microstrip `|S11| > 1` (non-passive) `compute_msl_s_matrix` returns the raw extraction, so a bound violation reaches you instead of being clipped. Read `result.sigma_max_excess` — `max(σ_max(S(f)) − 1, 0)` per bin — to see how far over and where, then `settling_db` and `reliable` for the cause. `enforce_passivity=True` bounds the matrix as a post-process; it does not make the touched bins measurements. Check probe placement first: the default probe offset is floored to clear the source fringing transient; with the floor the reference patch device is passive at every tested mesh. **STOP — swapping in a voltage-only spatial-fit (γ/α) extractor.** The fit is ill-conditioned at high-SWR resonant loads — the forward-wave amplitude collapses at resonance, so the extracted `|S11|` blows up non-passively and gets *worse* with mesh refinement. The matched-line regime is a separate question and is not addressed by this extractor. ### B. Resonance / dip location looks wrong Measure the resonance with a source-off probe trace first (Step 0). Then: **STOP — refining the mesh to move the microstrip patch dip.** The dip location decelerates toward an asymptote outside the expected band; pushing it in-band needs a cell count far beyond practical GPU budgets, and the dip is the wrong observable anyway (match point, not resonance — see Step 0). **STOP — Zin-rotation de-embedding** (rotating V/I to the device edge via `tan(βd)`). The rotation amplifies the feed-line propagation-constant error and yields incoherent device impedances across `+βd`/`−βd`. Use the de-embedding built into the production extractors instead. ### C. Waveguide per-frequency `|S11|` oscillation or magnitude deficit **STOP — redesigning the waveguide port extractor for this symptom.** The WR-90 PEC-short per-frequency oscillation traces to a missing Yee leapfrog half-step correction in a *diagnostic script*, not in production; `compute_waveguide_s_matrix` is accurate. The remaining magnitude floor is Yee grid dispersion, which no extractor change removes. Cross-solver *phase* disagreements at single frequencies are usually solver-convention mismatches — Meep, OpenEMS, and Palace can disagree with each other by 100°+ on the same slab. ### D. Curved-PEC / staircase accuracy miss **STOP — implementing a conformal-PEC scheme.** Four standard methods (Dey–Mittra, Kottke tensor, and two contour-FIT variants) were implemented and falsified under the production PEC + CPML configuration — none was both stable and better than staircase. **Staircase is the supported floor**; see the [conformal PEC guide](https://remilab.ai/rfx/guide/conformal-pec/) for the honest envelope. Use a finer mesh or `NonUniformGrid` instead. ### E. "We need subgridding" **STOP — SBP-SAT subgridding.** The SBP-SAT boundary stencil loses closure under FDTD leapfrog and is incompatible with CPML. The supported alternative is `NonUniformGrid` (see the [non-uniform mesh guide](https://remilab.ai/rfx/model/domain-mesh/)). ## Step 3 — none of the branches match You may have a genuinely new failure. Open an issue with the full per-frequency dump, the `validate_port_smatrix` output, every preflight warning, and the independent witness from Step 0 attached. Budget one fix attempt; if it does not close the gap, stop and redesign rather than iterating parameter tweaks. ## Related - [Port & source selection](https://remilab.ai/rfx/markdown/agent/port-selection.md) — choose the right primitive before debugging the wrong one. - [Support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.md) — per-family validation envelopes and AD-traceability. --- # Recipe: Parameter Sweeps Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/markdown/agent/recipe-parameter-sweeps.md Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/agent/recipe-parameter-sweeps.mdx `parametric_sweep` and `vmap_material_sweep` are top-level rfx exports; import `ntff_sweep` from `rfx.ntff_sweep`. rfx has three sweep entry points. Pick by **what varies** between sweep points, not by what is convenient to write. ## Decision tree 1. **Geometry or structure changes per point** (patch length, slot gap, port position, …) → `parametric_sweep(sim_factory, param_name, param_values)`. Runs one full simulation per value, so it works with everything — ports, S-parameters, NTFF. Returns a `SweepResult` with `.s11_min_db()` / `.peak_field()` extractors and `plot_sweep` for visualization. 2. **Only a material parameter changes** (`"eps_r"`, `"sigma"`, `"mu_r"`, or scoped to one material as `"substrate.eps_r"`) → `vmap_material_sweep`. The entire FDTD time loop is `jax.vmap`-ed into a single fused kernel — typically the fastest option on GPU, with memory scaling as `n_batch × grid_size` plus `n_batch × n_planes × n_freqs × n1 × n2` complex for any registered DFT plane accumulators. - The vmap fast path supports **only**: PEC/periodic walls, CPML absorbing boundaries, soft sources, point probes, and DFT plane probes (`add_dft_plane_probe`). - Simulations using UPML, lumped RLC elements, lumped/MSL/coaxial/floquet ports, TFSF, material dispersion, waveguide ports, flux monitors, NTFF, or a non-uniform mesh take a **sequential fallback** — same results, without the fused-kernel speedup. The fallback carries the full material specification (including Kerr χ³) and also returns DFT plane accumulators, so a registered plane works on either path. 3. **Far-field over many frequencies** where the NTFF accumulator dominates memory → `ntff_sweep(sim, freqs, batch_size=…)`. Requires `add_ntff_box(...)` on the simulation; accumulates in memory-bounded frequency batches and returns the merged NTFF data. ## Where each lane is the wrong tool - A hand-rolled Python loop over `sim.run()` for a material-only sweep gives up the fused-kernel speedup that lane 2 exists for. - `vmap_material_sweep` cannot sweep geometry — only material fields are batchable; geometry changes the stamped arrays themselves. - If you want to *optimize* a parameter with gradients rather than scan it, none of these lanes apply — use `forward()` / `optimize()` (see the [design workflows page](https://remilab.ai/rfx/markdown/agent/design-workflows.md)). ## Result containers Both result classes share one metric kernel (`rfx.sweep.peak_abs_field`): - `SweepResult.peak_field()` — one value per sweep point (NaN where a result has no probe time series). - `VmapSweepResult.peak_field()` — vectorized over the stacked `(n_batch, n_steps, n_probes)` array. ## Related - [Port & source selection](https://remilab.ai/rfx/markdown/agent/port-selection.md) — choose the primitive before sweeping it. - [Failed-gate triage](https://remilab.ai/rfx/markdown/agent/recipe-failed-gate-triage.md) — if a sweep surfaces a gate failure. --- # Resonance Extraction Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/markdown/agent/recipe-resonance-extraction.md Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/agent/recipe-resonance-extraction.mdx A port-match dip and a modal resonance are different measurements. Use `Result.find_resonances` for a source-off probe trace, and inspect its frequency range and source-decay-time arguments in the API reference. Use the [R/T recipe](https://remilab.ai/rfx/markdown/agent/recipe-rt-measurement.md) for power spectra. ## Ringdown API controls Import `RingdownSpec` and `gradient_witness` from `rfx.ringdown`; `gradient_record_length_witness` is a top-level rfx export. `run(ringdown=...)` and `forward(ringdown=...)` accept a `RingdownSpec`. Use `RingdownSpec(identification_probes=...)` to declare identification probes explicitly. `until_identified` is the identification-based stopping control on `run`; read the method's supported-configuration checks before combining it with other run options. `gradient_witness` and `gradient_record_length_witness` are the user-facing checks for a gradient workflow. Consult their current signatures and reported verdicts rather than inferring validity from a successful run. This recipe specifies API selection only. It does not supply device geometry, a probe trace, or an executable simulation. --- # Recipe: Broadband R(f)/T(f) of a Slab or Scatterer Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/markdown/agent/recipe-rt-measurement.md Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/agent/recipe-rt-measurement.mdx Use this recipe when you need broadband power R(f) / T(f) spectra of a slab, thin-film stack, periodic surface, or other scatterer. For guided-mode S-parameters in a bounded waveguide, see [the sources and ports guide](https://remilab.ai/rfx/model/ports/) instead. ## When to use - Broadband reflectance and transmittance vs frequency for free-space or periodic-boundary geometries. - Dispersive materials (Lorentz resonances, Drude metals) where frequency dependence is essential. - Any case where the analytic transfer-matrix or Fresnel stack result is the benchmark. **Not for**: guided modes, coaxial lines, microstrip — those have dedicated port primitives and compute paths. ## Right primitives ### `add_flux_monitor` On-the-fly DFT accumulation of the Poynting flux on a plane. Each frequency component is integrated continuously throughout the run, so no time-series storage is needed and no post-run FFT is required. ```python sim.add_flux_monitor(axis="x", coordinate=refl_x, freqs=freqs_rt, name="refl") sim.add_flux_monitor(axis="x", coordinate=trans_x, freqs=freqs_rt, name="trans") ``` ### `run(until_decay=...)` Decay-adaptive termination (see the `Simulation.run()` docstring for the authoritative lane-by-lane description). The stop criterion depends on the boundary: on absorbing (CPML/UPML) boundaries — including the non-uniform `dx/dy/dz`-profile runner — the run stops when the total interior-domain energy decays to `until_decay * peak` (issue #169); on closed/PEC boundaries the uniform runner falls back to the field at a single monitor position, and `decay_monitor_component` / `decay_monitor_position` control which field and where to sample that fallback. This kwarg is load-bearing for accuracy: a fixed `n_steps` truncates the signal and aliases residual energy into the spectrum. ### Two-run reference subtraction Run once without the scatterer (reference run) and once with it (sample run). Subtract the DFT accumulators at the field level before computing the power flux. ## Two-run template This template requires domain dimensions, frequency samples, source settings, monitor coordinates and a Lorentz pole supplied by the caller; the complete `examples/tutorials/slab_rt_flux_monitor.py` is the runnable reference. ```python import numpy as np from rfx import Simulation, Box from rfx.probes.probes import flux_spectrum, subtract_flux_monitors def build_sim(with_slab: bool) -> Simulation: sim = Simulation( freq_max=freq_max, domain=(dom_x, dom_y, dx), dx=dx, boundary="cpml", cpml_layers=20, mode="2d_tmz", ) if with_slab: sim.add_material("slab", eps_r=eps_inf, lorentz_poles=[pole]) # Box must span full y/z extent including CPML padding — # with TFSF, transverse axes are periodic, and any cell # outside the material mask breaks the plane-wave assumption. sim.add(Box((slab_x_lo, -1, -1), (slab_x_hi, 1, 1)), material="slab") sim.add_tfsf_source(f0=f0, bandwidth=bw, polarization="ez", direction="+x") sim.add_flux_monitor(axis="x", coordinate=refl_x, freqs=freqs_rt, name="refl") sim.add_flux_monitor(axis="x", coordinate=trans_x, freqs=freqs_rt, name="trans") return sim # Run 1 — reference (no scatterer) sim_ref = build_sim(with_slab=False) res_ref = sim_ref.run( n_steps=12000, until_decay=1e-5, decay_monitor_component="ez", decay_monitor_position=(trans_x, y_center, 0), ) ref_refl_fm = res_ref.flux_monitors["refl"] ref_trans_flux = np.asarray(flux_spectrum(res_ref.flux_monitors["trans"])) # Run 2 — sample (with scatterer) sim_slab = build_sim(with_slab=True) res_slab = sim_slab.run( n_steps=12000, until_decay=1e-5, decay_monitor_component="ez", decay_monitor_position=(trans_x, y_center, 0), ) slab_refl_fm = res_slab.flux_monitors["refl"] slab_trans_flux = np.asarray(flux_spectrum(res_slab.flux_monitors["trans"])) # Field-level subtraction for reflection scat_refl_fm = subtract_flux_monitors(slab_refl_fm, ref_refl_fm) scat_refl_flux = np.asarray(flux_spectrum(scat_refl_fm)) # Reflection and transmission coefficients T = slab_trans_flux / ref_trans_flux # transmittance R = -scat_refl_flux / ref_trans_flux # reflectance (sign: reflection is −x) ``` ## Critical details ### (a) Field-level subtraction is required Poynting flux is bilinear in E and H. If you subtract the pre-computed flux values (`F_slab - F_ref`), the cross terms do not cancel, and the result is wrong for all scatterers. Subtracting the E and H DFT accumulators first, then calling `flux_spectrum`, is exact because Maxwell's equations are linear. `subtract_flux_monitors(sample, ref)` does exactly that, so nobody has to write out the four private accumulator field names — and it raises `ValueError` instead of broadcasting when the two monitors do not cover the same plane and the same frequencies. ### (b) `until_decay` is load-bearing A fixed `n_steps` makes the DFT aliasing depend on where the signal happens to be truncated. For dispersive or high-Q scatterers, use `until_decay=1e-5` or tighter. For simple lossless slabs `1e-3` is often sufficient. `until_decay` is not supported with `solver='adi'`; a `NotImplementedError` is raised before the first step if you try. ### (c) TFSF transverse-periodicity gotcha `add_tfsf_source` forces the transverse axes (y, z) to be periodic. The material `Box` must span the full y/z extent including the CPML padding. Any cell outside the material mask breaks the plane-wave assumption. Use oversized corners: `Box((x_lo, -1, -1), (x_hi, 1, 1))`. ### (d) Draw the slab at its corners, on the lattice A dielectric `Box` is sampled at NODE coordinates, half-open `[lo, hi)`: a slab drawn `lo = i*dx` to `hi = k*dx` owns nodes `i .. k-1` and realizes exactly `hi - lo`. No nudge is needed or wanted. What DOES bite is the float route to the corner. `center + d/2` can land one ulp above the node that `hi` is meant to be, the half-open test then keeps that node, and the slab realizes one cell too many against an oracle evaluated at `d`. Spell on-lattice corners in lattice arithmetic — `i_lo = round((center - d/2)/dx)`, `x_lo = i_lo*dx`, `x_hi = (i_lo + round(d/dx))*dx` — and assert the realized extent from `sim.fidelity_report()` before quoting anything against a Fresnel oracle. `examples/tutorials/slab_rt_flux_monitor.py` does both. ### (e) dtype-truncation warnings are expected, not errors Under the default `precision="float32"`, the flux DFT accumulators emit benign `complex128 → complex64` truncation `UserWarning`s (from `rfx/probes/probes.py`) on every run. They are expected and do not affect the R/T result — do **not** enable `jax_enable_x64` globally to silence them. ## Do NOT use **`add_probe` (time-series) + `np.fft.rfft`** for dispersive media. A chirped pulse transmitted through a dispersive slab has frequencies arriving at different times. Any time-gate window (Hann, Tukey, rectangular) introduces a frequency-dependent bias because the chirped transmitted pulse and the incident reference pulse have different effective envelopes under the same window. This bias is independent of grid resolution — halving `dx` does not reduce it. `add_flux_monitor` sidesteps this entirely: each frequency is integrated continuously, and `until_decay` ensures the signal has truly ended before the DFT is read. For dispersionless media there is no group-velocity chirp, so the raw time-series approach does not produce this artifact — but `add_flux_monitor` is still preferred for its cleaner interface and decay-adaptive stopping. ## Known accuracy The cross-validation case that gated normal-incidence slab R(f) and T(f) against the analytic Fresnel / transfer-matrix result was removed on 2026-09-21. The analytic slab comparisons that remain are in `tests/oracle/`. Do not read a band-mean threshold as an accuracy statement about the fringe structure: a band mean over an interference pattern is blind to every zero-mean shape error, and with mean gates alone that case passed while the true slab permittivity was 12.33% away from what the FDTD built (issue #812). If you need an accuracy figure for a slab, quote per-fringe deviations, not band means. For dispersive media there is currently **no tracked R(f)/T(f) accuracy oracle** in this repository — treat dispersive R/T results as unvalidated against analytic references until such a gate exists. Two source-verifiable facts to keep in mind when comparing against other solvers: rfx discretizes Lorentz/Debye poles with a second-order central-difference ADE (`rfx/materials/lorentz.py`), while some other FDTD solvers use recursive-convolution D-field updates; both are O(Δt²) but produce slightly different effective ε(f) near stiff resonances, so a small residual between solvers at a sharp Lorentz peak is a discretization-level difference, not necessarily a usage error in either tool. ## Canonical examples - `examples/tutorials/slab_rt_flux_monitor.py` — **the recommended path**: `add_flux_monitor` + two-run field-level reference subtraction via the `Simulation` API, with an analytic Fresnel witness and an energy-conservation readout (on the coarse demo grid R+T sits slightly above 1; refine `dx` to tighten it). Start here. ## Companion primitives - `add_dft_plane_probe` — full complex E/H snapshot on a 2D plane; useful for mode patterns and near-field maps. - `add_ntff_box` — near-to-far-field transform for 3D scatterers. - `compute_waveguide_s_matrix` — S-parameters in bounded waveguides (not plane-wave); see [the sources and ports guide](https://remilab.ai/rfx/model/ports/). --- # Automation Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/api/functions/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/api/functions.mdx These helpers propose a mesh, domain, absorber and run length from your geometry and frequency range, and build graded cell profiles. Use them for a first setup, then read what they chose and check it with preflight. For when to grade a mesh at all, see [Non-Uniform Mesh](https://remilab.ai/rfx/model/domain-mesh/). ## Planning from geometry | Helper | Signature | Returns | |---|---|---| | `plan_mesh` | `plan_mesh(geometry, freq_range, materials=None, accuracy="standard", *, boundary="cpml", dx_override=None, margin_override=None, n_steps_override=None, max_memory_mb=None, artifact_root=None)` | `MeshPlan` | | `auto_configure` | `auto_configure(geometry, freq_range, materials=None, accuracy="standard", *, boundary="cpml", dx_override=None, margin_override=None, n_steps_override=None, max_memory_mb=None, waveform=None, thin_conductors=())` | `SimConfig` | - `geometry`: a list of `(shape, material_name)` pairs. - `freq_range`: `(f_min, f_max)` in Hz. `f_max` sets the cell size, `f_min` the margin around the geometry. - `materials`: `{name: {"eps_r": ..., "sigma": ...}}` for every material in `geometry`. Pass it even for library names such as `"fr4"`: a material the planner cannot find is treated as vacuum, and the cell comes out too coarse. - `accuracy`: `"draft"` (10 cells per wavelength), `"standard"` (20) or `"high"` (40). The thinnest feature can force a finer cell than that. - `max_memory_mb`: coarsen `dx` until the estimated memory fits. - `thin_conductors`: declared sheets. Each sheet's plane becomes a z mesh line, so the sheet lands where you drew it. - A thin z feature makes the plan grade z and return a `dz_profile`. `plan_mesh` wraps `auto_configure` and adds a serializable record of what it chose. Prefer it. ```python import numpy as np from rfx import Box, Simulation, plan_mesh geometry = [(Box((0.0, 0.0, 0.0), (0.02, 0.01, 0.0016)), "fr4")] materials = {"fr4": {"eps_r": 4.4, "sigma": 0.025}} plan = plan_mesh(geometry, freq_range=(1.5e9, 3.5e9), materials=materials, accuracy="standard") print(plan.to_markdown()) kwargs = plan.to_sim_kwargs() # freq_max, domain, boundary, cpml_layers, dx (+ dz_profile) sim = Simulation(**kwargs) print(sorted(kwargs)) ``` `MeshPlan` holds the derived `domain`, `cell_sizes`, `cfl`, `absorber`, `memory`, `grid_shape`, `support_checks` and `warnings`. Export it with `to_dict()`, `to_json()` or `to_markdown()`. Values rfx could not determine are `None`, not missing. `artifact_root` only records intended output paths; it writes nothing. `to_sim_kwargs()` gives the constructor arguments only. It does not add your geometry or move it into the new domain. Add the shapes yourself, a few cells in from the domain edges (where the absorber begins) and with thin layers aligned to the final profile's cell edges, then run `sim.preflight()` and resolve what it reports. `SimConfig.summary()` prints the same choices as text. `sim.plan_mesh(...)` is the equivalent for a simulation you have already built. It also runs preflight and can check an S-parameter calculator; see [Simulation](https://remilab.ai/rfx/api/simulation/#checking-before-you-run). `Simulation.auto(freq_range, ...)` builds a simulation from the frequency range alone, with no geometry; see [Simulation](https://remilab.ai/rfx/api/simulation/#simulationauto). ## Building graded profiles by hand | Helper | Signature | What it does | |---|---|---| | `make_band_profile` | `make_band_profile(edges, cell_sizes, *, max_ratio=1.4, protected=None, boundary_cell=None, min_cells=1)` | Builds a profile from a declared stack. Every interface in `edges` lands exactly on a node plane, and every adjacent ratio stays at or below `max_ratio`. | | `smooth_grading` | `smooth_grading(cells, max_ratio=1.3, *, preserve_regions=None)` | Inserts transition cells so no adjacent ratio exceeds `max_ratio`. | | `apply_thirds_rule` | `apply_thirds_rule(cells, boundary_indices)` | Splits the cells at the given material interfaces into 1/3 + 2/3 pairs. | ```python from rfx import apply_thirds_rule, make_band_profile, smooth_grading # 10 mm of 1 mm air, a 1.5 mm substrate at 0.25 mm, 13.5 mm of 1 mm air. dz = make_band_profile([0.0, 0.010, 0.0115, 0.025], [1e-3, 0.25e-3, 1e-3], max_ratio=1.4, protected=[False, True, False]) print(len(dz), f"{dz.sum() * 1e3:.3f} mm", f"max ratio {np.max(dz[1:] / dz[:-1]):.2f}") graded = smooth_grading([1e-3] * 4 + [0.25e-3] * 6 + [1e-3] * 4, max_ratio=1.3) print(len(graded), f"{graded.sum() * 1e3:.3f} mm") # longer: cells were inserted ``` `make_band_profile` is the one to use when layer faces must sit on grid planes: the total length and every interface are exact. `smooth_grading` **adds** cells, so the total length and the positions after the inserted cells change. Recompute your layer coordinates from the final profile's cumulative sum. Use `max_ratio=1.4` along z and 1.3 in-plane; those are the ratios rfx has evidence for, and preflight flags larger ones. ## Limits - The plan is a starting point, not a validation. It has not run `Simulation.preflight()` for geometry you add later, nor a convergence study. - The cell count per wavelength in `accuracy` is a free-space figure. Check that dielectrics still get enough cells: preflight warns below 15 per wavelength in the material. - On a graded mesh, a `Box` whose faces fall between grid planes snaps to the nearest one. Preflight reports this as `graded_box_rasterization`. Align layers to the profile's cell edges. - Keep the grading out of the absorber: the first and last `cpml_layers` cells of a profile on a CPML axis should be uniform. Preflight warns with `nu_grading_reaches_absorber`. --- # Generated API Reference Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/api/generated/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/api/generated/index.mdx [Open the complete Python reference](https://remilab.ai/rfx/api/generated/rfx.html). The reference includes the public `Simulation` builder, execution and measurement methods, including methods inherited from internal mixins. Each build displays its package version, documentation channel and source commit. The source links point to that commit. For an agent or a script, use the [typed API inventory](https://remilab.ai/rfx/api/inventory.json). It includes parameter kinds, annotations, defaults, return annotations, docstrings and source links. An absent annotation or reference link is represented as `null`, rather than inferred. Importable symbols are broader than the recommended workflows. Read the [API guide](https://remilab.ai/rfx/api/) and [support boundaries](https://remilab.ai/rfx/support/) before selecting a source, port, mesh or observable. [Machine-readable documentation](https://remilab.ai/rfx/api/machine-readable/) describes the Markdown and JSON entry points. --- # Geometry and Materials Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/api/geometry-materials/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/api/geometry-materials.mdx This page lists the calls that define what is in the domain: materials, shapes, metal sheets and dispersive media. The guide [Materials and Geometry](https://remilab.ai/rfx/model/materials/) explains how shapes land on the grid and which kind of conductor to use. All lengths are in metres and conductivity is in S/m. ## Materials ```text Simulation.add_material(name, *, eps_r=1.0, sigma=0.0, mu_r=1.0, debye_poles=None, lorentz_poles=None, chi3=0.0) -> Simulation ``` Registers a named, isotropic material. Use the name in `add(...)`. ```python import numpy as np from rfx import (Box, Cylinder, DebyePole, PolylineWire, Simulation, Sphere, Via, drude_pole, lorentz_pole) sim = Simulation(freq_max=5e9, domain=(0.04, 0.03, 0.02), dx=1e-3, boundary="cpml", cpml_layers=6) sim.add_material("substrate", eps_r=4.3, sigma=0.02) sim.add_material("lossy_block", sigma=5.0e5) # below the PEC threshold sim.add_material("water_body", eps_r=4.9, debye_poles=[DebyePole(delta_eps=74.1, tau=8.3e-12)]) sim.add_material("resonant", lorentz_poles=[ lorentz_pole(delta_eps=2.0, omega_0=2 * np.pi * 10e9, delta=1e8)]) sim.add_material("plasma", # 10 GHz plasma frequency lorentz_poles=[drude_pole(omega_p=2 * np.pi * 10e9, gamma=2 * np.pi * 1e9)]) ``` | Argument | Meaning | |---|---| | `eps_r` | relative permittivity; for a dispersive material this is ε∞ | | `sigma` | conductivity in S/m. At or above `1e6` the material is a perfect conductor (see below). | | `mu_r` | relative permeability | | `debye_poles` | list of `DebyePole(delta_eps, tau)`: Δε = εs − ε∞, τ in seconds | | `lorentz_poles` | list of `LorentzPole`. Build them with `lorentz_pole(delta_eps, omega_0, delta)` or `drude_pole(omega_p, gamma)` (all rates in rad/s) rather than by hand. A pole set above its own time-step limit on the model's cells is refused when the model is assembled (an optical-frequency metal such as gold always is on RF cells). | | `chi3` | Kerr (third-order) nonlinearity | **Library materials** need no `add_material` call: `"vacuum"`, `"air"`, `"fr4"`, `"rogers4003c"`, `"rogers4350b"`, `"rt_duroid_5880"`, `"alumina"`, `"silicon"`, `"ptfe"`, `"water_20c"`, `"copper"`, `"aluminum"` and `"pec"`. The values are in `rfx.MATERIAL_LIBRARY`. The dielectric losses there are a constant conductivity matched at one frequency, so use your laminate's data for real designs. **The conductor threshold.** Any material with `sigma >= 1e6` S/m is treated as PEC: tangential E is forced to zero and the conductivity is not used. So `"copper"` and `"aluminum"` are lossless PEC, and their skin depth is not resolved. Keep `sigma` below `1e6` for a lossy bulk medium. For conductor loss on a metal sheet, use `add_thin_conductor(..., surface_impedance_f0=f0)`. **Fitting measured data.** `fit_debye(freqs, eps_measured, n_poles=1, *, eps_inf=None)` and `fit_lorentz(...)` fit poles to complex permittivity data; `load_material_csv(path, ...)` reads it from a CSV file. See the [generated reference](https://remilab.ai/rfx/api/generated/). ## Placing shapes ```text Simulation.add(shape, *, material) -> Simulation ``` Fills the shape with a registered or library material. Shapes are rasterized in the order you add them. Where dielectrics overlap, the later one wins. Metal accumulates: a later shape cannot cut a hole in a conductor. ```python sim.add(Box((0.005, 0.005, 0.004), (0.035, 0.025, 0.0056)), material="substrate") sim.add(Sphere((0.020, 0.015, 0.012), radius=0.003), material="alumina") sim.add(Cylinder((0.010, 0.010, 0.0048), radius=0.0005, height=0.0016, axis="z"), material="pec") sim.add(PolylineWire(((0.028, 0.010, 0.008), (0.028, 0.020, 0.008)), radius=0), material="pec") sim.add(Via(center=(0.015, 0.020), drill_radius=0.3e-3, pad_radius=0.6e-3, layers=[(0.004, 0.0056)]), material="pec") ``` | Shape | Signature | Notes | |---|---|---| | `Box` | `Box(corner_lo, corner_hi)` | axis-aligned block; a zero-thickness `Box` is a sheet | | `Sphere` | `Sphere(center, radius)` | | | `Cylinder` | `Cylinder(center, radius, height, axis="z")` | `center` is the middle of the cylinder, not its base | | `PolylineWire` | `PolylineWire(points, radius)` | a wire through the given vertices; `radius=0` selects the legacy filament, while a positive PEC radius must resolve as a volume wire | | `Via` | `Via(*, center, drill_radius, pad_radius, layers, material="pec")` | a PCB via with pads; `center` is `(x, y)` and `layers` is a list of `(z_bottom, z_top)` | | `CurvedPatch` | `CurvedPatch(*, center, length, width, radius, axis="x")` | a patch bent along `axis`, built from flat segments | | `MeshShape` | `MeshShape.from_file(path, *, scale, translate=(0.0, 0.0, 0.0))` | CAD import, see below | `sim.fidelity_report()` prints where every shape actually landed next to what you drew. Run it before a long solve. ### CAD import `MeshShape.from_file` reads a watertight STL, OBJ or PLY mesh, or STEP/STP with the `cad` extra installed (`pip install "rfx-fdtd[cad]"`). `scale` is required: STL, OBJ and PLY have no units, so pass `1e-3` for a file drawn in millimetres. STEP files are converted to metres on load, so pass `1.0`. The mesh is rasterized on the host, so a CAD shape cannot carry a gradient. ## Metal sheets ```text Simulation.add_thin_conductor(shape, *, sigma_bulk=5.8e7, thickness=35e-6, eps_r=1.0, surface_impedance_f0=None) -> Simulation ``` Declares copper foil (ground plane, patch, trace) as a **sheet**: one grid plane, zero thickness, no cell of its own. Give the shape zero thickness along its normal. ```python sim.add_thin_conductor(Box((0.005, 0.005, 0.004), (0.035, 0.025, 0.004))) sim.add_thin_conductor(Box((0.012, 0.010, 0.0056), (0.028, 0.020, 0.0056)), surface_impedance_f0=3e9) ``` - With the default `sigma_bulk` (copper) the sheet is **lossless PEC**, and rfx warns once to say so. `thickness` and `eps_r` are then not used. - `surface_impedance_f0=f0` adds conductor loss as a surface resistance matched at `f0`. The sheet keeps exactly the same footprint, so `f0` changes the loss and never the geometry. Combinations the loss model does not support raise. - `sigma_bulk < 1e6` makes a resistive sheet whose sheet conductance is `sigma_bulk * thickness`. A metal plate or wall that really is thick is a **volume**: `add(Box(...), material="pec")`. A PEC volume has walls at both drawn faces at any thickness. The removed `two_plane=True` keyword of `add()` raises `TypeError`. Delete it: a PEC volume already gets both faces. ## Limits - Metal with `sigma >= 1e6` S/m is lossless. For ohmic loss, use a sheet with `surface_impedance_f0` or a below-threshold `sigma`. - A shape face between two grid planes snaps to the nearest plane. For thin layers, choose `dx` or a graded profile so the faces fall on grid planes, and check with `fidelity_report()`. - A sheet declared through `add_thin_conductor` must be one grid plane thick. A PEC shape that is thinner than a cell but not zero is ambiguous; see [How conductors land on the lattice](https://remilab.ai/rfx/model/materials/#how-conductors-land-on-the-lattice). - `MeshShape` geometry is not differentiable. --- # API Reference Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/api/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/api/index.mdx These pages describe the rfx API you should build on: the `Simulation` class, its `add_*`, `run()` and calculator methods, and the result objects they return. Each entry gives the signature, what it does, the key arguments and what it returns. For every public symbol, including lower-level helpers, see the [generated reference](https://remilab.ai/rfx/api/generated/). ## Pages | Page | Covers | |---|---| | [Simulation](https://remilab.ai/rfx/api/simulation/) | the constructor, preflight, `run()`, `forward()`, `Simulation.auto()` | | [Results & Observables](https://remilab.ai/rfx/api/results/) | probes, flux monitors, DFT planes, NTFF, snapshots; `Result`, `ForwardResult` and the S-parameter result objects | | [Geometry & Materials](https://remilab.ai/rfx/api/geometry-materials/) | `add_material`, dispersive poles, shapes, CAD import, metal sheets | | [Sources & Ports](https://remilab.ai/rfx/api/sources-ports/) | waveforms, sources, the four port families, lumped R/L/C, the S-parameter calculators | | [Automation](https://remilab.ai/rfx/api/functions/) | `plan_mesh`, `auto_configure`, graded-profile builders | | [Support Boundaries](https://remilab.ai/rfx/support/) | what is supported, limited, experimental or refused | ## Conventions - Import from the top-level package: `from rfx import Simulation, Box, GaussianPulse`. - Units are SI: lengths in metres, frequencies in hertz, time in seconds, conductivity in S/m, impedance in ohms. - Positions are physical coordinates measured from the domain's lower corner, which is the inner edge of the absorber, not grid indices. - `add_*` methods return the simulation, so they can be chained. - S-parameter matrices are indexed `S[receiver_port, driven_port, freq_index]`. - A combination rfx does not implement raises an error. It is not silently skipped. ## Minimal example ```python from rfx import Box, GaussianPulse, Simulation sim = Simulation(freq_max=5e9, domain=(0.04, 0.03, 0.03), dx=2e-3, boundary="cpml", cpml_layers=6) sim.add_material("slab", eps_r=3.0, sigma=0.01) sim.add(Box((0.018, 0.0, 0.0), (0.024, 0.03, 0.03)), material="slab") sim.add_source((0.010, 0.015, 0.015), "ez", waveform=GaussianPulse(f0=3e9, bandwidth=0.8), amplitude_kind="current") sim.add_probe((0.030, 0.015, 0.015), "ez") result = sim.run(n_steps=300) print(result.time_series.shape, f"{result.settling_db:.1f} dB") ``` [Quick Start](https://remilab.ai/rfx/start/first-simulation/) walks through the same steps, and [Core Concepts](https://remilab.ai/rfx/start/concepts/) explains the choices behind them. --- # Documentation for Agents and Scripts Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/api/machine-readable/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/api/machine-readable.mdx Start with [llms.txt](https://remilab.ai/rfx/llms.txt). It links the same manuals and examples served on this site, as plain Markdown without navigation markup. Each Markdown page identifies its documentation channel, package version, source commit and human-readable URL. For a command-line reader: ```bash curl --fail --location https://remilab.ai/rfx/llms.txt ``` Check the HTTP status before treating a response as documentation. The public edge can return an HTTP 403 response containing Cloudflare error 1010 to some automated client user agents; an HTML error response is not an empty API reference. The curl command above and Python `requests` were checked against these public endpoints. | Resource | Contents | |---|---| | [Documentation index](https://remilab.ai/rfx/llms.txt) | Task-oriented links to the public pages and API | | [Combined manual](https://remilab.ai/rfx/llms-full.txt) | Public page text and code in one document | | [Typed API inventory](https://remilab.ai/rfx/api/inventory.json) | Importable exports and public `Simulation` methods, generated from Python | | [Support contract](https://remilab.ai/rfx/api/support/support_matrix.json) | The source repository's unchanged configuration support data | | [S-parameter contract](https://remilab.ai/rfx/api/support/sparameter_support_matrix.json) | Port families, calculators, result conventions and restrictions | | [Build manifest](https://remilab.ai/rfx/docs-manifest.json) | Source identity, page URLs, source files and artifact hashes | | [Deep reference](https://remilab.ai/rfx/api/generated/rfx.html) | Rendered signatures and full docstrings | ## Read the constraints with the signature A signature tells you what can be called. The support contracts and [support boundaries](https://remilab.ai/rfx/support/) tell you which combinations have documented evidence. A successful preflight or a completed run does not establish accuracy. Historical gallery artifacts retain their original provenance; the documentation build commit is not a new simulation run. The inventory preserves annotations and docstrings as written. It does not invent missing units, shape constraints, support levels or validation claims. Use the parameter descriptions and result conventions alongside the signature, then follow the commit-pinned source link when more detail is needed. A useful request to a coding agent is: > Read this version's llms.txt, API inventory and support contracts. Select the > matching example and measurement API, state its units and configuration limits, > and cite the source commit. Do not turn a field probe into a port S-parameter > calculator or claim that an optimization movie verifies a gradient. ## Match the installed version The documentation channel and source SHA appear on each page. Development docs may describe changes after the installed release. Use the corresponding release snapshot when available and compare its package version with `rfx.__version__`. The JSON `schema_version` identifies the file format; it is separate from the simulator's `package_version`. --- # Results and Observables Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/api/results/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/api/results.mdx This page lists what you can record, where it appears after the run, and the result objects of the S-parameter calculators. For worked examples see [Probes and S-Parameters](https://remilab.ai/rfx/measure/s-parameters/), [Far-Field and RCS](https://remilab.ai/rfx/measure/far-field-and-rcs/) and [Visualization and Analysis](https://remilab.ai/rfx/measure/plots-and-export/). ## Observables | Method | Records | Appears in | |---|---|---| | `add_probe(position, component="ez")` | one field component at one point, every step | `Result.time_series[:, i]` | | `add_vector_probe(position)` | all six components at one point | six more columns of `time_series` | | `add_dft_plane_probe(*, axis, coordinate, component="ez", freqs=None, n_freqs=50, name=None)` | one component on a whole plane, at chosen frequencies | `Result.dft_planes[name]` | | `add_flux_monitor(*, axis, coordinate, freqs=None, n_freqs=50, size=None, center=None, name=None, dft_window="rect", dft_window_alpha=0.25)` | power through a plane (or a `size` rectangle of it) | `Result.flux_monitors[name]` | | `add_ntff_box(corner_lo, corner_hi, freqs=None, n_freqs=50)` | tangential fields on a closed box, for far fields | `Result.ntff_data`, `Result.ntff_box` | | `run(snapshot=SnapshotSpec(...))` | field frames during the run | `Result.snapshots`, `Result.snapshot_axes` | Probe columns follow registration order. Frequencies are in Hz. Positions are in metres and snap to the nearest grid location. ```python import numpy as np from rfx import Box, GaussianPulse, Simulation, SnapshotSpec, flux_spectrum sim = Simulation(freq_max=6e9, domain=(0.04, 0.03, 0.03), dx=1e-3, boundary="cpml", cpml_layers=8) sim.add_material("slab", eps_r=3.0, sigma=0.01) sim.add(Box((0.022, 0.0, 0.0), (0.028, 0.03, 0.03)), material="slab") sim.add_source((0.012, 0.015, 0.015), "ez", waveform=GaussianPulse(f0=3e9, bandwidth=0.8), amplitude_kind="current") freqs = np.linspace(1e9, 5e9, 9) sim.add_probe((0.016, 0.015, 0.015), "ez") # column 0 sim.add_flux_monitor(axis="x", coordinate=0.034, freqs=freqs, name="trans") sim.add_dft_plane_probe(axis="z", coordinate=0.015, component="ez", freqs=freqs, name="mid") result = sim.run( n_steps=500, snapshot=SnapshotSpec(interval=50, components=("ez",), slice_axis=2, slice_index=20), ) print(result.time_series.shape) # (n_steps, n_probes) power = np.asarray(flux_spectrum(result.flux_monitors["trans"])) print(power.shape) # one value per frequency axes = result.snapshot_axes["ez"] print(result.snapshots["ez"].shape, axes.dims, axes.times_s[:2]) ``` - `flux_spectrum(monitor)` gives the net power through the plane at each frequency. For transmission, divide by the same monitor in a reference run without the structure; `subtract_flux_monitors(sample, ref)` removes the incident field at field level, so `flux_spectrum` of the result is the scattered power. - A snapshot's `slice_index` counts grid cells including the absorber pad. Read positions and times from `result.snapshot_axes[component]` (`coords`, `times_s`, `steps`) rather than working them out. Frames are `interval` steps apart. - Far fields: `compute_far_field(result.ntff_data, result.ntff_box, result.grid, theta, phi)` returns a `FarFieldResult`; `directivity(ff)` and `radiation_pattern(ff)` read it. RCS uses `compute_rcs(...)`. Both are covered in [Far-Field and RCS](https://remilab.ai/rfx/measure/far-field-and-rcs/). ## `Result` `run()` returns a `Result` (a named tuple). Point and vector probe samples use SI field units: `ex`, `ey`, `ez` are in V/m; `hx`, `hy`, `hz` are in A/m. `time_series` keeps those raw component values, without division by the source amplitude or a field maximum. Field snapshots use the same units. A gallery plot labelled `Ez / Eref` is a separately normalized visualization, not the stored probe value. | Field | Contents | |---|---| | `time_series` | probe records, shape `(n_steps, n_columns)` | | `dt` | time step in seconds | | `settling_db` | worst share, over the frequencies the run's spectral results read, of the value that still rings past the record's end, in dB, from poles fitted after the source ends. At or below −40 dB passes. `nan` or `None` when it could not be judged; read the status. | | `settling_witness` | `status` (`pass`, `fail`, `undetermined`, `absent`; only `pass` is a pass), `reason`, and the per-frequency shares behind `settling_db` | | `s_params`, `freqs` | lumped/wire port S-matrix `[receiver, driven, freq]` and its frequencies (Hz) | | `wire_port_sparams` | wire-port details | | `ringdown` | ring-down completion, when `run(ringdown=...)` was used | | `dft_planes` | `{name: DFTPlaneProbe}` | | `flux_monitors` | `{name: FluxMonitor}` | | `ntff_data`, `ntff_box` | far-field accumulation and its box | | `waveguide_ports`, `waveguide_sparams`, `waveguide_port_flux` | per-port waveguide data from `run()`; for the full matrix use `compute_waveguide_s_matrix()` | | `snapshots`, `snapshot_axes` | `{component: frames}` and their coordinates and times | | `grid` | the grid the run used | | `freq_range` | the analysis range used by `find_resonances()` | | `state` | final field state, for inspection or `save_state` | Helpers on `Result`: | Method | Returns | |---|---| | `find_resonances(freq_range=None, probe_idx=0, source_decay_time=None, bandpass=None, waveform=None)` | list of `HarminvMode(freq, decay, Q, amplitude, phase, error)` from one probe | | `s(m, n)`, `s11()`, `s21()`, `s12()`, `s22()` | complex S-parameter vector, ports numbered from 1 | | `s_db(m, n)` | `20 log10 |S_mn|` | | `freqs_hz` | the S-parameter frequency vector | | `assert_finite(*, raise_on_nonfinite=False)` | `True` when the observables hold no NaN/Inf | | `plot_time_series()`, `plot_s_params()`, `plot_smith()` | quick matplotlib plots | ### Resonances `find_resonances()` runs Harminv on the part of a probe record after the source has died away. It suits resonators better than S-parameters because it does not depend on port calibration. Record a long ring-down, and give the structure some loss, or Q has no finite value to converge to. ```python cav = Simulation(freq_max=12e9, domain=(0.030, 0.020, 0.010), dx=1e-3, boundary="pec") cav.add_material("lossy_air", eps_r=1.0, sigma=1e-3) cav.add(Box((0.0, 0.0, 0.0), (0.030, 0.020, 0.010)), material="lossy_air") cav.add_source((0.007, 0.006, 0.005), "ez", waveform=GaussianPulse(f0=9e9, bandwidth=0.8), amplitude_kind="current") cav.add_probe((0.021, 0.013, 0.005), "ez") ring = cav.run(n_steps=3000) for mode in ring.find_resonances(freq_range=(6e9, 11e9)): print(f"{mode.freq / 1e9:.3f} GHz, Q = {mode.Q:.0f}") c0 = 299_792_458.0 print(f"analytic TM110: {c0 / 2 * np.hypot(1 / 0.030, 1 / 0.020) / 1e9:.3f} GHz") ``` For a lower-level call on any signal, use `harminv(signal, dt, f_min, f_max, ...)`. ## `ForwardResult` `forward()` returns a smaller named tuple: `time_series`, `s_params` and `freqs` (lumped/wire S11 vectors when `port_s11_freqs=` is set), `lumped_port_sparams`, `wire_port_sparams`, `dft_planes`, `ntff_data`, `ntff_box`, `grid`, `dt` and `settling_probe_info`. Everything in it can carry a gradient. ## S-parameter results All full matrices are indexed `S[receiver_port, driven_port, freq_index]`. | Calculator | Result | Main fields | |---|---|---| | `run(compute_s_params=True)` | `Result` | `s_params`, `freqs` | | `compute_msl_s_matrix()` | `MSLSMatrixResult` | `S`, `freqs`, `Z0`, `beta`, `port_names`, `reliable`, `settling_db`, `reference_impedances`, `sigma_max_excess`, `S_raw`, `passivity_correction` | | `compute_waveguide_s_matrix()` | `WaveguideSMatrixResult` | `s_params`, `freqs`, `port_names`, `port_directions`, `reference_planes`, `settling_db`, `s21_phase_residual_deg_rms` | | `compute_coaxial_line_reflection()` | `CoaxialLineReflectionResult` | `s11`, `freqs`, `status`, `annulus_cells`, `gamma`, `recurrence_residual`, `fit_residual`, `z0_numerical_ohm`, `termination` | | `compute_coaxial_two_port()` | `CoaxialTwoPortResult` | `s_params`, `freqs`, `port_names`, `reference_planes`, `status`, `settling_db`, `cond_a`, `gamma`, `recurrence_residual`, `fit_residual` | **Microstrip.** `S` is the raw extraction, the same function a gradient differentiates. A raw S can exceed passivity. `sigma_max_excess[k]` gives how far the largest singular value exceeds 1 at bin k, and a warning names the bins. That is a measurement artifact, usually a record that ended too early or a mesh that is too coarse. `enforce_passivity=True` projects S onto the passive set instead; the unprojected matrix stays in `S_raw` and the correction in `passivity_correction`. Don't quote projected values where the correction is large. `reliable[p, k]` marks low-signal bins: where any entry is `False`, drop the whole slice `S[:, :, k]`, because S is solved jointly across drives. `reference_impedances` are the power-wave references; `Z0` is the fitted line impedance, reported separately. **Waveguide.** S is a ratio of modal voltage waves, each referenced to its own port's discrete modal impedance. It equals a power-wave S only when all ports share one cross-section. `s21_phase_residual_deg_rms` is the RMS difference between the S21 phase and `−βL` for a single-mode two-port. On an empty or matched guide it measures the port's discretization error. With a device between the planes, it includes the device's own phase. **Coax.** Check `status == "passed"` before using `s11`. This result does not go through the shared passivity check. ## Checking a result Some checks run automatically and warn without changing the arrays: - `run()` and the uniform `forward()` check `time_series` and lumped/wire `s_params` for NaN/Inf and for any `|S| > 1.1`. They do not test the total power in a column. - The microstrip and waveguide calculators check column power and each entry. - Every calculator warns when its record has not rung down to −40 dB. Nothing checks DFT planes, flux, NTFF data or the state. Use `np.isfinite(...)` on them yourself. A missing warning is not a passivity or convergence result. For your own regression checks, `validate_port_smatrix(obj, *, check_passivity=True, passivity_tol=1e-9, check_reciprocity=False, ...)` returns a `PortValidationReport` for any S-matrix result, and `assert_port_smatrix_valid(...)` raises instead. ```python from rfx import validate_port_smatrix fake = np.zeros((2, 2, 3), dtype=complex) fake[1, 0] = fake[0, 1] = 0.9 report = validate_port_smatrix(s_params=fake, freqs=np.array([1e9, 2e9, 3e9]), check_reciprocity=True) print(report.summary()) ``` ## Saving results `write_touchstone(path, s_params, freqs, z0=50.0, ...)` and `read_touchstone(path)` handle `.sNp` files. `save_snapshots`, `save_field_vtk`, `save_far_field` and `save_state` write other outputs; see the [generated reference](https://remilab.ai/rfx/api/generated/). ## Limits - A spectrum from a record that stopped too early is wrong. Check `settling_db` before you trust flux, DFT planes, far fields, S-parameters or Q. - Probes, DFT planes and flux monitors have no reference impedance and cannot give S-parameters. Use a port. - `find_resonances()` on a lossless structure returns an unbounded Q. Add a little loss. - `save_snapshots(..., dt=result.dt)` without `axes=` is wrong at the default interval, because frames are `interval` steps apart. Pass `axes=result.snapshot_axes` instead. --- # Simulation Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/api/simulation/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/api/simulation.mdx `Simulation` holds one model: the domain and mesh, boundaries, materials, geometry, sources, ports and observables. You build it with `add_*` calls, check it with `preflight()`, and solve it with `run()` or, for gradients, `forward()`. This page lists the constructor and those entry points. The mental model behind them is in [Core Concepts](https://remilab.ai/rfx/start/concepts/). ## Constructor ```text Simulation(freq_max, domain, *, boundary="cpml", cpml_layers=16, cpml_kappa_max=1.0, dx=None, mode="3d", dz_profile=None, dx_profile=None, dy_profile=None, dt=None, dt_min_cell=None, precision="float32", solver="yee", adi_cfl_factor=2.0, stencil_order=2, interface_eps="sampled") ``` ```python import numpy as np from rfx import Box, GaussianPulse, Simulation sim = Simulation( freq_max=5e9, # highest frequency of interest (Hz) domain=(0.04, 0.03, 0.03), # x, y, z size (m) dx=2e-3, # cubic cell size (m) boundary="cpml", cpml_layers=6, ) ``` | Argument | Meaning | |---|---| | `freq_max` | Highest frequency you care about, in Hz. It sets the default time step, the default `dx` and the mesh checks in preflight. | | `domain` | `(Lx, Ly, Lz)` in metres. On a graded axis, pass `0` for that axis and the size comes from the profile's sum. | | `dx` | Cell size in metres. If omitted, rfx derives it from `freq_max`. | | `boundary` | `"cpml"` (default), `"pec"`, `"upml"`, or a `BoundarySpec` for per-face control, including `"pmc"` and `"periodic"`. See [Core Concepts](https://remilab.ai/rfx/start/concepts/#boundaries). | | `cpml_layers` | Absorber thickness in cells (default 16). rfx adds the absorber **outside** `domain` on each CPML face, so the whole domain is interior. Keep sources and structures a few cells from its edge. | | `dx_profile`, `dy_profile`, `dz_profile` | Cell sizes along an axis, for a graded mesh. See [Non-Uniform Mesh](https://remilab.ai/rfx/model/domain-mesh/). | | `mode` | `"3d"` (default), `"2d_tmz"` (Ez, Hx, Hy) or `"2d_tez"` (Hz, Ex, Ey). | | `precision` | `"float32"` (default), `"float64"` (enable JAX x64 first) or `"mixed"` (float16 fields). The last two run on the uniform single-device solver only. | | `solver` | `"yee"` (default) or the experimental `"adi"`. See [ADI Solver](https://remilab.ai/rfx/guide/adi-solver/). | | `stencil_order` | `2` (default) or `4`. Order 4 only runs on a uniform, PEC or periodic, non-dispersive model. | | `dt`, `dt_min_cell` | Pin the time step on a graded mesh (for mesh-as-design-variable work). Refused on a uniform mesh. | Put PEC faces in a `BoundarySpec`. Neither is a finite ground plane: for that, draw a sheet with `add_thin_conductor()`. ## Building the model Every `add_*` method returns the simulation, so calls can be chained. | Method | Purpose | Reference | |---|---|---| | `add_material(name, *, eps_r, sigma, mu_r, ...)` | register a named material | [Geometry & Materials](https://remilab.ai/rfx/api/geometry-materials/) | | `add(shape, *, material)` | fill a shape with a material | [Geometry & Materials](https://remilab.ai/rfx/api/geometry-materials/) | | `add_thin_conductor(shape, ...)` | a zero-thickness metal sheet | [Geometry & Materials](https://remilab.ai/rfx/api/geometry-materials/) | | `add_source`, `add_polarized_source`, `add_tfsf_source` | field excitation, no impedance | [Sources & Ports](https://remilab.ai/rfx/api/sources-ports/) | | `add_port`, `add_msl_port`, `add_waveguide_port`, `add_coaxial_port` | ports for S-parameters | [Sources & Ports](https://remilab.ai/rfx/api/sources-ports/) | | `add_lumped_rlc` | an R/L/C element in one cell | [Sources & Ports](https://remilab.ai/rfx/api/sources-ports/) | | `add_probe`, `add_vector_probe`, `add_dft_plane_probe`, `add_flux_monitor`, `add_ntff_box` | observables | [Results & Observables](https://remilab.ai/rfx/api/results/) | | `add_refinement(z_range, *, ratio=4, ...)` | a finer z slab (subgridding) | experimental; only `run()` on a uniform mesh accepts it | ```python sim.add_material("slab", eps_r=3.0, sigma=0.01) sim.add(Box((0.018, 0.0, 0.0), (0.024, 0.03, 0.03)), material="slab") sim.add_source((0.010, 0.015, 0.015), "ez", waveform=GaussianPulse(f0=3e9, bandwidth=0.8), amplitude_kind="current") sim.add_probe((0.030, 0.015, 0.015), "ez") ``` ## Checking before you run | Method | Returns | Use | |---|---|---| | `preflight(*, strict=False, check_ntff=True, check_resolution=True, ...)` | `PreflightReport` | Setup errors and warnings. `run()` calls it for you. | | `preflight_sparameters(*, calculator="run", strict=False, ...)` | `PreflightReport` | Is this port family valid for the S-parameter calculator you plan to call? See [Sources & Ports](https://remilab.ai/rfx/api/sources-ports/#checking-a-calculator-before-the-run). | | `fidelity_report(print_report=True)` | report | Where each shape actually landed on the grid, next to what you drew. | | `freeze_mesh()` | `Grid` or `NonUniformGrid` | Fix the mesh now, so you can read grid indices before placing more geometry. | | `plan_mesh(*, n_steps=None, available_memory_gb=None, sparameter_calculator=None, ...)` | `MeshPlan` | A serializable summary of the mesh, memory estimate and checks. See [Automation](https://remilab.ai/rfx/api/functions/). | | `mesh_intelligence_report(...)` | `MeshIntelligenceReport` | Grid, preflight issues and AD memory in one report. | ```python report = sim.preflight() print(report.ok, len(report.errors), len(report.warnings)) report.raise_for_failure() # raises only on errors sim.fidelity_report() ``` `PreflightReport` is a list of coded issues. Read `.ok`, `.errors`, `.warnings`, `.infos` or `.by_code(code)`. Don't test it with `if report:`: that raises `TypeError`, because an advisory-only report is non-empty but fine. `strict=True` raises on errors instead of returning them. ## `run()` `run()` is the general entry point. It returns a `Result`; see [Results & Observables](https://remilab.ai/rfx/api/results/). ```text run(*, n_steps=None, num_periods=20.0, until_decay=None, decay_check_interval=50, decay_min_steps=100, decay_max_steps=50000, decay_energy_consecutive=2, ..., checkpoint=False, compute_s_params=None, s_param_freqs=None, s_param_n_steps=None, snapshot=None, subpixel_smoothing=False, conformal_pec=None, conformal_min_weight=0.1, devices=None, exchange_interval=1, skip_preflight=False, report_every=None, report_label="", ringdown=None) -> Result ``` ```python result = sim.run(n_steps=300) print(result.time_series.shape, f"settling {result.settling_db:.1f} dB") ``` **Run length.** Give exactly one of: | Argument | Meaning | |---|---| | `n_steps=N` | exactly N steps | | `num_periods=P` | P periods at `freq_max` (default 20, used when nothing else is given) | | `until_decay=1e-3` | step until the interior field energy falls to that fraction of its peak, up to `decay_max_steps` | `until_decay` works on absorbing boundaries, on uniform and graded meshes. It watches the total energy in the interior, so it is a sound stop for spectra, flux and S-parameters. In a closed PEC box the energy does not decay. There the uniform solver falls back to a single field sample, which is reliable only for lossy or radiating ring-down, and a graded mesh runs the fixed `n_steps` with a warning. Use a fixed `n_steps` in closed structures and in convergence studies. **Other arguments you will use:** | Argument | Meaning | |---|---| | `compute_s_params=True`, `s_param_freqs=` | S-parameters for lumped and wire ports (`add_port`) only. Other port families have their own calculator. | | `snapshot=SnapshotSpec(interval=10, components=("ez",), slice_axis=None, slice_index=None)` | Record field snapshots. See [Visualization and Analysis](https://remilab.ai/rfx/measure/plots-and-export/). | | `report_every=N` | Print progress (steps, rate, ETA) every N steps. Does not change the result. | | `subpixel_smoothing=True` | Smoothed permittivity at dielectric interfaces. | | `conformal_pec=True` | Dey–Mittra conformal PEC for curved metal. | | `ringdown=RingdownSpec(...)` | Complete a wire port's S-parameters when the record was cut while the structure still rings. See below. | | `devices=[...]` | Run across several devices (x-slab decomposition). `exchange_interval` must stay 1. | | `skip_preflight=True` | Skip the automatic preflight. Use only after you have read it once. | A run argument that the selected solver path does not implement **raises before the first step**, for example snapshots on a graded mesh or `until_decay` with the ADI solver. Nothing is silently dropped. **Warnings after the run.** `run()` warns when a probe recorded no field, when a Gaussian-pulse probe record had not rung down (end of record above −40 dB of peak), and when `time_series` or lumped/wire `s_params` contain NaN/Inf or an `|S|` above 1.1. These checks cover only those arrays. For DFT planes, flux and NTFF data, check `np.isfinite(...)` yourself. No warning does not mean the run converged. ### Ring-down completion `from rfx.ringdown import RingdownSpec`, then `run(..., ringdown=RingdownSpec())`. For a wire-port run (`add_port(..., extent=...)`) whose record ended too early, rfx fits the slowly decaying poles in the last part of the record and adds the missing tail of each spectrum in closed form. The completed matrix is `result.ringdown.s_params`, on the same frequencies as `result.s_params`, and `result.ringdown.report` holds the fitted poles and the checks. `report.ok` is decided by the error witness (completing from `[T/4, T]` versus `[T/2, T]`). The two-window difference `W2` is reported, not judged. None of the witnesses is an error bound. The source must be off in the fit window. It covers wire ports (one port on a uniform mesh, or a graded mesh) with a fixed `n_steps`. Other ports, TFSF, `until_decay` and `devices=` are refused with the reason. `s_params` is `None` when the report says no completion was produced. ## `forward()`: the differentiable path `forward()` runs the same physics with only the outputs a loss function needs, so it can sit inside `jax.grad`, `jax.value_and_grad` or `jax.jit`. It returns a `ForwardResult` with `time_series`, `s_params`/`freqs` (lumped/wire S11 vectors when `port_s11_freqs=` is set), `ntff_data`, `dft_planes`, `grid` and `dt`. ```text forward(*, eps_override=None, sigma_override=None, mu_r_override=None, design_box=None, design_eps_override=None, design_sigma_override=None, design_occupancy_override=None, pec_mask_override=None, pec_occupancy_override=None, n_steps=None, num_periods=20.0, checkpoint=True, checkpoint_segments=None, emit_time_series=True, checkpoint_every=None, n_warmup=0, skip_preflight=False, distributed=False, devices=None, exchange_interval=1, port_s11_freqs=None, rlc_values_override=None) -> ForwardResult ``` ```python import jax import jax.numpy as jnp grid = sim.freeze_mesh() i0 = grid.position_to_index((0.018, 0.0, 0.0))[0] i1 = grid.position_to_index((0.024, 0.0, 0.0))[0] def loss(eps_slab): eps = jnp.ones(grid.shape).at[i0:i1].set(eps_slab) out = sim.forward(eps_override=eps, n_steps=300, skip_preflight=True) return jnp.sum(out.time_series[:, 0] ** 2) print(jax.grad(loss)(3.0)) ``` | Input | What it differentiates | |---|---| | `eps_override`, `sigma_override`, `mu_r_override` | whole-grid material arrays, shape `grid.shape`; they replace the registered materials everywhere | | `design_box=` with `design_eps_override=` / `design_sigma_override=` / `design_occupancy_override=` | a box-sized design region; far less reverse-mode memory than a whole-grid override (pass `checkpoint=False` with it) | | `pec_occupancy_override` | relaxed metal occupancy in [0, 1] | | `port_s11_freqs=` | lumped/wire S11 vectors, uniform single-device only | | `rlc_values_override={index: {"R": ..., "L": ..., "C": ...}}` | values of registered `add_lumped_rlc` elements | **Per-cell versus per-edge conductivity in a design box.** A single `design_sigma_override` array is per **cell**. rfx averages the four cells around each E edge, and that average also reaches the edge layer on the box's plus faces. A 3-tuple `(sigma_x, sigma_y, sigma_z)` is per **edge** at the box indices and is written as-is. If a per-edge design needs that plus-face layer, declare the box one cell larger on that side. Keep anything that changes array shapes, the geometry list or the solver path outside the function you differentiate. A combination the differentiable path does not support raises instead of returning a zero gradient. A model with `add_refinement` is refused by `forward()`. Guides: [Autodiff and Adjoint](https://remilab.ai/rfx/design/gradients/), [Inverse Design](https://remilab.ai/rfx/design/optimization/), [Memory Reduction](https://remilab.ai/rfx/run/memory-and-speed/). ## S-parameter calculators Each port family has its own calculator method on `Simulation`: `compute_msl_s_matrix()`, `compute_waveguide_s_matrix()`, `compute_coaxial_line_reflection()` and `compute_coaxial_two_port()`. Lumped and wire ports use `run(compute_s_params=True)`. `compute_s_matrix(lane=None, **kwargs)` picks the right method from the registered ports and forwards your arguments. `s_matrix_lane()` tells you which one it would pick without running anything. Details: [Sources & Ports](https://remilab.ai/rfx/api/sources-ports/#s-parameter-calculators). ## `Simulation.auto()` ```text Simulation.auto(freq_range, *, accuracy="standard", **kwargs) -> Simulation ``` ```python auto_sim = Simulation.auto(freq_range=(1.5e9, 3.5e9), accuracy="draft") ``` This derives `dx`, the domain and the CPML thickness from the frequency range alone. `accuracy` is `"draft"`, `"standard"` or `"high"`. Any keyword argument overrides the derived value. It warns on purpose: the domain was sized without your geometry, so check it after you add shapes. To derive a mesh from your geometry instead, use `auto_configure` or `plan_mesh` on the [Automation](https://remilab.ai/rfx/api/functions/) page. ## Other methods These are documented in the [generated reference](https://remilab.ai/rfx/api/generated/): `estimate_ad_memory`, `plan_ad_memory`, `ad_memory_preflight` and `explain_ad_memory` (reverse-mode memory, see [Memory Reduction](https://remilab.ai/rfx/run/memory-and-speed/)); `export_scene`, `export_artifact_bundle` and `artifact_report` (saving a model and its results); `conductor_mask`. ## Limits - A graded mesh accepts fewer features than a uniform one. Snapshots, lumped port S-parameters, coaxial ports, UPML and Floquet ports raise there. See [Support Boundaries](https://remilab.ai/rfx/support/). - `add_refinement` works only through `run()` on a uniform mesh. `forward()`, `optimize()` and `topology_optimize()` refuse a refined model. - `precision="mixed"` raises the CPML reflection floor. Don't use it for S-parameters or low-reflection work. - A completed run is not a converged run. Check `result.settling_db` and do a [convergence study](https://remilab.ai/rfx/evidence/mesh-convergence/) before you quote a number. --- # Sources and Ports Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/api/sources-ports/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/api/sources-ports.mdx This page lists the excitation calls, the port families and the S-parameter calculator for each family. A **source** injects a field and has no impedance. A **port** also terminates the line in a known impedance, so rfx can compute S-parameters. Worked examples are in the [Sources and Ports guide](https://remilab.ai/rfx/model/ports/) and [Probes and S-Parameters](https://remilab.ai/rfx/measure/s-parameters/). ## Waveforms `add_source()`, `add_polarized_source()`, `add_port()` and `add_msl_port()` take a `waveform=` object. Waveguide, TFSF and Floquet sources take their own pulse arguments instead. | Waveform | Signature | Use | |---|---|---| | `GaussianPulse` | `GaussianPulse(f0, bandwidth=0.5, amplitude=1.0, cutoff=3.0)` | default wideband pulse (differentiated Gaussian, almost no DC) | | `ModulatedGaussian` | `ModulatedGaussian(f0, bandwidth=0.5, amplitude=1.0, cutoff=5.0)` | band-pass pulse centred on `f0` | | `CWSource` | `CWSource(f0, amplitude=1.0, ramp_steps=50)` | single frequency with a ramped start, for steady-state fields | | `CustomWaveform` | `CustomWaveform(func)` | your own `func(t) -> amplitude`, with `t` in seconds | `bandwidth` is a fraction of `f0`. If you omit `waveform`, rfx uses a `GaussianPulse` at `freq_max / 2`. ## Sources ```python import numpy as np from rfx import GaussianPulse, Simulation sim = Simulation(freq_max=6e9, domain=(0.03, 0.03, 0.03), dx=1e-3, boundary="cpml", cpml_layers=8) sim.add_source((0.010, 0.015, 0.015), "ez", waveform=GaussianPulse(f0=3e9, bandwidth=0.8), amplitude_kind="current") sim.add_polarized_source((0.010, 0.020, 0.015), polarization="slant45", amplitude_kind="current") ``` | Method | Signature | Notes | |---|---|---| | `add_source` | `add_source(position, component="ez", *, waveform=None, amplitude_kind=None)` | soft point source in one cell; no load, so it suits resonance and Harminv work | | `add_polarized_source` | `add_polarized_source(position, *, polarization="ez", waveform=None, amplitude_kind=None)` | linear polarization: `"ex"`, `"ey"`, `"ez"`, `"slant45"` or a real `(Ex, Ey)` tuple | | `add_tfsf_source` | `add_tfsf_source(*, f0=None, bandwidth=0.5, amplitude=1.0, margin=3, polarization="ez", direction="+x", angle_deg=0.0, waveform="differentiated_gaussian", method="bloch")` | plane wave travelling along x, for scattering; 3-D or 2-D (`mode="2d_tmz"`/`"2d_tez"`), with CPML | | `add_floquet_port` | see the [generated reference](https://remilab.ai/rfx/api/generated/) | experimental; no documented S-parameter API | **`amplitude_kind`.** The default is `"current"`: the waveform amplitude is a current moment in A·m, injected as `E += Cb * I / dV` on Yee paths. `"field"` adds the waveform directly to E each step. The signature sentinel `None` also resolves to `"current"` and warns once per simulation; pass the kind explicitly to silence that warning. ADI requires explicit `"field"`. Witness: `tests/unit/sources/test_source_amplitude_kind.py`. `add_polarized_source` also accepts `"rhcp"`, `"lhcp"` and complex tuples, but circular polarization from this source has not been verified. Don't use it to report axial ratio or sense. For the plane wave on a graded mesh, only normal incidence along `+x` or `-x` is valid, and only when x itself is uniform. A graded x axis still runs, with a preflight warning. Far-field and RCS use is covered in [Far-Field and RCS](https://remilab.ai/rfx/measure/far-field-and-rcs/). ## Ports | Method | Port family | Calculator | |---|---|---| | `add_port(...)` | lumped (one cell) or wire (`extent=`) | `run(compute_s_params=True)`, or `forward(port_s11_freqs=...)` for S11 gradients | | `add_msl_port(...)` | microstrip line | `compute_msl_s_matrix()` | | `add_waveguide_port(...)` | rectangular waveguide | `compute_waveguide_s_matrix()` | | `add_coaxial_port(...)` | coaxial line | `compute_coaxial_line_reflection()` (one-port), `compute_coaxial_two_port()` | ### Lumped and wire ports ```text add_port(position, component="ez", *, impedance=50.0, waveform=None, extent=None, radius=None, excite=True, direction=None, reference_plane_cells=None, terminates=None) -> Simulation ``` - `impedance`: the port's reference impedance in ohms. - `extent`: omit for a one-cell lumped port. Set it (in metres) to make a wire port spanning `extent` from `position` along the component axis. - `radius`: the physical probe-pin radius for a wire port, subject to the local thin-probe mesh restrictions. With `radius=None`, the effective pin radius follows the mesh, so refining changes its series inductance. - `excite=False`: a matched load only, no source. Use it for ports that only receive. - `direction`: outward normal of the port (`"+x"`, `"-x"`, `"+y"`, `"-y"`), used to split V and I into incoming and outgoing waves. Detected from the nearest face if omitted. - `terminates`: the conductor(s) this port ends, given as the shape or its index in the geometry list. A line should end at its ports, not in the absorber. - `reference_plane_cells=N`: opt-in reference planes for a wire port's off-diagonal terms. See the [generated reference](https://remilab.ai/rfx/api/generated/). ```python lp = Simulation(freq_max=6e9, domain=(0.03, 0.03, 0.03), dx=1e-3, boundary="cpml", cpml_layers=8) lp.add_port((0.015, 0.015, 0.015), "ez", impedance=50.0, waveform=GaussianPulse(f0=3e9, bandwidth=0.8)) lp.add_lumped_rlc((0.015, 0.015, 0.016), "ez", R=50.0, C=1e-12, topology="series") print(lp.s_matrix_lane()) # run(compute_s_params=True) print(lp.preflight_sparameters(calculator="run").ok) result = lp.run(n_steps=600, compute_s_params=True, s_param_freqs=np.linspace(1e9, 5e9, 9)) print(result.s_params.shape) # (n_ports, n_ports, n_freqs) ``` `result.s_params[i, j, k]` is the response at port `i` with port `j` driven, at frequency `result.freqs[k]`. `result.s11()` and `result.s_db(2, 1)` are shortcuts. The mesh here is coarse for speed. For a real one-port S11, use about 60 cells per wavelength at the highest frequency of interest. ### Lumped R, L, C elements ```text add_lumped_rlc(position, component="ez", *, R=0.0, L=0.0, C=0.0, topology="series") -> Simulation ``` Puts an R, L and/or C in one cell, updated with the fields. `topology` is `"series"` or `"parallel"`. Set unused values to 0. A series element with only one non-zero value is folded into the cell's material, so give at least two to get the series circuit. `add_lumped_rlc` is **not** a port. It embeds a circuit element in the FDTD update, but it is not a reflection-referenced port: it fixes no reference impedance, so it gives no Z0-referenced S11. Read on its own, a one-cell element reports only its near-field self-interaction. To measure what a load does to a line, drive with `add_port(..., impedance=Z0)` and place the `add_lumped_rlc` element **a cell or more away from the port cell**, as in the example above. An element on the port cell sits in parallel with the source, and S11 does not see it. For gradients with respect to R, L or C, pass `forward(rlc_values_override={index: {"R": R, "L": L, "C": C}})` on the uniform single-device path. That changes the element; it still does not make it a port. ### Microstrip ports ```text add_msl_port(position, *, width, height, direction="+x", impedance=50.0, waveform=None, excite=True, n_probe_offset=None, n_probe_spacing=None, n_probes=5, name=None, mode="laplace", eps_r_sub=None, terminates=None) -> Simulation ``` The port covers the full trace cross-section and fits the line's β and Z0 from a short probe array downstream of the feed. - `position = (feed, centre, z_substrate_bottom)`: the coordinate along `direction` is the feed plane; the other in-plane coordinate is the trace centre. - `direction`: `"+x"`, `"-x"`, `"+y"` or `"-y"`. The substrate normal is always z. - `mode`: `"laplace"` (default, quasi-TEM). `"eigenmode"` is x-only and outside the documented scope. - `n_probe_offset`, `n_probe_spacing`: in cells. When left at `None` on a graded runway or auto mesh, rfx recounts them in the cells under the port at run time. ```python from rfx import Box h, w = 0.3e-3, 0.6e-3 # 300 um substrate, 600 um trace msl = Simulation(freq_max=10e9, domain=(0.020, 0.008, 0.003), dx=100e-6, boundary="cpml", cpml_layers=8) msl.add_material("ro4350b", eps_r=3.66) msl.add(Box((0.0, 0.0, 0.0), (0.020, 0.008, h)), material="ro4350b") msl.add_thin_conductor(Box((0.0, 0.0, 0.0), (0.020, 0.008, 0.0))) # ground msl.add_thin_conductor(Box((0.0, 0.004 - w / 2, h), (0.020, 0.004 + w / 2, h))) msl.add_msl_port((0.003, 0.004, 0.0), width=w, height=h, direction="+x") msl.add_msl_port((0.017, 0.004, 0.0), width=w, height=h, direction="-x", excite=False) print(msl.s_matrix_lane()) # compute_msl_s_matrix ``` A cell of 100 µm puts 3 cells across the 300 µm substrate, which is coarse for speed: preflight warns below 4. For microstrip, the substrate thickness sets the cell size, not the wavelength. ### Waveguide ports ```text add_waveguide_port(x_position, *, x_range=None, y_range=None, z_range=None, mode=(1, 0), mode_type="TE", direction="+x", freqs=None, n_freqs=50, f0=None, bandwidth=0.5, amplitude=1.0, probe_offset=10, ref_offset=3, calibration_preset=None, reference_plane=None, probe_plane=None, name=None, n_modes=1, waveform="modulated_gaussian", mode_profile="discrete") -> Simulation ``` `x_position` is the port plane's coordinate along the axis named by `direction` (for `direction="-y"` it is a y coordinate). The two `*_range` arguments across that axis give the guide aperture. An aperture that spans the whole cross-section needs PEC on the transverse faces and an absorber on the port axis only (`boundary={"x": "cpml", "y": "pec", "z": "pec"}` for an x-directed port); on a uniform mesh `boundary="cpml"` on every face is refused. Waveguide ports need `mode="3d"`, and cannot share a simulation with lumped ports. ```python wg = Simulation(freq_max=12e9, domain=(0.10, 0.023, 0.010), dx=1e-3, boundary={"x": "cpml", "y": "pec", "z": "pec"}, cpml_layers=8) band = np.linspace(8.2e9, 12.4e9, 11) wg.add_waveguide_port(0.01, y_range=(0.0, 0.023), z_range=(0.0, 0.010), direction="+x", freqs=band, name="p1") wg.add_waveguide_port(0.09, y_range=(0.0, 0.023), z_range=(0.0, 0.010), direction="-x", freqs=band, name="p2") print(wg.s_matrix_lane()) # compute_waveguide_s_matrix ``` The [Waveguide Ports guide](https://remilab.ai/rfx/guide/waveguide-ports/) covers calibration planes and `normalize`. ### Coaxial ports ```text add_coaxial_port(position, face="top", *, pin_length=0.005, pin_radius=0.000635, outer_radius=0.002055, impedance=50.0, waveform=None, terminates=None) -> Simulation ``` The coaxial calculators build their own line, TEM source, probe planes and termination. The port supplies only the x/y centre, `face`, the two radii and the waveform. See the complete example below. ## S-parameter calculators | Calculator | Signature (key arguments) | Returns | |---|---|---| | lumped/wire | `run(..., compute_s_params=True, s_param_freqs=None, s_param_n_steps=None)` | `Result.s_params`, `Result.freqs` | | lumped/wire, differentiable | `forward(..., port_s11_freqs=freqs)` | `ForwardResult.s_params` (S11 vectors) | | microstrip | `compute_msl_s_matrix(*, n_steps=None, num_periods=40.0, freqs=None, n_freqs=100, enforce_passivity=False, eps_override=None, ...)` | `MSLSMatrixResult` | | waveguide | `compute_waveguide_s_matrix(*, n_steps=None, num_periods=20.0, normalize=False, eps_override=None, sigma_override=None, strict_passivity=False, ...)` | `WaveguideSMatrixResult` | | coax, one-port | `compute_coaxial_line_reflection(*, termination="short", n_steps=6000, freqs=None, n_freqs=11, cpml_axes="xyz", probe_count=12, probe_start_cells=8, probe_spacing_cells=4, feed_impedance=None, dut_impedance=None, eps_scale=None, ...)` | `CoaxialLineReflectionResult` | | coax, two-port | `compute_coaxial_two_port(*, n_steps=6000, freqs=None, n_freqs=11, cpml_axes="xyz", probe_count=12, feed_impedance=None, eps_scale=None, ...)` | `CoaxialTwoPortResult` | Result fields are listed on [Results & Observables](https://remilab.ai/rfx/api/results/#s-parameter-results). `run(compute_s_params=True)` handles lumped and wire ports only. It does not dispatch to the other families. **Let rfx choose.** `sim.compute_s_matrix(lane=None, **kwargs)` reads the registered ports, calls the matching calculator and forwards your keyword arguments unchanged. `sim.s_matrix_lane()` returns that choice as a string and runs nothing. Two cases need you to decide: - One coaxial port is ambiguous. Pass `lane="compute_coaxial_line_reflection"` or `lane="compute_coaxial_two_port"`. - Lumped/wire-only models raise, naming `run(compute_s_params=True)`, because that call needs your `n_steps`. **Experimental cross-family calculators.** `compute_mixed_s_matrix()` (lumped or wire plus microstrip) and `compute_coax_msl_transition()` (coax plus microstrip) are diagnostics. Their magnitudes are not validated, and the coax-to-microstrip matrix is not passive, so by default the call raises `ValueError`. Measure each side with its own calculator instead. ### Checking a calculator before the run ```text preflight_sparameters(*, calculator="run", strict=False, normalize=None, include_general=False) -> PreflightReport ``` `calculator` is `"run"`, `"forward"`, `"msl"` or `"waveguide"`. It checks that the registered ports suit that calculator, without running FDTD. Read `report.ok`. `strict=True` raises on errors only. ```python print(msl.preflight_sparameters(calculator="msl").ok) report = wg.preflight_sparameters(calculator="waveguide") print(report.ok, [issue.code for issue in report]) ``` For waveguides it also audits the setup: - `record_shorter_than_far_boundary_round_trip` (error): the record is shorter than one round trip to the far boundary. Raise `num_periods`. `record_far_boundary_band_below_cutoff` means the lowest frequency is below the port's cutoff, so the check could not be made. - `port_index_mirror_asymmetry`: an opposite-facing port pair is not placed symmetrically. A healthy two-port guide always shows the informational `port_index_mirror_known_e_plane_offset` note, so a non-empty report is not a failure. - `layout_measured_from_band_low_edge` (information): the band's lowest bin is within 6 % of cutoff. Size the layout from below the band. The coaxial calculators check their own requirements when called. ## Complete coaxial example `compute_coaxial_line_reflection()` needs a float32, 3-D, second-order uniform Yee grid with CPML on all six faces (positive thickness on both z faces), no periodic axis, and exactly one `face="top"` coaxial port. The line absorbs on all three axes (`cpml_axes="xyz"`, the only accepted value); its outer wall must stay more than one cell from every lateral absorber. Nothing else may be registered: it rejects extra geometry, sheets, RLC elements, probes, monitors and NTFF boxes rather than ignoring them. ```python coax = Simulation(freq_max=40e9, domain=(0.008, 0.008, 0.040), dx=0.35e-3, boundary="cpml", cpml_layers=16) coax.add_coaxial_port( (0.004, 0.004, 0.020), # x, y are used; z is derived internally face="top", pin_radius=0.635e-3, outer_radius=2.055e-3, waveform=GaussianPulse(f0=8e9, bandwidth=1.2), ) refl = coax.compute_coaxial_line_reflection( termination="short", probe_count=12, n_steps=4000, freqs=np.linspace(4e9, 12e9, 5), ) print(refl.status, f"{refl.annulus_cells:.2f} annulus cells") print(np.abs(refl.s11)) ``` - `termination`: `"short"` or `"matched"`. With `"matched"`, `dut_impedance=R` makes the load a resistor R. Don't pass `dut_impedance` otherwise. - `feed_impedance`: the feed resistor. `None` uses the line's analytic TEM impedance. - `probe_count` must be at least 3, and every probe plane must fit between the load and the source; otherwise the call raises before running. The probe ladder is placed in cells, so refining the mesh shortens the span it covers. Give it enough physical length. - `dx = 0.35 mm` puts about 4 cells across the 1.42 mm annulus. Keep at least 4. `status` reads `"under_resolved"` below about 3.5 and `"contaminated"` when the field is not a clean single TEM mode at some frequency. Check that it reads `"passed"`. - The result does not pass through the shared passivity check. Inspect `status`, `recurrence_residual` and `s11` yourself. ## Limits - **Resolution.** Lumped and wire ports: about 60 cells per wavelength at the highest frequency for one-port S11. At 60 cells per wavelength, `|S11|` on resistive loads is within 0.54 dB of the closed form, and exact on a short or open. At 30 cells per wavelength, magnitudes are within 1.1 dB and the line's phase-crossing frequencies sit 1.7–2.9 % from the closed form (the lumped/wire chain battery, measured through `forward(port_s11_freqs=...)`). Coax: at least 4 cells across the annulus. Microstrip: at least 4 cells through the substrate (preflight warns below that). - **Port reference plane.** A one-cell lumped or wire port reads V at its node and I half a cell toward the line. Lumped and wire ports return identical S11. - **End lines at their ports, not in the CPML.** CPML terminates a guided line much less well than a plane wave. Let the ports terminate the line. - **Coax results from before the three-axis absorber** (`cpml_axes="z"`, now refused) came from a line in a closed metal can and do not reproduce. - **Graded meshes.** Lumped-port S-parameters and coaxial ports raise on a graded mesh. Microstrip and waveguide calculators run there but are experimental. See [Support Boundaries](https://remilab.ai/rfx/support/). - **Microstrip bins.** Exclude every frequency bin where any entry of `MSLSMatrixResult.reliable` is `False`: S is solved jointly across drives, so the whole slice `S[:, :, k]` is affected. A `True` entry does not guarantee accuracy. - **Sources, probes and monitors are not ports.** They have no reference impedance, so they cannot produce S-parameters. --- # Check the model on the grid Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/check/model-on-grid/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/check/model-on-grid.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Refused inputs Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/check/refusals/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/check/refusals.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Design regions Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/design/design-regions/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/design/design-regions.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Autodiff and Adjoint Background Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/design/gradients/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/design/gradients.mdx rfx gives you the gradient of a scalar design objective, such as reflected energy or |S21|, with respect to thousands of design variables at the cost of about one extra simulation. This page explains where that gradient comes from, what it does and does not tell you, and how to check it before you optimize. ## Why gradients A normal RF design loop is: choose geometry and materials, simulate, extract a metric, change the design, repeat. With one or two variables, a sweep or a finite difference is enough. With a design region of hundreds of cells, finite differences need one or two runs per variable, which is too slow. A gradient answers a narrower question: at the current design, which direction in design space improves a scalar loss fastest? ```text design variables -> FDTD fields -> probes / ports / far field -> scalar loss J eps_r(x) E, H(t) S11, S21, power dJ/d eps_r ``` | Method | Cost | Use it for | |---|---|---| | Parameter sweep | one run per design point | 1–3 variables, trade studies ([Parametric Sweeps](https://remilab.ai/rfx/design/sweeps/)) | | Finite difference | one or two runs per variable | checking a gradient on a few variables | | Adjoint / reverse-mode autodiff | about one forward plus one backward run, for any number of variables | design regions and topology optimization | ## Autodiff versus a separate adjoint solve Solvers such as Meep run a forward simulation and then a separate adjoint simulation with special sources, and assemble the gradient from both fields. rfx takes a different route. The whole time-stepping loop is written in JAX, so `jax.grad` differentiates the discrete program directly (reverse-mode automatic differentiation). Mathematically this is a discrete adjoint: both compute a vector-Jacobian product for a scalar objective. In practice there is no adjoint run object to manage. You write a loss function and call `jax.grad(loss_fn)(params)`. The ideas from adjoint design still apply: 1. **The objective must be an explicit scalar.** Reflected energy, transmitted energy, directivity at one angle, mismatch to a target impedance. The solver does not know you want a "good antenna". 2. **Design variables must be continuous.** Permittivity, a material density, a smooth shape parameter. Cell counts, on/off geometry and CAD booleans are not differentiable. 3. **The gradient belongs to the observable you wrote.** A reflected-energy proxy is not a calibrated S11. Verify the final design with the matching S-parameter or far-field workflow. 4. **Optimization still needs engineering.** Filters, projection, minimum feature size, mesh convergence and an independent final check are still your job. ## What the gradient covers in rfx - `sim.forward(...)` is the differentiable entry point. It takes overrides such as `eps_override`, `sigma_override` or a `design_box` with `design_eps_override`, and returns time series and other observables as JAX arrays. - The result is the exact gradient of the discrete simulation you set up, not proof that your loss is the right RF metric. - By default `forward` uses `checkpoint=True`, which recomputes each step in the backward pass to limit memory. For long runs, segmented checkpointing (`checkpoint_segments` on a uniform mesh, `checkpoint_every` on a non-uniform one) cuts memory to roughly `sqrt(n_steps)`. See [Memory Reduction](https://remilab.ai/rfx/run/memory-and-speed/). - Keep design variables out of the CPML. Gradients through absorber cells are not meaningful design information. Typical choices: | Task | Design variable | First objective | Verify with | |---|---|---|---| | Dielectric matching block | bounded `eps_r` in a design region | reflected energy, or `forward(port_s11_freqs=...)` | the port's S-parameter workflow | | Waveguide taper | dielectric profile or smooth shape | transmitted energy | `compute_waveguide_s_matrix()` with a convergence check | | Patch tuning | substrate or feed-region parameters | a resonance or probe proxy | resonance extraction on a converged mesh | | Far-field shaping | dielectric density away from the CPML | NTFF directivity | the far-field workflow on a refined mesh | ## Check the gradient first The [periodic multilayer slab example](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/examples/inverse_design/multilayer_ar_coating.py) uses the public `Simulation.freeze_mesh()` accessor and prints a `gradient_record_length_witness` result for its reflection objective. Run its smallest workflow without optimization or plotting: ```bash JAX_PLATFORMS=cpu python examples/inverse_design/multilayer_ar_coating.py --gradient-witness-only ``` A passing record-length witness compares gradients at two run lengths; it does not establish mesh convergence or agreement with an external solver. Before you optimize a new setup, compare the autodiff gradient with a central finite difference at one cell. The model below is a small closed PEC box, so every grid index is a physical cell and nothing lands in an absorber. The mesh is coarse so the check runs in seconds. ```python import jax import jax.numpy as jnp from rfx import Box, GaussianPulse, Simulation sim = Simulation(freq_max=4e9, domain=(0.04, 0.03, 0.02), dx=2e-3, boundary="pec") sim.add_material("slab", eps_r=4.0, sigma=0.01) sim.add(Box((0.014, 0.010, 0.006), (0.026, 0.020, 0.014)), material="slab") sim.add_source((0.008, 0.015, 0.010), "ez", waveform=GaussianPulse(f0=3e9, bandwidth=0.8), amplitude_kind="current") sim.add_probe((0.032, 0.015, 0.010), "ez") # Design variable: the full permittivity grid. params = jnp.full(sim.run(n_steps=1).grid.shape, 4.0, dtype=jnp.float32) def loss_fn(eps_r): result = sim.forward(eps_override=eps_r, n_steps=200) return jnp.sum(result.time_series ** 2) ad_grad = jax.grad(loss_fn)(params) idx = (10, 7, 5) # a cell inside the slab h = 1e-2 # FD step in eps_r units fd_grad = (loss_fn(params.at[idx].add(h)) - loss_fn(params.at[idx].add(-h))) / (2 * h) rel_err = abs(ad_grad[idx] - fd_grad) / max(abs(fd_grad), 1e-30) print(f"loss = {float(loss_fn(params)):.3e}, AD/FD relative error = {float(rel_err):.2e}") ``` On this model the two agree to a few parts in a thousand. The loss is in arbitrary units here (a raw probe signal), so only the relative error matters. rfx runs in float32, so a very small finite-difference step is dominated by round-off. Start with `h = 1e-2` for permittivity-like variables and raise it toward `1e-1` when the loss is large (see [Gradient Behavior](https://remilab.ai/rfx/guide/gradient-behavior/)). `rfx.gradient_check(sim, design_params, objective_fn, eps=...)` runs the same comparison for you. It adds `design_params` to the model's permittivity, calls `objective_fn` on the forward result, and perturbs every element of `design_params` in turn, so keep it small (a scalar checks a uniform permittivity shift). Its default step `eps=1e-3` is small for float32; pass a larger one. ### A passing finite-difference check is necessary, not sufficient Agreement between autodiff and finite differences validates the **autodiff machinery**: the derivative of the loss you wrote is computed correctly. It does not show that the loss observes the physics you intended. Both methods differentiate the same loss over the same observation window. If that window is empty, for example because the reflection you are minimizing never reaches the probe during the run, or the monitor sits in the absorber, they still agree, on a gradient of numerical noise. So also check the loss's **absolute value** against a physical expectation. A time-domain reflected-energy loss for a design that reflects a real fraction of the pulse lands around `1e-2` to `1e-1`. A loss many orders of magnitude smaller (say `~1e-7`) usually means the reflection never landed in the window, and the gradient is meaningless however well it matches finite differences. Fix the setup (a longer run, a correct monitor or split placement) before you read the derivative. [Inverse Design](https://remilab.ai/rfx/design/optimization/) explains the round-trip condition behind `minimize_reflected_energy`, and [Gradient Behavior](https://remilab.ai/rfx/guide/gradient-behavior/) shows how to check that a run is long enough. ## Further reading - [Meep adjoint solver tutorial](https://meep.readthedocs.io/en/latest/Python_Tutorials/Adjoint_Solver/): the forward/adjoint two-run structure and `MaterialGrid` design variables. - A. M. Hammond et al., ["High-performance hybrid time/frequency-domain topology optimization for large-scale photonics inverse design"](https://doi.org/10.1364/OE.442074), *Optics Express* 30, 4467–4491 (2022). - M. B. Giles and N. A. Pierce, ["An Introduction to the Adjoint Approach to Design"](https://doi.org/10.1023/A:1011430410075), *Flow, Turbulence and Combustion* 65, 393–415 (2000). - S. Molesky et al., ["Outlook for inverse design in nanophotonics"](https://doi.org/10.1038/s41566-018-0246-9), *Nature Photonics* 12, 659–670 (2018). - JAX [automatic differentiation guide](https://docs.jax.dev/en/latest/automatic-differentiation.html) and [Autodiff Cookbook](https://docs.jax.dev/en/latest/notebooks/autodiff_cookbook.html). - A. Taflove and S. C. Hagness, *Computational Electrodynamics: The Finite-Difference Time-Domain Method*, 3rd ed., Artech House (2005). ## Limits - **Loss far below a physical scale** (for example `~1e-7` for a reflection proxy): the observable window is probably empty. Lengthen the run or move the monitor before trusting the gradient. - **Design cells inside the CPML**: the gradient there is not design information. Keep the design region in the physical domain. - **Out-of-memory in the backward pass**: use segmented checkpointing and the memory estimators in [Memory Reduction](https://remilab.ai/rfx/run/memory-and-speed/). - **A proxy loss optimized well, but the real metric did not improve**: verify the final design with the calibrated workflow for that metric (see the [support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/support_matrix.md)). --- # Inverse Design Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/design/optimization/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/design/optimization.md rfx is differentiable end to end: `jax.grad` returns the gradient of a scalar loss through every FDTD time step. This page shows how to optimize a permittivity region with `optimize()`, which objectives work inside the gradient loop, and how to write your own loop with `Simulation.forward()`. For the background (and how this relates to adjoint solvers), see [Autodiff and Adjoint Background](https://remilab.ai/rfx/design/gradients/). | You want to | Use | |---|---| | optimize the permittivity of a box | `optimize(sim, DesignRegion(...), objective)` | | optimize a metal/dielectric layout with filtering and projection | `topology_optimize(sim, TopologyDesignRegion(...), objective)` | | a custom loss, several kinds of variable, or your own optimizer | `jax.grad` over `sim.forward(...)` | | tune a lumped R, L or C value | `forward(rlc_values_override=...)` | The examples use coarse 2 mm meshes and a few hundred steps so the page runs in under a minute on a CPU. A real design uses a mesh that resolves the geometry, runs long enough to capture the response you optimize, and ends with a verification run of the final design. ## Optimize a design region `optimize()` runs an Adam loop over the permittivity inside a `DesignRegion`. The region is a box in metres plus an `eps_range`; the optimizer works on an unbounded latent variable that a sigmoid maps into that range. This toy model puts a dielectric region between a source and a probe and maximizes the energy that reaches the probe: ```python import jax import jax.numpy as jnp from rfx import (DesignRegion, GaussianPulse, Simulation, maximize_transmitted_energy, optimize) sim = Simulation(freq_max=10e9, domain=(0.06, 0.03, 0.03), dx=2e-3, boundary="cpml", cpml_layers=6) sim.add_source((0.012, 0.015, 0.015), "ez", waveform=GaussianPulse(f0=6e9, bandwidth=0.8), amplitude_kind="current") sim.add_probe((0.048, 0.015, 0.015), "ez") # time_series column 0 region = DesignRegion(corner_lo=(0.020, 0.008, 0.008), corner_hi=(0.040, 0.022, 0.022), eps_range=(1.0, 6.0)) objective = maximize_transmitted_energy(output_probe_idx=0) result = optimize(sim, region, objective, n_iters=5, # a real design runs tens to hundreds lr=0.5, n_steps=250, jit=True, verbose=False) print("loss per iteration:", [f"{v:.3e}" for v in result.loss_history]) print("eps range in the design:", float(result.eps_design.min()), float(result.eps_design.max())) ``` `result` is an `OptimizeResult` with `eps_design` (the permittivity in the region's cells), `loss_history`, and the final `latent`. Pass `latent` back as `init_latent=` to continue a run. Keep the region inside the declared `domain`. Any part that reaches into the CPML padding outside it is clipped off, and `optimize()` raises `ValueError` if nothing is left. ### Useful `optimize()` options | Option | What it does | |---|---| | `n_steps` / `num_periods` | record length per iteration (default 20 periods at `freq_max`). Fewer steps use less memory, but the run must still contain the response you optimize. | | `jit=True` | compile the loss and gradient once and reuse them in every iteration. Much faster on real models. See the caveats below. | | `checkpoint_segments` | segmented checkpointing on a uniform mesh, for about `sqrt(n_steps)` memory at about 2x compute. Must divide `n_steps`. See [Memory Reduction](https://remilab.ai/rfx/run/memory-and-speed/). | | `checkpoint_every` | the non-uniform-mesh counterpart (a chunk size in steps). | | `n_starts=N`, `seed` | `N` restarts from random latents; returns the best one. Helps only on a multimodal loss. | | `best_iterate=True` | return the lowest-loss iterate visited, not the last one. | | `step_clamp=value` | cap the L2 norm of each Adam step, to stop overshoot past a sharp minimum. | | `port_s11_freqs` | accumulate port S11 inside the loop, for the exact S11 objective below. | With `jit=True` the objective is traced, not run. It must not read a traced value on the host, for example `float(x)`, `np.asarray(x)` or a Python `if` on an array. Two models still do not trace and stop with a `TracerArrayConversionError` or `ConcretizationTypeError`; use the default `jit=False` for them: - a model with a microstrip (MSL) port; - a model whose mesh is itself a design variable (a traced `dz_profile`) that has a wire port together with a conductor. The jitted loss and gradient can differ from the eager ones in the last float32 bits, so a jitted optimization path can drift slightly from an eager one. ## Choose an objective Objectives are functions `objective(result) -> scalar`, minimized by the optimizer. There are two families, and only one of them works inside the gradient loop. ### Objectives that work inside `optimize()` `forward()` returns probe time series, NTFF data and, on request, port S11. It does not build the full post-processed S-parameter matrix. These objectives read only what `forward()` provides: | Objective | Reads | Meaning | |---|---|---| | `minimize_reflected_energy(port_probe_idx=0, late_fraction=0.5)` | probe time series | late-time energy / early-time energy at the port probe (an S11 proxy) | | `maximize_transmitted_energy(output_probe_idx=-1)` | probe time series | negated energy at an output probe (an S21 proxy) | | `minimize_s11_at_freq_wave_decomp(target_freq, port_idx=0)` | `forward(port_s11_freqs=...)` | exact \|S11\|² of a lumped port from its V/I waves | | `maximize_directivity(theta_target, phi_target)` | NTFF box data | directivity toward one direction | `minimize_s11_at_freq_wave_decomp` lives in `rfx.optimize_objectives`. Pass `port_s11_freqs=` to `optimize()` so the port accumulates the S11 it reads. Because it separates incident and reflected waves exactly, it has no timing precondition; prefer it over the reflected-energy proxy when your model has a lumped port. **The split window must contain the reflection.** `minimize_reflected_energy` splits the port probe's time series at `late_fraction` (by default, at half-way) and treats the late part as reflected energy. That only works if the round trip from the port to the reflecting feature and back arrives *after* the split and *before* the run ends. On a short round trip (a thin substrate, a feature close to the port) the reflection lands in the early window, the late window is almost empty, and the loss collapses toward zero. Its gradient is then numerical noise, and nothing raises an error. Check the loss value at the starting design before you optimize. A design that reflects a meaningful fraction of the pulse gives a loss around `1e-2` to `1e-1`; a loss near `1e-7` means the window is empty. Then fix it by cause: - the reflection arrives after the run ends: increase `n_steps`; - it arrives before the split: raise `late_fraction` (this moves the split earlier); - the incident pulse and the reflection overlap in time: no split separates them. Narrow the source bandwidth, or use a port with `forward(port_s11_freqs=...)` and `minimize_s11_at_freq_wave_decomp`. A finite-difference check of the gradient does not catch an empty window, because both methods differentiate the same window and agree (see [A passing finite-difference check is necessary, not sufficient](https://remilab.ai/rfx/design/gradients/#a-passing-finite-difference-check-is-necessary-not-sufficient)). **Directivity.** `maximize_directivity` reads `result.ntff_data`, so register a box with `sim.add_ntff_box(corner_lo, corner_hi)` first; without it the objective raises `ValueError`. Angles are in radians. Keep the default `log_ratio=True`: it gives the correct gradient sign for every design variable, including ones that change the total radiated power (conductors, loss, large permittivity changes). `log_ratio=False` raises `ValueError` because it gives wrong-sign gradients for power-changing variables; use the default. NTFF objectives cost more than probe objectives, so iterate on a coarse mesh and check the final design with [Far-Field and RCS](https://remilab.ai/rfx/measure/far-field-and-rcs/). ### Objectives for a finished `run()` These read `result.s_params` from `run(compute_s_params=True)`. They are for scoring a design after a full run, not for the gradient loop: inside `optimize()` or `forward()` they raise `ValueError`. ```python from rfx import maximize_bandwidth, maximize_s21, minimize_s11, target_impedance obj_s11 = minimize_s11(freqs=jnp.array([5e9]), target_db=-10) obj_s21 = maximize_s21(freqs=jnp.linspace(4e9, 6e9, 20)) obj_z = target_impedance(freq=5e9, z_target=50.0) obj_bw = maximize_bandwidth(f_center=5e9, f_bw=2e9, s11_threshold=-10) ``` An objective is only as good as the S-parameter extraction behind it. Check the port's status in the [S-parameter support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.md) before you treat an optimized number as a physical result. ## Write your own gradient loop For a custom loss, call `sim.forward(...)` inside a function and differentiate it. Build the `Simulation` once, outside the loss. `eps_override` replaces the whole permittivity grid, so its shape is the grid shape: ```python sim2 = Simulation(freq_max=10e9, domain=(0.03, 0.02, 0.02), dx=2e-3, boundary="cpml", cpml_layers=6) sim2.add_source((0.008, 0.010, 0.010), "ez", waveform=GaussianPulse(f0=5e9, bandwidth=0.8), amplitude_kind="current") sim2.add_probe((0.022, 0.010, 0.010), "ez") grid = sim2.run(n_steps=1).grid # a one-step run gives you the grid eps0 = jnp.ones(grid.shape, dtype=jnp.float32) def loss(eps_r): out = sim2.forward(eps_override=eps_r, n_steps=150) return -jnp.sum(out.time_series ** 2) grad = jax.jit(jax.grad(loss))(eps0) print("gradient shape:", grad.shape) ``` You can wrap the loss or its gradient in `jax.jit`. Keep anything that changes array shapes (the mesh, the number of steps, which ports exist) outside the jitted function, and pass continuous arrays or scalars in as arguments. ### Design-box overrides (less memory) A whole-grid `eps_override` puts grid-sized arrays on the autodiff tape at every step. If only a box is a design variable, pass the box and its values instead. The gradient is the same, and the tape stores box-sized arrays. Pass `checkpoint=False` with it, because per-step checkpointing saves the full field state every step regardless. The array must match the number of cells the box realizes on the grid (both corners resolve to their nearest cell and both ends are included): ```python box = ((0.012, 0.006, 0.006), (0.018, 0.014, 0.014)) lo, hi = grid.position_to_index(box[0]), grid.position_to_index(box[1]) box_shape = tuple(int(b) - int(a) + 1 for a, b in zip(lo, hi)) def box_loss(eps_box): out = sim2.forward(design_box=box, design_eps_override=eps_box, n_steps=150, checkpoint=False) return -jnp.sum(out.time_series ** 2) g_box = jax.grad(box_loss)(jnp.full(box_shape, 2.0)) print("box gradient shape:", g_box.shape) ``` `design_box` also accepts a `DesignRegion`. The box must not reach into the CPML or contain a source, port, lumped element or surface-impedance sheet, and it does not combine with dispersive (Debye/Lorentz) or Kerr materials, subpixel smoothing, UPML, `stencil_order=4`, or the other whole-grid overrides. Those cases raise an error; use `eps_override` for them. The one exception is a lumped or wire feed port: pass `design_box_holds_ports=True` and the port's edges keep the drawn materials and load (their gradient is zero) while the rest of the box stays a design variable. **A design conductivity** goes in `design_sigma_override`, in one of two forms: - A single box-shaped array is **per cell**. rfx averages the four cells around each edge, as it does everywhere on the grid, so the design value also reaches the layer of edges on the box's plus faces. - A 3-tuple `(sigma_x, sigma_y, sigma_z)` is **per edge**: each array is written as-is to that component's edges at the box indices, with no averaging. This is what a conducting sheet needs, since its current flows only along its two in-plane edges. A box-shaped per-edge array cannot reach the plus-face edge layer. If your per-edge design needs that layer, declare the box one cell larger on that side. For metal-shape design, `design_occupancy_override` does the same for a relaxed PEC occupancy in `[0, 1]` on a uniform mesh. ## Tune a lumped R, L or C value `add_lumped_rlc(...)` adds a circuit element to the FDTD update. It is not a port and produces no S-parameters. To measure the load, add a port with `add_port(..., impedance=Z0)` and read S11 from `forward(port_s11_freqs=...)`. Scalar component values enter the gradient through `rlc_values_override`, keyed by the 0-based order of the `add_lumped_rlc` calls. Keys you leave out keep the registered value. This works on the uniform, single-device `forward()` path. **Put the element one cell away from the port, not on the port cell.** A port reads S11 from the voltage and current at its own cell. An element inside that cell is in parallel with the source, not in the network the port measures, so S11 barely sees it and its gradient is numerical noise. ```python dx = 1.5e-3 sim3 = Simulation(freq_max=10e9, domain=(0.02, 0.02, 0.02), dx=dx, boundary="cpml", cpml_layers=6) port_pos = (0.0093, 0.0093, 0.0093) load_pos = (port_pos[0] + dx, port_pos[1], port_pos[2]) # one cell away sim3.add_port(position=port_pos, component="ez", impedance=50.0) sim3.add_lumped_rlc(position=load_pos, component="ez", R=200.0, C=0.2e-12, topology="series") def load_loss(R): out = sim3.forward(n_steps=800, port_s11_freqs=jnp.array([5e9]), rlc_values_override={0: {"R": R}}) return jnp.abs(out.s_params[0]) ** 2 # |S11|^2 at 5 GHz R0 = 200.0 value, dloss_dR = jax.value_and_grad(load_loss)(R0) assert jnp.isfinite(value) and jnp.isfinite(dloss_dR) fd = (load_loss(R0 + 10.0) - load_loss(R0 - 10.0)) / 20.0 print(f"|S11|^2 = {float(value):.4f}, AD dL/dR = {float(dloss_dR):.3e}, " f"FD dL/dR = {float(fd):.3e}") ``` The gradient is taken at the value the element was registered with. Check that the loss and gradient are finite at every value you evaluate, as the `assert` does, and keep an optimizer's R inside a physical range (for example, optimize a bounded latent variable). ## Limits - **Loss near zero with a clean finite-difference check**: the observation window is probably empty. See the split-window rule above. - **A proxy loss improved but the real metric did not**: re-run the final design through the calibrated workflow (port S-parameters, resonance extraction or far field) on a converged mesh. See [Validation](https://remilab.ai/rfx/guide/validation/). - **`ValueError` from `minimize_s11` / `maximize_s21` inside `optimize()`**: those read post-processed S-parameters. Use a time-domain proxy or `minimize_s11_at_freq_wave_decomp` in the loop. - **Mesh refinement**: `add_refinement` acts only through `run()`. `forward()`, `optimize()` and `topology_optimize()` refuse a refined model. - **`jit=True` fails with a tracer error**: an MSL port, or a traced mesh with a wire port and a conductor. Use `jit=False`. - **Out of memory in the backward pass**: use a design box, segmented checkpointing or fewer steps. See [Memory Reduction](https://remilab.ai/rfx/run/memory-and-speed/). --- # Parametric Sweeps Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/design/sweeps/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/design/sweeps.mdx A sweep runs the same model over a set of parameter values and compares a metric. It is the simplest way to explore a small design space, and a good check before you start a gradient optimization. This page shows the four sweep tools and when to use each. | Tool | Use it for | |---|---| | `rfx.parametric_sweep(factory, name, values)` | one parameter, any change (geometry, mesh, sources) | | `rfx.batch.ParameterSweep` + `run_batch` | a Cartesian grid of several parameters | | `rfx.vmap_material_sweep(sim, "material.eps_r", values)` | material values only, run as one batched simulation | | `rfx.batch.run_batch_with_manifest` | long sweeps you want to resume and reproduce | The examples use a coarse 2 mm mesh and a few hundred steps so the page runs quickly. A real sweep needs a converged mesh and a run long enough for the metric you compare. ## A model factory Every sweep tool except `vmap_material_sweep` calls a factory that builds a fresh `Simulation` from the parameters. Keep the factory pure: everything that defines a case comes from its arguments. This model is a dielectric insert between a soft source and a probe: ```python import numpy as np from rfx import Box, GaussianPulse, Simulation def make_insert_sim(insert_width=0.012, eps_r=4.0): sim = Simulation(freq_max=4e9, domain=(0.06, 0.04, 0.03), dx=2e-3, boundary="cpml", cpml_layers=6) sim.add_material("insert", eps_r=eps_r, sigma=0.01) y0, y1 = 0.020 - insert_width / 2, 0.020 + insert_width / 2 sim.add(Box((0.024, y0, 0.012), (0.036, y1, 0.018)), material="insert") sim.add_source((0.014, 0.020, 0.015), "ez", waveform=GaussianPulse(f0=2.4e9, bandwidth=0.8), amplitude_kind="current") sim.add_probe((0.046, 0.020, 0.015), "ez") return sim def peak_probe(result): """Example metric: peak absolute probe signal.""" return float(np.max(np.abs(np.asarray(result.time_series[:, 0])))) ``` ## One parameter `parametric_sweep` calls `factory(value)` for each value, runs it, and returns a `SweepResult` with `.results`, `.param_values` and `.param_name`: ```python from rfx import parametric_sweep widths = [0.008, 0.012, 0.016] sweep1 = parametric_sweep(lambda w: make_insert_sim(insert_width=w), "insert_width", widths, n_steps=300) for w, result in zip(sweep1.param_values, sweep1.results): print(f"width {w * 1e3:.0f} mm: peak probe {peak_probe(result):.3e}") ``` `n_steps=` and `until_decay=` go to each `run()`; anything else goes in `run_kwargs=`. `rfx.plot_sweep(sweep1, metric=peak_probe)` plots a metric against the parameter. It also accepts the built-in names `"s11_min_db"` (for runs with S-parameters) and `"peak_field"`. ## A grid of parameters `ParameterSweep` holds the Cartesian product of several parameter lists. `run_batch` calls `factory(**params)` for each combination and returns a list of `(params, result)` pairs: ```python from rfx.batch import ParameterSweep, SimulationDataset, run_batch grid = ParameterSweep(insert_width=[0.008, 0.016], eps_r=[3.0, 6.0]) print(grid.keys, grid.total) # ['insert_width', 'eps_r'] 4 results = run_batch(make_insert_sim, grid, run_kwargs={"n_steps": 300}) for params, result in results: print(params, f"{peak_probe(result):.3e}") ``` `SimulationDataset` turns the pairs into arrays or a CSV file, for plotting or a surrogate model. `output_fn` returns a 1-D feature vector per case: ```python dataset = SimulationDataset.from_results( results, input_keys=["insert_width", "eps_r"], output_fn=lambda r: np.array([peak_probe(r)]), ) X, y = dataset.to_numpy() # X: (4, 2) inputs, y: (4, 1) outputs dataset.to_csv("sweep_metrics.csv") ``` ## Material values in one batched run When only material values change, the grid is the same for every case, and `vmap_material_sweep` runs all of them as one batched simulation with `jax.vmap`. On a GPU this is much faster than a loop. Name the parameter as `"."`, or just `"eps_r"`, `"sigma"` or `"mu_r"`: ```python from rfx import vmap_material_sweep base = make_insert_sim() vsweep = vmap_material_sweep(base, "insert.eps_r", np.array([2.0, 4.0, 6.0]), n_steps=300) print(vsweep.time_series.shape) # (n_values, n_steps, n_probes) ``` The fast path supports PEC and CPML boundaries, soft sources, point probes and DFT plane probes. A model with ports, TFSF, dispersion, flux monitors or an NTFF box falls back to running the cases one by one. Memory grows with the batch size, because each case keeps its own fields. Geometry and incidence-angle sweeps belong in `parametric_sweep`. ## Resumable sweeps with a manifest For long sweeps, `run_batch_with_manifest` writes a `manifest.json` in the output directory. It records a stable case ID, the parameters, status, timing, metrics, and the size and hash of every artifact each case wrote. ```python import json from pathlib import Path from rfx.batch import run_batch_with_manifest, summarize_batch_manifest def metric_fn(result): return {"peak_probe": peak_probe(result)} def artifact_fn(case_dir: Path, params, result): (case_dir / "metric.json").write_text(json.dumps(metric_fn(result))) return {"metric": "metric.json"} # paths relative to case_dir records = run_batch_with_manifest( make_insert_sim, grid, "runs/insert-sweep", run_kwargs={"n_steps": 200}, metric_fn=metric_fn, artifact_fn=artifact_fn, resume=True, ) print(summarize_batch_manifest("runs/insert-sweep/manifest.json")["status_counts"]) ``` How resume decides what to skip: - A case is skipped only if its record is `completed`, has at least one metric, matches the current parameters and run settings, and every artifact it declared is still on disk with the recorded size and hash. Anything else (failed, interrupted, modified or missing files) runs again. - `run_kwargs` are fingerprinted, so changing `n_steps` reruns every case. If you change something the runner cannot see (the factory, the metric, an input file), pass a new `run_fingerprint="..."`. - The runner stops at the first failure. Pass `continue_on_error=True` to record failed cases with `status="failed"` and carry on. ## Exporting S-parameters If each case computes S-parameters (for example `run_kwargs={"compute_s_params": True}` on a model with ports), write one Touchstone file per case: ```python from rfx import write_touchstone out_dir = Path("outputs/sweep") out_dir.mkdir(parents=True, exist_ok=True) for i, (params, result) in enumerate(results): if result.s_params is None: # this example model has no ports continue write_touchstone(out_dir / f"case_{i:03d}.s2p", result.s_params, result.freqs, z0=50.0) ``` A Touchstone file is an exchange format, not a validation. Check the port type in the [S-parameter support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.md) before you rely on the numbers. ## Limits - **A sweep repeats one model's assumptions.** If a single case uses an unsupported source, port or mesh combination, the sweep inherits it. Run and inspect one case before sweeping. - **Comparisons across geometry need the same accuracy everywhere.** A feature that snaps to the grid differently in each case adds a staircase error that looks like a trend. Check `sim.preflight()` for off-lattice warnings, and refine the mesh if the metric moves by less than a cell's worth. - **`vmap_material_sweep` holds every case in memory.** Reduce the batch or the number of DFT-plane frequencies if it runs out of memory. - **A short `n_steps` ranks cases by an unfinished response.** Use a run length that lets each case settle before you compare metrics. --- # The accuracy bar and status words Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/evidence/accuracy-bar/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/evidence/accuracy-bar.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Checking a gradient Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/evidence/gradients/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/evidence/gradients.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Tutorial: Convergence Study Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/evidence/mesh-convergence/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/evidence/mesh-convergence.mdx An FDTD answer depends on the cell size. A convergence study shows how much: you solve the same problem on several meshes and check that the number you care about stops moving. Do one before you quote a number from a new geometry. This tutorial runs a three-mesh study of a resonance in a loaded cavity. It takes about ten seconds on a laptop CPU. ## 1. Choose one number A study tracks one scalar. Pick the one your claim is about: | Claim | Metric | |---|---| | resonance | a Harminv frequency in a fixed band | | S-parameter | `abs(S[receiver, driven, k])` at one stated frequency | | field amplitude | a peak or RMS probe value | | far field | directivity, or the pattern at one angle | Different quantities converge at different rates. If you need two, run two studies. ## 2. Write a factory that builds the same problem at any cell size `convergence_study` calls your factory with each cell size. The factory must build the **same physical problem** every time, so choose cell sizes that put every face of your geometry on a grid plane. Otherwise the geometry itself changes from mesh to mesh as faces snap to different planes. Here the cavity is 80 × 64 × 40 mm with a 16 × 16 × 8 mm dielectric insert. Every dimension and position is a multiple of 8 mm, so cells of 2.0, 1.6 and 1.0 mm all land on them exactly. ```python from rfx import Box, GaussianPulse, Simulation def make_case(dx: float) -> Simulation: sim = Simulation(freq_max=4e9, domain=(0.080, 0.064, 0.040), boundary="pec", dx=dx) sim.add_material("insert", eps_r=4.0, sigma=0.01) sim.add(Box((0.032, 0.024, 0.016), (0.048, 0.040, 0.024)), material="insert") sim.add_source((0.024, 0.032, 0.008), "ez", waveform=GaussianPulse(f0=2.4e9, bandwidth=0.8), amplitude_kind="current") sim.add_probe((0.056, 0.032, 0.008), "ez") return sim ``` For geometry that can't be aligned at every size, run `make_case(dx).fidelity_report()` at each size to see where the faces landed, and include that snapping in what you report. ## 3. Run the study ```python from rfx import convergence_study def first_resonance(result) -> float: modes = result.find_resonances(freq_range=(2e9, 4e9)) if not modes: raise RuntimeError("no mode in the band: lengthen the run or widen freq_range") return float(min(m.freq for m in modes)) # the lowest mode, at every mesh study = convergence_study( sim_factory=make_case, dx_values=[2.0e-3, 1.6e-3, 1.0e-3], # coarse to fine metric_fn=first_resonance, run_kwargs={"num_periods": 40}, # the same record at every mesh ) study.summary() ``` One CPU run printed the following. The last digits can differ slightly between versions and hardware. ```text Mesh Convergence Summary ================================================== dx (mm) Metric Rel. Error -------------------------------------------------- 2.0000 2.92606e+09 0.0998% 1.6000 2.9269e+09 0.0708% 1.0000 2.92797e+09 0.0344% -------------------------------------------------- Extrapolated value : 2.92898e+09 Convergence order : 1.54 ``` `study.metrics`, `study.extrapolated`, `study.order` and `study.errors` hold the same numbers; `study.results` keeps each run's `Result`; `study.plot()` draws them. The metric function picks the lowest mode in a fixed band, so the study follows the same mode at every mesh. It raises when no mode is found instead of indexing an empty list. ## 4. Check the record length separately A study that changes `dx` says nothing about whether the runs were long enough. Rerun the finest mesh with a longer record and compare: ```python longer = make_case(1.0e-3).run(num_periods=80) f_long = first_resonance(longer) f_study = float(study.metrics[-1]) print(f"40 periods: {f_study / 1e9:.5f} GHz, 80 periods: {f_long / 1e9:.5f} GHz") ``` Here the two agree to far better than the mesh-to-mesh change, so the record length is not what limits the answer. If they differed, you would need a longer record for every mesh in the study. ## 5. Read the result - **The change between the two finest meshes** is the most direct measure of the remaining mesh error. Here it is about 0.04 %. - **The extrapolated value** estimates the zero-cell-size answer, assuming the error follows a power law in `dx`. `errors` are relative to it. - **The observed order** should be plausible. Standard Yee is second order; staircased dielectric edges and conductors often pull it lower, as here (1.54). An order near zero, a negative one, or metrics that don't move monotonically mean the study is not in the converging range. Refine further or look for a setup problem before trusting the extrapolation. When you report the result, give the cell sizes, the metric at each, the extrapolated value and order, and the script that produced them. A study covers the metric it tracked, on the geometry and settings it used. It is not evidence for other outputs of the same model. Accuracy against the real device also needs a comparison with a reference: another solver, an analytic result or a measurement. See [Cross-Validation and Accuracy](https://remilab.ai/rfx/guide/validation/) and the [Benchmark Table](https://remilab.ai/rfx/guide/benchmarks/). ## Other kinds of refinement - **Graded meshes.** `convergence_study` passes one number to your factory, so it can be a scale factor instead of a cell size: build `dx`, `dz_profile` and so on from it. Refine the fine and coarse regions together, and keep neighbouring cells within a ratio of about 1.3. See [Non-Uniform Mesh](https://remilab.ai/rfx/model/domain-mesh/). - **Ports.** Lumped and wire port S11 needs about 60 cells per wavelength at the top frequency. Microstrip is usually set by the substrate thickness. Coax needs at least 4 cells across the annulus. See [Probes and S-Parameters](https://remilab.ai/rfx/measure/s-parameters/). - **Probe ladders are placed in cells.** A coaxial port's probes are spaced in cells, so refining the mesh shortens the physical span they cover. Give the probe array enough span at the finest mesh. - **Domain and absorber.** A fine mesh does not fix an absorber that is too close. Vary the air margin and `cpml_layers` as a separate check. ## Limits - **Metrics jump around between meshes** → the geometry probably snaps differently at each size. Align faces to all cell sizes, or check `fidelity_report()` at each one. - **A different mode is picked at each mesh** → select by a fixed band and rule (as `first_resonance` does), or by field pattern. - **Finest mesh is too expensive** → use a graded mesh to refine only where fields vary quickly, and refine it as a whole. - **The answer moves with record length** → lengthen every run in the study, not only the finest. --- # Is the run long enough? Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/evidence/simulation-time/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/evidence/simulation-time.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Your First Patch Antenna Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/examples/first-patch/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/examples/first-patch.mdx This tutorial builds a real device: a 2.4 GHz rectangular patch on a finite FR4 board. It teaches four things you will need for your own models: - sizing a starting design from closed-form formulas; - a **graded z mesh** that resolves a thin substrate without refining the whole domain; - declaring copper as **sheets**; - reading preflight output, and knowing what a short run can and cannot tell you. It ends with a quick execution check. Reporting a resonance, a return loss or a Q needs more than that, and the last section lists what. ## 1. Starting dimensions The usual transmission-line model gives a first guess for the patch length L and width W: ```python import numpy as np C0 = 299_792_458.0 f0 = 2.4e9 # design frequency eps_r = 4.4 # nominal FR4 h = 1.5e-3 # substrate thickness tan_d = 0.02 # nominal FR4 loss tangent W = C0 / (2 * f0) * np.sqrt(2 / (eps_r + 1)) eps_eff = (eps_r + 1) / 2 + (eps_r - 1) / 2 * (1 + 12 * h / W) ** -0.5 dL = 0.412 * h * ((eps_eff + 0.3) * (W / h + 0.264) / ((eps_eff - 0.258) * (W / h + 0.8))) L = C0 / (2 * f0 * np.sqrt(eps_eff)) - 2 * dL # A constant conductivity that matches tan(delta) at f0 only. sigma_fr4 = 2 * np.pi * f0 * 8.854_187_8128e-12 * eps_r * tan_d print(f"L = {L * 1e3:.2f} mm, W = {W * 1e3:.2f} mm") ``` These formulas give a starting point, not a reference to validate the solver against. Real FR4 permittivity and loss depend on the laminate and the frequency, so use your material's data. ## 2. A graded z mesh The substrate is 1.5 mm thick. Resolving it with 6 cells needs 0.25 mm cells, but the air around the antenna is fine at 1 mm. A `dz_profile` lists the z cell sizes from bottom to top, so you can use fine cells only where you need them: ```python from rfx import Box, GaussianPulse, Simulation from rfx.mesh_edges import edge_aware_profiles dx = 1.0e-3 # nominal x and y cell size dz_sub = h / 6 # 6 cells through the substrate # Grade 1.0 mm -> 0.25 mm with each neighbour ratio below 1.3. transition = np.array([1.000, 0.780, 0.610, 0.477, 0.373, 0.291, 0.250]) * 1e-3 lower_air = np.concatenate([np.full(10, dx), transition[1:]]) upper_air = np.concatenate([transition[-2::-1], np.full(24, dx)]) dz_profile = np.concatenate([lower_air, np.full(6, dz_sub), upper_air]) z_sub_lo = float(lower_air.sum()) # substrate bottom z_sub_hi = z_sub_lo + h # substrate top dom_x, dom_y = 0.100, 0.095 gx_lo, gx_hi = 0.020, 0.080 gy_lo, gy_hi = 0.020, 0.075 px0, px1 = dom_x / 2 - L / 2, dom_x / 2 + L / 2 py0, py1 = dom_y / 2 - W / 2, dom_y / 2 + W / 2 ground = Box((gx_lo, gy_lo, z_sub_lo), (gx_hi, gy_hi, z_sub_lo)) substrate = Box((gx_lo, gy_lo, z_sub_lo), (gx_hi, gy_hi, z_sub_hi)) patch = Box((px0, py0, z_sub_hi), (px1, py1, z_sub_hi)) feed_x, feed_y = px0 + 8e-3, dom_y / 2 probe_x, probe_y = dom_x / 2 + 5e-3, dom_y / 2 + 5e-3 profiles = edge_aware_profiles( (dom_x, dom_y, float(dz_profile.sum())), dx, sheets=[ground, patch], solids=[substrate], faces={"x": [feed_x, probe_x], "y": [feed_y, probe_y]}, axes="xy", ) sim = Simulation( freq_max=4e9, domain=(dom_x, dom_y, 0.0), # z size comes from sum(dz_profile) dx=dx, dz_profile=dz_profile, **profiles, boundary="cpml", cpml_layers=8, ) ``` Three rules for graded meshes: - Keep neighbouring cell sizes within a ratio of about 1.3. - Start and end the profile with cells equal to `dx`, so the absorber sees the same cell size on every face. - Put material interfaces exactly on cell boundaries. Here the substrate starts at `sum(lower_air)` for that reason. [Non-Uniform Mesh](https://remilab.ai/rfx/model/domain-mesh/) covers graded meshes in depth. ## 3. Board, copper, source and probe The ground plane and the patch are copper foil, which rfx models as **sheets** of zero thickness on one grid plane (see [Core Concepts](https://remilab.ai/rfx/start/concepts/)): ```python sim.add_material("fr4_design", eps_r=eps_r, sigma=sigma_fr4) sim.add(substrate, material="fr4_design") sim.add_thin_conductor(ground) sim.add_thin_conductor(patch) # A soft source under the patch, 8 mm in from the edge, and an offset probe. src_z = z_sub_lo + 2.5 * dz_sub sim.add_source((feed_x, feed_y, src_z), "ez", waveform=GaussianPulse(f0=f0, bandwidth=0.8), amplitude_kind="current") sim.add_probe((probe_x, probe_y, src_z), "ez") report = sim.preflight() print(report.format()) ``` ### Reading what rfx tells you Each `add_thin_conductor` call warns that the sheet is a **lossless PEC sheet**, because copper's conductivity is above the 1e6 S/m PEC threshold. The patch therefore has no conductor loss. If conductor loss matters for your Q or efficiency, pass `surface_impedance_f0=f0`. The sheet is then modelled as a resistive sheet with copper's surface resistance at `f0`, on the same footprint, instead of as PEC. The in-plane `edge_aware_profiles` mesh registers the ground and patch to their solved edges and the feed and probe to grid nodes. This keeps the drawn patch dimensions without rounding them to a uniform lattice. Read `sim.fidelity_report()` and the preflight findings to check that realization; registration does not replace a mesh-convergence study. This edge-aware in-plane grading raises four `nu_grading_reaches_absorber` advisories because the interior cells adjacent to the x/y absorber faces are not uniform. The grading changes the discrete medium normal to the boundary where the absorber starts, leaving no interior runway to separate the transition’s reflection from the absorber’s, so this example is outside the validated multi-band accuracy envelope. Treat preflight warnings as part of your result. Use `preflight(strict=True)` only for models you expect to have no findings at all, because strict mode turns every finding into an error. ## 4. A short execution check ```python short_run = sim.run(num_periods=2) ts = np.asarray(short_run.time_series) assert ts.size > 0 and np.isfinite(ts).all() print(f"{ts.shape[0]} steps, settling {short_run.settling_db:.1f} dB") ``` This shows that the model runs and produces finite data. Two periods is far too short for the patch to ring down, so rfx warns that the ring-down is truncated. Nothing from this run is a result. ## 5. From a model to a result To report a resonance, Q or return loss for this patch: 1. **Run long enough.** Use `until_decay=` or a long fixed record, and check `result.settling_db`. A patch has a moderately high Q, so it needs many periods. 2. **Extract the resonance** with `result.find_resonances(freq_range=(2e9, 3e9))`. Pick the mode by its field pattern or far field, not by which peak is biggest. Handle the case where no mode is found. 3. **Refine the mesh** in x/y and z, and enlarge the air margin. Report how much the frequency moves, not just the finest answer. See [Convergence Study](https://remilab.ai/rfx/evidence/mesh-convergence/). 4. **For return loss, use a port.** The soft source here has no impedance, so it cannot give S11. Use a lumped or microstrip port and its calculator from [Sources and Ports](https://remilab.ai/rfx/model/ports/). 5. **Compare with a reference.** Use another solver or a measurement. [Benchmarks](https://remilab.ai/rfx/guide/benchmarks/) shows how rfx's own patch case compares with openEMS. `examples/tutorials/patch_antenna_demo.py` carries out this workflow end to end, including far-field mode identification. [Examples](https://remilab.ai/rfx/examples/) lists the full learning path. --- # Examples Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/examples/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/examples/index.mdx ## Choose by what you want to see [![Open versus reflecting boundaries for an electric pulse](https://remilab.ai/rfx/gallery/assets/current-visuals/boundary-reflection.png)](https://remilab.ai/rfx/gallery/boundary-reflection/) Follow a field Begin with a pulse, a boundary and a dielectric interface. Actual time-domain snapshots make a useful first experiment. [Watch and run →](https://remilab.ai/rfx/gallery/boundary-reflection/) [![Guided-wave material design and reflection spectrum](https://remilab.ai/rfx/gallery/assets/showcase-20260927/taper_traj.jpg)](https://remilab.ai/rfx/gallery/waveguide-taper/) Connect structure to response Explore historical films of a guided-wave taper or microstrip filter, then use the related source examples. [Explore RF components →](https://remilab.ai/rfx/gallery/waveguide-taper/) [![Material pattern and radiation direction during a historical optimization](https://remilab.ai/rfx/gallery/assets/showcase-20260927/steer_traj.jpg)](https://remilab.ai/rfx/gallery/beam-steering/) Shape the radiation See a dielectric cover change a beam's direction, and learn which variables its sensitivity plot describes. [Explore a design history →](https://remilab.ai/rfx/gallery/beam-steering/) New to the API? Start with the [first runnable model](https://remilab.ai/rfx/guide/first-run/) and the [visual manual](https://remilab.ai/rfx/gallery/visual-manual/). ## Run from a source checkout The scripts below ship in a source checkout under `examples/`. Run them in order: each one teaches one decision a real design needs. The last column says which ports and boundaries the script builds, so you can find the one closest to your structure. Paths in the tables are relative to `examples/`. Run a script from the repository root, for example: ```bash python examples/quickstart/hello_world.py ``` Most finish in seconds to a few minutes on a CPU. Steps 4 and 10 take tens of minutes. The [machine-readable index](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/examples/index.json) lists builders and output files. ## The learning path | # | Script | What it teaches | Ports and boundaries | |---|---|---|---| | 1 | [`quickstart/hello_world.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/quickstart/hello_world.py) | install check: the smallest Simulation → run → probe loop | PEC box; point source and one probe | | 2 | [`tutorials/boundary_spec_demo.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/boundary_spec_demo.py) | choosing boundaries: CPML for open problems, PEC for closed ones, PMC symmetry, periodic cells; and declaring metal inside the domain as a sheet, a volume or a wire | `BoundarySpec` per axis and face; point source and probe | | 3 | [`tutorials/slab_rt_flux_monitor.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/slab_rt_flux_monitor.py) | reflection and transmission spectra done right: flux monitors plus a reference run, and why a probe FFT misleads | CPML; plane wave; two flux monitors | | 4 | [`tutorials/nonuniform_patch_demo.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/nonuniform_patch_demo.py) | a graded z mesh for a thin substrate, and checking where the layers actually landed after grading shifts them | CPML; `dz_profile`; point source and probe | | 5 | [`tutorials/materials_and_dispersion.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/materials_and_dispersion.py) | library and custom materials, why a lossless resonator's Q is meaningless, Debye and Lorentz poles | CPML; point source and probe | | 6 | [`tutorials/run_control_and_fields.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/run_control_and_fields.py) | `n_steps` vs `num_periods` vs `until_decay`, the truncation warning, reading field slices | CPML; point source and probe | | 7 | [`tutorials/ports_and_sparams_101.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/ports_and_sparams_101.py) | which port fits which structure, `S11` basics, and the bins the extractor marks unreliable | lumped, microstrip, waveguide and coaxial ports, and `add_lumped_rlc`; CPML and PEC | | 8 | [`tutorials/resonance_harminv.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/resonance_harminv.py) | ring-down resonance extraction, picking a mode by its physics rather than its amplitude, record length vs resolution | closed PEC cavity; point source and probe; no port, which would load the cavity | | 9 | [`tutorials/antenna_farfield_pattern.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/antenna_farfield_pattern.py) | placing a far-field box at least half a wavelength from the radiator, and checking directivity against the textbook short dipole; writes `output/short_dipole_e_plane.csv` | CPML; `add_ntff_box`; point source | | 10 | [`tutorials/patch_antenna_demo.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/patch_antenna_demo.py) | a real antenna end to end: a stack registered to the built mesh, copper as sheets, the radiating mode picked by its far field, a settling check and an error budget against openEMS | CPML; graded `dz_profile`; `add_ntff_box`; point source and probe | | 11 | [`tutorials/rcs_scattering.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/rcs_scattering.py) | radar cross section with `subtract_incident_reference=True` | CPML; plane wave and an empty reference run | | 12 | [`inverse_design/differentiable_s11_design.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/inverse_design/differentiable_s11_design.py) | `jax.grad` through `compute_waveguide_s_matrix`, checked against central finite differences | CPML; two WR-90 waveguide ports | | 13 | [`tutorials/artifact_report_demo.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/artifact_report_demo.py) | exporting a shareable scene, mesh and preflight metadata, a report and a manifest | PEC box; lumped port and probe; no FDTD run | Read the preflight output of every script. Its warnings name the conditions that would make a result wrong: a lossless resonator, a layer that landed on the wrong cells, a run cut off before the field rang down. ## Other scripts | Script | What it shows | Ports and boundaries | |---|---|---| | [`tutorials/adi_solver_demo.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/adi_solver_demo.py) | the ADI solver's time-step vs accuracy trade, against an exact cavity; see [ADI Solver](https://remilab.ai/rfx/guide/adi-solver/) | closed PEC cavity; point source and probe | | [`tutorials/cad_mesh_import_demo.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/cad_mesh_import_demo.py) | importing an STL part with `MeshShape` and a mm → m `scale`. Needs the `rfx-fdtd[cad]` extra and exits cleanly without it; exposes `build_simulation()` | CPML; point source | | [`inverse_design/multilayer_ar_coating.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/inverse_design/multilayer_ar_coating.py) | a three-layer anti-reflection coating optimized through `forward(eps_override=...)`, checked against the transfer-matrix optimum | CPML on x, periodic y and z; point source and two probes | | [`inverse_design/field_observable_shielding.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/inverse_design/field_observable_shielding.py) | pooling two DFT plane probes into one worst-case leakage objective with `field_softmax` | CPML; plane wave; two DFT planes | | [`inverse_design/progressive_demo.py`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/inverse_design/progressive_demo.py) | `progressive_optimize` running a design over increasing mesh resolutions. A minimal API demo with a trivial optimum | CPML; point source and probe | | [`config/microstrip_thru.yaml`](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/config/microstrip_thru.yaml) | the YAML front end, run with `rfx run`, `rfx plot` and `rfx export-geometry` | CPML; two 50 Ω wire ports; ground and trace as sheets | ## Validation scripts are not tutorials The comparisons in `tests/crossval/` check rfx against another solver. They assume you already understand the setup and they judge numbers, so they teach little about building a model. See [Benchmarks](https://remilab.ai/rfx/guide/benchmarks/) for what they show. --- # Tutorial: Patch Antenna Design Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/examples/patch-antenna/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/examples/patch-antenna.mdx [Your First Patch Antenna](https://remilab.ai/rfx/examples/first-patch/) builds a patch model that passes preflight and runs. This page is the roadmap from there to numbers you can report (resonance, return loss, radiation pattern) and then to design iteration. Each step links to the page that covers it in detail. ## The workflow | Step | Goal | Where | |---|---|---| | 1. Model | size the patch, grade the z mesh, declare foils as sheets, clear preflight | [Your First Patch Antenna](https://remilab.ai/rfx/examples/first-patch/) | | 2. Resonance | run until the ring-down settles, extract modes, identify the radiating one | [Probes and S-Parameters](https://remilab.ai/rfx/measure/s-parameters/) | | 3. Return loss | replace the soft source with a port and compute S11 | [Sources and Ports](https://remilab.ai/rfx/model/ports/) | | 4. Radiation | add an NTFF box and compute pattern and directivity | [Far-Field and RCS](https://remilab.ai/rfx/measure/far-field-and-rcs/) | | 5. Convergence | refine the mesh and report how much each number moves | [Convergence Study](https://remilab.ai/rfx/evidence/mesh-convergence/) | | 6. Design iteration | sweep or optimize the dimensions and substrate | [Parametric Sweeps](https://remilab.ai/rfx/design/sweeps/), [Inverse Design](https://remilab.ai/rfx/design/optimization/) | `examples/tutorials/patch_antenna_demo.py` runs steps 1, 2 and 4 end to end on the openEMS "Simple Patch Antenna" geometry, including far-field mode identification. [Benchmarks](https://remilab.ai/rfx/guide/benchmarks/) shows how rfx's patch cases compare with openEMS. ## Modelling rules - **Finite ground, open domain.** Draw the ground plane as a conductor inside an all-CPML domain. A PEC boundary wall is infinite and closes the structure into a cavity, which moves the resonance. - **Foil is a sheet.** Declare copper foil (ground and patch) with `add_thin_conductor()` on a zero-thickness `Box`. Use a thick `Box` of metal only when the metal really is that thick. See [Materials and Geometry](https://remilab.ai/rfx/model/materials/). - **Resolve the substrate.** A thin substrate needs several cells through its thickness. Use a graded `dz_profile` rather than refining the whole domain, and put the substrate faces on cell boundaries. If you build the profile with `smooth_grading`, derive every z coordinate from the final profile: smoothing inserts transition cells that move the fine band. See [Non-Uniform Mesh](https://remilab.ai/rfx/model/domain-mesh/). - **Keep the drawn size.** Preflight warns when patch edges fall between grid planes. Refine the mesh rather than rounding L and W to the grid, because rounding changes the antenna. ## Resonance A patch rings down slowly, so run for many periods and check `result.settling_db` against a -40 dB bar before you quote a frequency. With a soft current source feeding the patch, `run(until_decay=...)` may never stop: the source leaves a static charge between patch and ground that neither radiates nor decays. Use a fixed `num_periods` and read the settling level instead. `result.find_resonances(freq_range=...)` returns several modes. A rectangular patch has at least two low-order modes (one along each side). Identify the radiating design mode by its far field: a broadside beam and the peak of the radiated power. Don't pick it by amplitude, or by which mode is closest to the formula estimate. Both have picked the wrong mode on real patches. ## Return loss A soft source has no impedance, so it cannot give S11. Feed the patch with a port instead: - a **wire port** (`add_port(..., extent=...)`) spanning the substrate, for a probe feed; - a **microstrip port** (`add_msl_port`), for an edge or inset feed. For a lumped or wire port, use about 60 cells per wavelength at the highest frequency of interest for one-port S11. At that resolution the magnitude on resistive loads is within 0.54 dB of closed form. For a microstrip port, the substrate thickness sets the cell (at least 4 cells through it; preflight warns below that). See [Sources and Ports](https://remilab.ai/rfx/model/ports/) and the [S-parameter support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.md). ## Radiation Add a near-to-far-field box with `sim.add_ntff_box(...)` around the antenna, with each face about half a wavelength from the radiator where the domain allows. The face below the ground plane often cannot be that far; preflight tells you, and you should report it with the result. Evaluate the pattern at the radiating mode's frequency. See [Far-Field and RCS](https://remilab.ai/rfx/measure/far-field-and-rcs/). ## Design iteration - **A few dimensions** (length, width, inset depth): sweep them with `parametric_sweep` or a `ParameterSweep` grid. Each case is a full run, so start coarse and refine the best region. - **Material or shape regions**: gradient optimization through `forward()` and `optimize()`. Use an S11 objective from a port (`port_s11_freqs=`), check the gradient first, and verify the final design with steps 2–5 on a converged mesh. See [Inverse Design](https://remilab.ai/rfx/design/optimization/). ## Limits - **A short run, or a clean preflight, is not a result.** Resonance, S11, Q and pattern all need a settled record and a mesh study. - **Coarse meshes shift the resonance, in either direction.** An under-resolved substrate tends to read high and a staircased patch edge low, so the error on a coarse mesh is not a fixed bias. Refine both and report the spread. - **Soft-source runs give no S11 and may never meet `until_decay`.** Use a port for return loss and a fixed record length for ring-down. - **The wrong mode is easy to pick.** Confirm the radiating mode from the far field. --- # Steer a beam with a dielectric cover Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/gallery/beam-steering/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/gallery/beam-steering.mdx A dipole radiates above a metal reflector. A patterned dielectric cover changes the phase of the outgoing field across the aperture. This example connects that material pattern to the direction of the main radiation lobe. Historical design visualization. This is the 441-latent-variable trajectory on a wavelength/20 mesh. The plotted response is directivity; it does not establish realized gain, efficiency or broadband performance. ![Video poster](https://remilab.ai/rfx/gallery/assets/showcase-20260927/steer_traj.jpg) [source](https://remilab.ai/rfx/gallery/assets/showcase-20260927/steer_traj.mp4) [Download the trajectory](https://remilab.ai/rfx/gallery/assets/showcase-20260927/steer_traj.mp4). Left: the dielectric cover's relative permittivity. Center: a directional radiation cut. Right: objective sensitivities to latent design variables, ∂L/∂latent, not to physical permittivity ∂L/∂εr. Each frame is a design iteration, not a field time step. ## Read the model | Part | Meaning | |---|---| | Radiator | An electric dipole above a metal reflector | | Design region | A dielectric cover above the source | | Parameterization | A latent grid mapped into a spatial permittivity field | | Measurement | A near-to-far-field transform and an angular directivity cut | | Objective | Concentrate radiation toward the selected direction while penalizing other directions | A radiation cut shows how intensity varies with angle. Directivity compares directional intensity with total radiated power; it does not include every loss or mismatch that would enter realized gain. ## Follow the descent ![Video poster](https://remilab.ai/rfx/gallery/assets/showcase-20260927/steer_descent.jpg) [source](https://remilab.ai/rfx/gallery/assets/showcase-20260927/steer_descent.mp4) [Download the descent view](https://remilab.ai/rfx/gallery/assets/showcase-20260927/steer_descent.mp4). A complementary historical optimization view. Keep its drawn values with its own mesh and parameterization; the presentation's finer-mesh summary and full-cell design are separate results. ## Run the related example The versioned [beam-steering source](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/validation/tmtt_paper/beam_steering_superstrate.py) provides a short CPU setup. Run it from a matching source checkout with the example dependencies installed: ```bash python -m pip install -e ".[optimization]" SMOKE=1 JAX_PLATFORMS=cpu PYTHONPATH=. python validation/tmtt_paper/beam_steering_superstrate.py ``` The short setup uses a coarse mesh and a few optimizer steps. It exercises the source workflow and produces a pattern figure; it does not reproduce the archived latent-variable movie or validate its numerical values. **Try changing:** the initial cover profile or target direction. Check the whole pattern, total-power normalization and collection-surface placement. See [Far-field & RCS](https://remilab.ai/rfx/measure/far-field-and-rcs/) for the measurement and [Gradient behavior](https://remilab.ai/rfx/guide/gradient-behavior/) for differentiation limits. ## Evidence and provenance The exact simulation source revision is not established by the public media bundle. The clips preserve presentation-era results; no new mesh, time-record or independent-solver validation accompanies them. Raw latent histories and field arrays are not included. In particular, a falling objective alone does not establish the accuracy of the final RF design. [Media catalog and SHA-256 hashes](https://remilab.ai/rfx/gallery/assets/showcase-20260927/media-catalog.json) · [Support Boundaries](https://remilab.ai/rfx/support/) · [Visual manual](https://remilab.ai/rfx/gallery/visual-manual/) --- # Let a pulse leave, or reflect it back Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/gallery/boundary-reflection/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/gallery/boundary-reflection.mdx **Current-run teaching example · actual FDTD snapshots · 2D TMz · single mesh** An absorbing boundary represents an open environment. A conducting wall sends waves back into the model. Start the movie and watch the same pulse in both: the open box becomes quiet while reflections keep crossing the closed box. ![Video poster](https://remilab.ai/rfx/gallery/assets/current-visuals/boundary-reflection.png) [source](https://remilab.ai/rfx/gallery/assets/current-visuals/boundary-reflection.mp4) [Download the field movie](https://remilab.ai/rfx/gallery/assets/current-visuals/boundary-reflection.mp4) The lower plot is the electric field at the green probe. The vertical cursor marks the exact simulation time of the frame above. It follows stored field samples; playback does not run a simulation in your browser. ## Read the model | Model detail | Value | |---|---| | Cross-section | 80 × 60 mm vacuum domain | | Solver | Uniform Yee, 2D TMz, 1 mm cells | | Source | Differentiated Gaussian current pulse at (20, 30) mm; `f0=6 GHz`, `bandwidth=0.8` | | Probe | `Ez` at (55, 30) mm | | Left boundary | CPML on the outer edges, 16 absorber layers outside the displayed domain | | Right boundary | PEC on the outer edges | | Recording | 640 steps, one actual snapshot every 4 steps | The colors show signed `Ez / Eref`, with one fixed linear scale for both models and all frames. `Eref` is the largest absolute recorded interior field outside a 4 mm radius around the current source. Source pixels can saturate; the raw values remain in the download. This is a 2D field cross-section, not a finite 3D antenna or cavity. ## Run this example Download the [complete generator](https://remilab.ai/rfx/gallery/assets/current-visuals/field_movies.py). It uses `Simulation`, `SnapshotSpec`, and the physical coordinates and frame times in `Result.snapshot_axes`. It also generates the [dielectric interface](https://remilab.ai/rfx/gallery/dielectric-interface/) and [cavity standing wave](https://remilab.ai/rfx/gallery/cavity-standing-wave/). For the solver version used here: ```bash git clone https://github.com/bk-squared/rfx.git cd rfx git checkout a9054338616b91bb1364699882c2530fa29a0048 python -m pip install -e . curl -fLO https://remilab.ai/rfx/gallery/assets/current-visuals/field_movies.py # Install ffmpeg using your operating system's package manager. JAX_PLATFORMS=cpu PYTHONPATH=. python field_movies.py \ --case boundary-reflection --output field-movies ``` The local output contains the full field arrays, probe CSV, poster, MP4 and manifest. `--render-only` redraws these saved arrays without solving again. The script prints preflight findings before each run and refuses preflight errors. These two vacuum models returned no preflight findings. ## Download and explore - [Probe CSV, in seconds and V/m](https://remilab.ai/rfx/gallery/assets/current-visuals/boundary-reflection.csv) - [Selected field arrays and all probe samples, NPZ](https://remilab.ai/rfx/gallery/assets/current-visuals/boundary-reflection.npz) - [Model, normalization, preflight and checksums, JSON](https://remilab.ai/rfx/gallery/assets/current-visuals/boundary-reflection.json) The small NPZ contains every 16th movie frame at full spatial resolution, with its actual coordinate arrays, step numbers and time stamps. Re-running the generator retains every movie frame locally. Load with `numpy.load(path, allow_pickle=False)`; fields end in `_ez_V_per_m`, coordinates in `_x_m`/`_y_m`, and times in `_frame_times_s`. Try moving the source or changing the domain size, then rerun. Compare when the first reflected wave reaches the same probe. CPML is a finite numerical absorber and can produce residual reflections; this example does not measure its reflection coefficient or an absorption percentage. ## Evidence and limits Generated on the CPU with RFX solver source `a9054338`. The manifest records the full source commit, generator hash and environment. The checks confirm finite fields, recorded frame steps and realized display coordinates. No mesh refinement or accuracy convergence was performed, and no S-parameter, decay rate, or performance claim follows from the movie. Continue with [boundary choices](https://remilab.ai/rfx/support/) or the [API reference](https://remilab.ai/rfx/api/). --- # A pulse leaves a standing wave Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/gallery/cavity-standing-wave/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/gallery/cavity-standing-wave.mdx **Current-run teaching example · actual FDTD snapshots · 2D TMz · single mesh** A closed conducting boundary traps the field. A carrier-centered pulse excites the rectangle; after the drive becomes quiet, its field pattern continues to change sign with nearly fixed nodal lines. Watch the entire domain, then follow the green probe in the trace below. ![Video poster](https://remilab.ai/rfx/gallery/assets/current-visuals/cavity-standing-wave.png) [source](https://remilab.ai/rfx/gallery/assets/current-visuals/cavity-standing-wave.mp4) [Download the cavity movie](https://remilab.ai/rfx/gallery/assets/current-visuals/cavity-standing-wave.mp4) ## From geometry to field | Model detail | Value | |---|---| | Cross-section | 80 × 60 mm vacuum rectangle with PEC walls | | Solver | Uniform Yee, 2D TMz, 1 mm cells | | Excitation | `ModulatedGaussian` current source at the center, bandwidth 0.35 | | Carrier choice | Analytic lowest TMz cavity mode: `c/2 × sqrt(1/a² + 1/b²)` | | Probe | `Ez` at (55, 30) mm | | Recording | 1,600 steps, one actual snapshot every 8 steps | The analytic expression chooses the source carrier; it is **not a resonance frequency extracted from this run**. A point source may excite other modes. This movie does not prove a pure mode or measure a frequency error. The color scale is fixed through the movie: signed `Ez / Eref`, where `Eref` is the largest absolute recorded field outside a 4 mm source neighborhood. Source pixels can saturate. Coordinates and frame times come directly from `Result.snapshot_axes`; these are time-stepped fields, not a harmonic phasor animated afterward. ## Reproduce and change it Use the pinned solver installation and generator download in [the boundary example](https://remilab.ai/rfx/gallery/boundary-reflection/#run-this-example), then: ```bash JAX_PLATFORMS=cpu PYTHONPATH=. python field_movies.py \ --case cavity-standing-wave --output field-movies ``` Change the rectangle's aspect ratio to change its field pattern and source carrier together. Moving the source away from the center changes which modes it excites. Recalculate the model after either edit. For a follow-up calculation that actually extracts modal frequencies, read [the resonance tutorial](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/resonance_harminv.py). Do not estimate a physical Q from the finite window here: vacuum and PEC are lossless, so this is not a lossy ring-down measurement. ## Data and scope - [Probe CSV, seconds and V/m](https://remilab.ai/rfx/gallery/assets/current-visuals/cavity-standing-wave.csv) - [Selected field frames and all probe samples, NPZ](https://remilab.ai/rfx/gallery/assets/current-visuals/cavity-standing-wave.npz) - [Preflight, source settings, normalization and checksums, JSON](https://remilab.ai/rfx/gallery/assets/current-visuals/cavity-standing-wave.json) - [Complete runnable generator](https://remilab.ai/rfx/gallery/assets/current-visuals/field_movies.py) The download retains every 16th actual movie frame with its native spatial coordinates and time stamps. Local regeneration retains all frames. Preflight reported no findings for this vacuum model. Solver source: `a9054338`; full SHA, generator hash and CPU environment are in the manifest. This is an introductory 2D illustration with one mesh and one record length. Finite output and correct snapshot axes were checked. Mesh convergence, modal frequency accuracy and physical Q were not measured. For the distinction between open and closed boundaries, compare [the pulse reflection example](https://remilab.ai/rfx/gallery/boundary-reflection/). --- # Watch a pulse meet a dielectric Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/gallery/dielectric-interface/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/gallery/dielectric-interface.mdx **Current-run teaching example · actual FDTD snapshots · 2D TMz · single mesh** Place a dielectric on the right side of the same open domain. The pulse reaches the interface, produces a reflected part, and propagates differently inside the material. The vacuum reference on the left makes the changed field and probe arrival easier to see. ![Video poster](https://remilab.ai/rfx/gallery/assets/current-visuals/dielectric-interface.png) [source](https://remilab.ai/rfx/gallery/assets/current-visuals/dielectric-interface.mp4) [Download the interface movie](https://remilab.ai/rfx/gallery/assets/current-visuals/dielectric-interface.mp4) **Teaching mesh:** preflight reports resolution advisories for the dielectric at the declared band edge. This movie is qualitative; it does not establish phase-velocity accuracy or reflection/transmission coefficients. ## The model | Model detail | Value | |---|---| | Cross-section | 80 × 60 mm; uniform Yee 2D TMz with 1 mm cells | | Left panel | Vacuum reference | | Right panel | Vacuum for `x < 40 mm`, ideal nondispersive dielectric `εr = 4` for `x ≥ 40 mm` | | Boundary | 16 CPML layers outside the physical domain | | Source | Same differentiated Gaussian current pulse in both models at (20, 30) mm | | Probe | `Ez` at (55, 30) mm | | Recording | 640 steps, actual field snapshot every 4 steps | The dielectric extends through the right absorber and the transverse edges; there is no second material interface in the simulated interior. The dashed line is the declared interface on the 40 mm grid plane. The source is a point in this 2D model, so the wavefront is curved; this is not a normally incident infinite plane wave. Both panels use the same fixed signed `Ez / Eref` scale. `Eref` is the largest absolute recorded field outside a 4 mm source neighborhood across both models. The source pixels can saturate, and all unmodified values remain in the data. The green-probe trace and cursor share the movie's actual simulation time. ## Run and explore Follow the pinned installation and generator download in [the boundary example](https://remilab.ai/rfx/gallery/boundary-reflection/#run-this-example), then: ```bash JAX_PLATFORMS=cpu PYTHONPATH=. python field_movies.py \ --case dielectric-interface --output field-movies ``` Try a different permittivity or move the interface. Rerun preflight and the simulation; a higher permittivity shortens the material wavelength and may require a finer mesh. For a quantitative slab spectrum, continue with the [slab reflection/transmission tutorial](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/slab_rt_flux_monitor.py), which uses flux monitors and reference subtraction. ## Preflight is part of the result The manifest preserves the complete preflight output. Five advisories were reported and retained: - Three resolution findings: the dielectric has 12.5 cells per effective wavelength at the declared 12 GHz band edge, below the 15-cell recommendation. - Geometry reaches into CPML because this example models a continuing half-space. Absorber-region fields are excluded from the plotted interior. - The material is perfectly lossless in an open domain. This example does not extract Q, material loss or an absorption fraction. No advisory was suppressed to make a clean banner. No mesh refinement or record-length convergence claim is made. ## Download the actual samples - [Probe traces, CSV in seconds and V/m](https://remilab.ai/rfx/gallery/assets/current-visuals/dielectric-interface.csv) - [Selected field arrays and all probe samples, NPZ](https://remilab.ai/rfx/gallery/assets/current-visuals/dielectric-interface.npz) - [Model, preflight output, normalization and checksums, JSON](https://remilab.ai/rfx/gallery/assets/current-visuals/dielectric-interface.json) - [Complete generator](https://remilab.ai/rfx/gallery/assets/current-visuals/field_movies.py) The NPZ contains every 16th movie frame at full spatial resolution. Its `*_frame_times_s`, `*_frame_steps`, `*_x_m` and `*_y_m` arrays describe the exact samples. Local regeneration retains every frame. Fields were checked for finite values; displayed coordinates and time steps were checked against `Result.snapshot_axes`. The solver source is `a9054338`; the manifest records the full SHA, generator hash and CPU environment. --- # Visual archive Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/gallery/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/gallery/index.mdx Earlier visual material, kept for reference. Current results are in the [Showcase](https://remilab.ai/rfx/showcase/). Choose a physical question. Each example connects a model to the motion or response it produces, with its source and evidence scope alongside the picture. The site plays stored results; it does not run a solver in your browser. Find a visual example 6 visual examples All examples are shown. Use your browser's Find command to search this page. ## Start with a field These teaching examples were generated for this documentation update using the public Simulation API. They show actual time-domain snapshots on a single mesh; they are not convergence or RF-accuracy benchmarks. [![Electric pulse in open absorbing and closed reflecting domains](https://remilab.ai/rfx/gallery/assets/current-visuals/boundary-reflection.png)](https://remilab.ai/rfx/gallery/boundary-reflection/) Open space or reflecting walls? The same pulsed source with absorbing CPML boundaries and metal PEC walls. [Compare the boundaries →](https://remilab.ai/rfx/gallery/boundary-reflection/) [![Electric pulse encountering a dielectric interface, compared with vacuum](https://remilab.ai/rfx/gallery/assets/current-visuals/dielectric-interface.png)](https://remilab.ai/rfx/gallery/dielectric-interface/) Meet a dielectric interface Watch the field respond when the medium changes, with a vacuum comparison. [Follow the pulse →](https://remilab.ai/rfx/gallery/dielectric-interface/) [![Electric field forming an interference pattern in a closed metal cavity](https://remilab.ai/rfx/gallery/assets/current-visuals/cavity-standing-wave.png)](https://remilab.ai/rfx/gallery/cavity-standing-wave/) Trap a field in a cavity See reflections and interference inside a closed conducting domain. [Watch the cavity →](https://remilab.ai/rfx/gallery/cavity-standing-wave/) ## Explore a design in motion These archived presentation films show more complex structures and optimization histories. Their original numbers are historical; the case pages distinguish phasor replay, real iterations and explanatory interpolation. [![Dielectric taper profile, reflected spectrum and sensitivities](https://remilab.ai/rfx/gallery/assets/showcase-20260927/taper_traj.jpg)](https://remilab.ai/rfx/gallery/waveguide-taper/) 2026 paper, original films Waveguide dielectric taper Connect a graded material profile to broadband reflection and its sensitivity. [Watch the taper →](https://remilab.ai/rfx/gallery/waveguide-taper/) [![Normalized electric field around an open microstrip stub](https://remilab.ai/rfx/gallery/assets/showcase-20260927/notch_phase.jpg)](https://remilab.ai/rfx/gallery/microstrip-notch/) 2026 paper, original films Microstrip notch filter Relate one geometric variable to a resonant branch and its local field. [Watch the filter →](https://remilab.ai/rfx/gallery/microstrip-notch/) [![Permittivity cover and angular radiation pattern during beam steering](https://remilab.ai/rfx/gallery/assets/showcase-20260927/steer_traj.jpg)](https://remilab.ai/rfx/gallery/beam-steering/) 2026 paper, original films Dielectric beam steering Follow a spatial material pattern as the main radiation lobe changes direction. [Watch the beam →](https://remilab.ai/rfx/gallery/beam-steering/) ## Read a reference comparison The earlier [dielectric-slab gallery](https://remilab.ai/rfx/gallery/multilayer_fresnel/) and [empty rectangular-waveguide gallery](https://remilab.ai/rfx/gallery/waveguide_wr90/) retain their field, response and derivative artifacts. Both bundles were computed with rfx 1.6.5 at `1eb551b`. Conductor placement and absorber behavior have changed since that version, so their recorded results are not current-version benchmarks. Read each page's precise limits and measurement definitions. The [patch-antenna diagnostic archive](https://remilab.ai/rfx/gallery/patch_antenna/) is preserved for traceability. Its stored port setup does not pass current preflight; it is not a validated antenna showcase. ## More visual experiments to build These are extensions to develop and validate, not additional films already published. Their source examples provide a starting point. | Physical question | A useful next visualization | Starting point | |---|---|---| | What does a dispersive material do to a pulse? | Synchronized field slices, probe traces and material response | [Materials example](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/materials_and_dispersion.py) | | How does a radiator's near field become a far-field pattern? | Fixed-scale field movie beside the angular radiation cut | [Antenna pattern example](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/antenna_farfield_pattern.py) | | Which field is actually scattered? | Total and incident-subtracted fields beside angular RCS | [Scattering example](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/rcs_scattering.py) | | How do coating layers interfere? | Layer permittivities, a field slice and the full reflection band at the same iteration | [Coating example](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/inverse_design/multilayer_ar_coating.py) | To build your own, follow the [visual manual](https://remilab.ai/rfx/gallery/visual-manual/), choose a [runnable example](https://remilab.ai/rfx/examples/) and keep the model, arrays and plots together. --- # Tune a microstrip notch Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/gallery/microstrip-notch/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/gallery/microstrip-notch.mdx A short open-ended branch attached to a microstrip line acts as a resonant stub. Changing its length changes the frequencies that pass through the line. It is a compact way to connect a geometric design variable, a frequency response and a local field pattern. Historical visualization. The design-path and field films use different stub lengths. They illustrate the same structure family, not one jointly reproduced result. ![Video poster](https://remilab.ai/rfx/gallery/assets/showcase-20260927/notch_phase.jpg) [source](https://remilab.ai/rfx/gallery/assets/showcase-20260927/notch_phase.mp4) [Download the field replay](https://remilab.ai/rfx/gallery/assets/showcase-20260927/notch_phase.mp4). The normalized real part of the complex electric field, Ez, as its phase is advanced. The x/y axes are millimeters. This is a single-frequency DFT phasor replay: it shows relative phase and standing-wave structure, not pulse propagation or arrival time. ## Read the model | Part | Meaning | |---|---| | Structure | A planar microstrip line with an open-ended side branch | | Materials | Conducting trace and ground with a dielectric substrate | | Design variable | The open-stub length | | Measured response | Transmission through the line, S21, over frequency | | Field panel | A normalized Ez slice at one frequency | The field movie uses a 7.22 mm stub. The final spectrum associated with the archived design path uses 7.14 mm. The field panel therefore explains the local mechanism without certifying the final design-path spectrum. ## Follow the design path ![Video poster](https://remilab.ai/rfx/gallery/assets/showcase-20260927/notch_path.jpg) [source](https://remilab.ai/rfx/gallery/assets/showcase-20260927/notch_path.mp4) [Download the design path](https://remilab.ai/rfx/gallery/assets/showcase-20260927/notch_path.mp4). The coarse portion of this path is explanatory interpolation. Only the fine-stage sequence of 60 iterations comes from the recorded optimization history. Intermediate drawn positions do not imply additional electromagnetic solves. ## Explore the related source The versioned [notch research example](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/validation/tmtt_paper/msl_stub_notch_tuning.py) contains the model and design objective. It is a full multi-start research workload: unlike the taper example, it has no short `SMOKE=1` mode. Read its [run notes](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/validation/tmtt_paper/README.md) before executing it from a matching source checkout: ```bash JAX_PLATFORMS=cpu PYTHONPATH=. python validation/tmtt_paper/msl_stub_notch_tuning.py ``` For a smaller first exercise, use [Ports and S-Parameters 101](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/ports_and_sparams_101.py). Keep the excitation, port calculator and reference planes with any new response. **Try changing:** the stub length. Track where the transmission minimum moves across the band, and inspect the full curve rather than a single frequency bin. ## Evidence and provenance These original presentation clips have not been regenerated on the documentation revision. The exact simulation source revision and a complete raw data bundle are not available here. The current source link identifies a related model; running it does not promise byte-for-byte recreation of either film. No current notch-depth, gradient-accuracy or cross-solver agreement is claimed by this page. [Media catalog and SHA-256 hashes](https://remilab.ai/rfx/gallery/assets/showcase-20260927/media-catalog.json) · [Sources & ports](https://remilab.ai/rfx/api/sources-ports/) · [Support Boundaries](https://remilab.ai/rfx/support/) · [Visual manual](https://remilab.ai/rfx/gallery/visual-manual/) --- # Dielectric slab (normal incidence) Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/gallery/multilayer_fresnel/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/gallery/multilayer_fresnel.mdx This page preserves a historical rfx 1.6.5 calculation. For a complete current workflow, use [the runnable flux-monitor example](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/examples/tutorials/slab_rt_flux_monitor.py) and [the reflection/transmission guide](https://remilab.ai/rfx/measure/s-parameters/). They use a reference run and field subtraction; the abbreviated low-level loop below is background for the archived figures. ## What this is A single lossless dielectric slab (εr = 4, refractive index n = 2) of finite thickness d sitting in free space, with a plane wave at normal incidence. Part of the wave reflects off the front face and part transmits through. The reflected and transmitted power follow the exact slab transfer matrix; the slab is transparent (R = 0, T = 1) whenever it is an integer number of half-wavelengths thick — the Fabry–Pérot condition `n·d = m·λ₀/2` for `m = 1, 2, 3, …`. ## What you'll learn - Driving a **plane wave** into a layered medium with a total-field/scattered-field (**TFSF**) source. - Reading **reflection R = |S11|²** and **transmission T = |S21|²** from the scattered and total fields. - Recognising the **Fabry–Pérot** transmission peak where the slab is a half-wave thick. - Cross-checking rfx against the **exact transfer-matrix** solution and confirming **energy conservation** R + T = 1 on a lossless slab. ## Geometry & materials The slab is the only structure; everything else is vacuum. The 1D physics is captured in a thin 2D TMz strip with periodic top/bottom boundaries, so the wave sees an infinite slab at normal incidence. ```python import numpy as np C0 = 299_792_458.0 eps_slab = 4.0 # slab relative permittivity (n = 2) d_slab = 10.0e-3 # slab thickness: 10 mm dx = 1.0e-3 # 1 mm cells -> 10 cells across the slab # half-wave (Fabry-Perot) frequency: n*d = lambda0/2 n_slab = np.sqrt(eps_slab) f_fp = C0 / (2 * n_slab * d_slab) # ~7.5 GHz, slab transparent # -> n = 2.0, first transmission peak near 7.5 GHz ``` A 10 mm slab of n = 2 is a half-wavelength thick near 7.5 GHz, so that is where the reflection vanishes and the slab passes the wave cleanly. ![Dielectric slab cross-section in free space](https://remilab.ai/rfx/gallery/assets/multilayer_fresnel/geometry.png) ## Source, ports, boundaries, mesh - **TFSF plane-wave source.** A total-field/scattered-field boundary injects a clean forward plane wave. The reflected wave is read in the scattered-field region ahead of the slab; the transmitted wave is read in the total-field region behind it. - **CPML on the propagation axis (20 cells).** The wave must leave the domain on both ends without re-entering; a thick absorber keeps the back-reflection well below the ~1% measurement floor across the whole band. - **Periodic transverse boundaries.** A normal-incidence plane wave has no transverse variation, so one cell across the width with periodic walls reproduces the infinite-slab problem at a fraction of the cost. - **10 cells across the slab.** At 1 mm cells the 10 mm slab is resolved with ten cells, and the half-wave field variation inside it is well sampled up to the top of the band. Because the geometry is one-dimensional, this whole case runs in seconds on a CPU — no GPU required. ## Geometry check (before running) The cross-section above confirms the setup before committing compute: a single 10 mm slab centred in free space, with the incident wave from the left, the reflection heading back, and the transmission continuing through. The probes sit in clear air on either side, away from the absorber. A 600-cell strip run over a few hundred steps finishes in a couple of seconds on a CPU; peak memory is a few megabytes. ## Run A broadband Gaussian plane wave covers roughly 3–12 GHz in one shot. The scattered field ahead of the slab gives R; the total field behind it gives T. ```python from rfx.grid import Grid from rfx.core.yee import init_state, init_materials, update_e, update_h from rfx.boundaries.cpml import init_cpml, apply_cpml_e, apply_cpml_h from rfx.sources.tfsf import ( init_tfsf, update_tfsf_1d_h, update_tfsf_1d_e, apply_tfsf_e, apply_tfsf_h, ) grid = Grid(freq_max=20e9, domain=(600 * dx, 0.004, dx), dx=dx, cpml_layers=20, mode="2d_tmz") dt = grid.dt # broadband plane wave, +x, Ez-polarised tfsf_cfg, tfsf_st = init_tfsf(grid.nx, dx, dt, cpml_layers=20, tfsf_margin=5, f0=10e9, bandwidth=0.5, polarization="ez", direction="+x", ny=grid.ny, nz=grid.nz) # stamp the slab into the permittivity slab_lo = grid.nx // 2 - int(d_slab / (2 * dx)) slab_hi = grid.nx // 2 + int(d_slab / (2 * dx)) materials = init_materials(grid.shape) materials = materials._replace( eps_r=materials.eps_r.at[slab_lo:slab_hi, :, :].set(eps_slab)) state = init_state(grid.shape) cp, cs = init_cpml(grid) # ... step the Yee update, record E_z at a reflection probe and a # transmission probe, then FFT to get R(f) and T(f) ... ``` Run it from the command line: ```bash python scripts/precompute_gallery_artifacts.py --case multilayer_fresnel ``` ## Fields The space-time map below shows E_z along the propagation axis (horizontal) as time advances (vertical). The incident pulse travels at c through the air and hits the front face of the slab. There it splits: part reflects (the branch that turns back toward the source) and part transmits (the branch that continues forward, with a kink in slope inside the slab where the wave slows to c/n). The two outgoing branches are the R and T measured below. ![E_z space-time map of the incident, reflected and transmitted pulse](https://remilab.ai/rfx/gallery/assets/multilayer_fresnel/field_xt.png) The same run played back in time: the plane-wave pulse travels in at `c`, hits the slab front face, and splits into a reflected pulse heading back and a transmitted pulse continuing forward (slowed to `c/n` inside the slab). ![Animation: a plane-wave pulse hitting the slab — incidence, reflection and transmission](https://remilab.ai/rfx/gallery/assets/multilayer_fresnel/field_anim.gif) ## Result Reflected and transmitted power versus frequency, with the rfx FDTD points laid over the exact transfer-matrix curves. Transmission peaks to 1 (and reflection drops to 0) at the Fabry–Pérot half-wave near 7.5 GHz, exactly where the slab is a half-wavelength thick. Reflection rises toward the band edges where the slab is electrically thinner or thicker than a half-wave. ![Reflection and transmission vs frequency, rfx over exact Fresnel](https://remilab.ai/rfx/gallery/assets/multilayer_fresnel/rt_overlay.png) [S-parameter data (Touchstone .s2p)](https://remilab.ai/rfx/gallery/assets/multilayer_fresnel/sparams.s2p) ## Validation & limits The rfx R/T track the exact Fresnel curves across the full 3–12 GHz band: | Quantity | rfx vs exact | Note | |---|---|---| | Reflection R | mean \|R_rfx − R_exact\| ≈ 0.007 | absolute power error | | Transmission T | mean \|T_rfx − T_exact\| ≈ 0.011 | absolute power error | | Energy R + T | mean \|R + T − 1\| ≈ 0.009 | lossless: should be 1 | Reference: the exact transfer-matrix (Fresnel) solution. Both errors sit near 1%, inside the 5% bound this case is held to (`mean |R/T error| < 0.05` and `|R + T − 1| < 0.05`). R + T comes back to unity, as it must on a lossless slab — an independent check that no energy is created or lost at the interfaces. **Where it drifts.** The largest deviation is near the top of the band (above ~9 GHz), where the cells are coarsest relative to the wavelength and numerical dispersion shifts the FDTD phase velocity slightly. Refining the mesh from 1 mm toward 0.5 mm pulls the high-frequency points back onto the analytic curve; the half-wave peak near 7.5 GHz is already converged at 1 mm. The model is lossless and normal-incidence only — oblique incidence and material loss would each need their own setup. ## Autodiff The slab is the cleanest place to see what makes rfx different: `jax.grad` flows straight through the FDTD time stepping. Here the design variable is the slab thickness `d` and the scalar of interest is the transmission `T = |S21|²` at one frequency. The thickness enters the permittivity as a soft top-hat — a smoothed slab edge that is a continuous function of `d` — so a single `jax.value_and_grad` call returns both `T(d)` and the sensitivity `dT/dd`, with the gradient computed by differentiating automatically back through every Yee update of the run. The slab transfer matrix gives `T(d)` in closed form, so the derivative has an exact analytic expression: `T(d) = 1 / (1 + F·sin²δ)` with `F = ((n − 1/n)²)/4` and `δ = 2·π·f·n·d/c`, giving `dT/dd = −T²·F·sin(2δ)·(2·π·f·n/c)`. Because the closed form exists, the AD gradient can be checked against the **exact derivative** directly. Two numbers are reported, because they answer two different questions: - **AD vs central finite differences** on the same discretised forward — is the AD machinery itself correct? Here `0.00013 %` (rel. error 1.3e-06). The gradient `jax.grad` returns is the exact derivative of the simulation it ran. - **AD vs the continuum analytic derivative** — has the FDTD physics converged to the textbook slab? Here `2.6 %` at 0.25 mm cells. The residual is numerical (Yee phase-velocity dispersion plus the one-cell soft edge) and shrinks as the mesh is refined. | Check | AD value | Compared against | rel. error | Sign agrees | |---|---|---|---|---| | AD vs central finite difference | `dT/dd = 64.7725` | central FD `64.7724` (h = 5e-5) | 1.3e-06 (0.00013%) | yes | | AD vs closed-form derivative | `dT/dd = 64.7725` | analytic `dT/dd` | 0.0261 (2.6%) | yes | Both land inside the 5% agreement asked of them. The 0.00013% row says the AD gradient is the exact derivative of the simulation that ran; the 2.6% row says how far that simulation sits from the textbook slab at 0.25 mm cells — a mesh question, not a gradient question. ![Slab transmission sensitivity dT/dd — AD markers on the exact derivative, and T(d) with its gradient through a Fabry-Perot half-period](https://remilab.ai/rfx/gallery/assets/multilayer_fresnel/autodiff.png) The left panel lays the AD markers on the exact `dT/dd` curve across a Fabry-Pérot half-period; they sit on the analytic line. The right panel shows `T(d)` and `dT/dd` together: the gradient passes through zero exactly at the half-wave transmission peak, where `T → 1` and the slab is momentarily insensitive to a small change in thickness. The working point is chosen on a steep mid-slope (`δ ≈ 2` rad), not at the peak, where the relative comparison is well posed. The differentiable knob here is a material/thickness quantity. The number of mesh cells across the slab is a discrete count and is not differentiable; thickness is made smooth on purpose, through the soft top-hat, so that `d` can be a continuous input to the solver. ## Next [Patch antenna diagnostics](https://remilab.ai/rfx/gallery/patch_antenna/) Computed with rfx 1.6.5 · 1eb551b · generated 2026-06-30T03:19:53Z --- # Patch Antenna Diagnostics Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/gallery/patch_antenna/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/gallery/patch_antenna.mdx ## Status These files are for **diagnosis**, not a validated patch-antenna result. Do not cite the plotted S11 dip, field pattern, resonance label, match, Q, or convergence as RF evidence. The artifact-generating port configuration does not pass the current preflight: the wire-port midpoint overlaps PEC, the feed is reported in PEC, the conductor and feed gap are under-resolved, and the generator bypassed those findings with `skip_preflight=True`. A finite S11 array or an AD/FD agreement does not repair that physical setup. ## What the files show The bundle records a finite-ground FR4 patch geometry, a one-port frequency sweep, field visualizations, Touchstone export, and a material-permittivity AD/finite-difference comparison. The assets are retained to inspect file formats and plotting behavior: - `geometry.png` — rendered geometry from the artifact setup; - `field_resonance.png` and `field_anim.gif` — field visualizations from a separate soft-source run; - `s11_db.png`, `sparams.json`, and `sparams.s1p` — output from the port setup that fails current preflight; - `autodiff.png` and `gradient.json` — internal derivative consistency for that same numerical objective. The soft-source field run and the port sweep are separate simulations. Their images do not establish that a return-loss dip and a ring-down mode came from one self-consistent run. No committed mesh and absorber convergence study supports the displayed frequency. ![Patch geometry diagnostic](https://remilab.ai/rfx/gallery/assets/patch_antenna/geometry.png) ![Patch S11 diagnostic from a setup that fails preflight](https://remilab.ai/rfx/gallery/assets/patch_antenna/s11_db.png) [Diagnostic Touchstone file](https://remilab.ai/rfx/gallery/assets/patch_antenna/sparams.s1p) ## Interpreting the AD/FD panel The stored AD and central finite-difference values agree for the implemented objective. This checks differentiation of that computation only. Because the port geometry fails current preflight, the derivative must not be described as patch sensitivity, matching sensitivity, or an antenna-design gradient. ![Internal AD/FD consistency diagnostic](https://remilab.ai/rfx/gallery/assets/patch_antenna/autodiff.png) ## Use a supported path - [Your First Patch Antenna](https://remilab.ai/rfx/examples/first-patch/) gives a model that passes preflight and separates a short execution check from RF evidence. - [Cross-Validation and Accuracy](https://remilab.ai/rfx/guide/validation/) defines the evidence required before publishing resonance, S-parameter, or Q claims. Computed with rfx 1.6.5 · 1eb551b · generated 2026-06-30T03:21:23Z --- # The visual manual Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/gallery/visual-manual/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/gallery/visual-manual.mdx A useful simulation has three views: **the model you built, the fields it produces, and the quantity you want to measure**. Keep those views together as you learn rfx. ## 1. Build the physical model Choose a structure, material and excitation. Open radiation problems use absorbing boundaries; a closed conducting cavity uses reflecting metal walls. The boundary changes the physical problem, not just how a plot ends. Begin with [Quick Start](https://remilab.ai/rfx/start/first-simulation/), [Core Concepts](https://remilab.ai/rfx/start/concepts/) and [Materials & geometry](https://remilab.ai/rfx/model/materials/). Run preflight before the simulation and inspect the realized mesh: a drawn dimension need not land exactly on a grid plane. ## 2. Watch the fields Read the field component, axes and color scale before reading the motion. A field slice shows one component on one plane; it is not the entire three-dimensional solution. Keep the color scale fixed when comparing times or designs. | Animation type | What moves | What it can explain | |---|---|---| | Time-domain snapshots | Recorded fields at successive simulation times | Propagation, reflection, transient decay and interference | | Phasor replay | The phase of a single-frequency complex field | Standing-wave structure and relative phase; it does not show pulse arrival | | Design history | The geometry or material and response at each optimization step | How a design changes; interpolated frames do not add simulation samples | | Stored parameter sweep | A selection of precomputed parameter values | Trends across sampled settings; the browser does not rerun FDTD | All new video players start on a still image. Press play when you want motion; pause or use the timeline to compare frames. The captions describe the important features without requiring animation. ![Video poster](https://remilab.ai/rfx/gallery/assets/current-visuals/boundary-reflection.png) [source](https://remilab.ai/rfx/gallery/assets/current-visuals/boundary-reflection.mp4) [Download the boundary comparison](https://remilab.ai/rfx/gallery/assets/current-visuals/boundary-reflection.mp4). The source and probe stay in the same positions. Absorbing boundaries let the pulse leave; metal walls reflect it and the probe continues to see returning fields. A current-run, single-mesh 2D TMz teaching example. [Read the model, data and limits →](https://remilab.ai/rfx/gallery/boundary-reflection/) ## 3. Measure the quantity you need A bright field is not by itself a power, efficiency or gain measurement. Use [flux monitors](https://remilab.ai/rfx/measure/s-parameters/) for reflected/transmitted power, the [matching port calculator](https://remilab.ai/rfx/api/sources-ports/) for S-parameters, and [near-to-far-field analysis](https://remilab.ai/rfx/measure/far-field-and-rcs/) for radiation or scattering. Keep the reference run, normalization and measurement planes with the result. ## Waves and materials Follow a pulse through a [dielectric interface](https://remilab.ai/rfx/gallery/dielectric-interface/) or watch interference in a [closed cavity](https://remilab.ai/rfx/gallery/cavity-standing-wave/). Both provide actual time-domain snapshots and downloadable arrays generated from their public source. They teach field behavior on one mesh, not numerical accuracy. Then read the [dielectric slab](https://remilab.ai/rfx/gallery/multilayer_fresnel/): see how the two interfaces create reflection and transmission, then run the [flux-monitor tutorial](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/slab_rt_flux_monitor.py). The linked legacy gallery records an older calculation; use its stated version when reading the comparison. **Try changing:** slab permittivity or thickness, one at a time. Plot the full frequency response and compare with a lossless slab's energy balance or its analytic reference, using the same model and reference planes. ## Guided waves and filters Compare the [empty rectangular guide](https://remilab.ai/rfx/gallery/waveguide_wr90/) with the [dielectric taper](https://remilab.ai/rfx/gallery/waveguide-taper/). The first introduces a guided mode; the second adds a material transition and a reflected-wave objective. The [microstrip notch](https://remilab.ai/rfx/gallery/microstrip-notch/) makes a compact next step: one length controls a resonant branch. **Try changing:** the taper's material profile or the stub length. Keep the port definition and normalization fixed so the curves answer the same question. ## Radiation and scattering The [beam-steering film](https://remilab.ai/rfx/gallery/beam-steering/) connects a dielectric cover to a far-field pattern. Then use the runnable [antenna pattern example](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/antenna_farfield_pattern.py) or [scattering example](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/examples/tutorials/rcs_scattering.py) from the source checkout. **Try changing:** the source orientation before adding structural complexity. Check the collection surface, clearance and incident-reference subtraction. Directivity, realized gain and efficiency are different quantities. ## Differentiable design Read each optimization frame from left to right: parameter field, measured response, then sensitivity. A derivative refers to a particular parameterization and objective. The [beam-steering film](https://remilab.ai/rfx/gallery/beam-steering/) differentiates latent variables; the [taper film](https://remilab.ai/rfx/gallery/waveguide-taper/) displays section permittivity sensitivities. Before using a gradient to support a design claim, check both finite-difference agreement and stability with a longer time record. See [Gradient behavior](https://remilab.ai/rfx/guide/gradient-behavior/) and the [inverse-design guide](https://remilab.ai/rfx/design/optimization/). ## 4. Make the result reproducible Record the source revision, inputs, realized geometry and mesh, preflight output, measurement definition, and raw arrays. Put the plot, movie and response curve under the same model identity. If two panels came from different runs, say so. The [Examples hub](https://remilab.ai/rfx/examples/) leads to runnable scripts. The [API reference](https://remilab.ai/rfx/api/) describes the calls; [Support Boundaries](https://remilab.ai/rfx/support/) and [Validation](https://remilab.ai/rfx/validation/) explain the scope of a result. --- # A dielectric taper for a guided wave Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/gallery/waveguide-taper/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/gallery/waveguide-taper.mdx A guided wave meets a higher-permittivity dielectric load. A transition made of short dielectric sections gives the wave a gradual change of environment. This example asks how the section permittivities change the reflected-wave spectrum. Historical design visualization. These films preserve a presentation-era run. They have not been regenerated with the documentation source revision and do not establish current RF accuracy. ![Video poster](https://remilab.ai/rfx/gallery/assets/showcase-20260927/taper_traj.jpg) [source](https://remilab.ai/rfx/gallery/assets/showcase-20260927/taper_traj.mp4) [Download the trajectory](https://remilab.ai/rfx/gallery/assets/showcase-20260927/taper_traj.mp4). Read left to right: relative permittivity along the guide, reflection magnitude across frequency, and the objective's sensitivity to each section's permittivity. The movie advances through optimization iterations, not physical time. ## Read the model | Part | Meaning | |---|---| | Guide | A rectangular metal-walled guide with absorbing ends along propagation | | Load | A dielectric-filled continuation of the guide | | Design | A sequence of dielectric sections ahead of the load | | Excitation and measurement | The guided mode and its reflected port response | | Objective | Band-averaged reflected power | The current [research source](https://github.com/bk-squared/rfx/blob/a9054338616b91bb1364699882c2530fa29a0048/validation/tmtt_paper/waveguide_dielectric_taper.py) distinguishes its geometries: the short CPU setup realizes WR-90 dimensions, while the paper meshes realize a 23 mm broad wall rather than the nominal 22.86 mm. Its reported paper responses belong to those realized guides. The legacy [empty-guide example](https://remilab.ai/rfx/gallery/waveguide_wr90/) is a different model. ## Watch the objective evolve ![Video poster](https://remilab.ai/rfx/gallery/assets/showcase-20260927/taper_descent.jpg) [source](https://remilab.ai/rfx/gallery/assets/showcase-20260927/taper_descent.mp4) [Download the descent view](https://remilab.ai/rfx/gallery/assets/showcase-20260927/taper_descent.mp4). A complementary view of the recorded design history. The comparison curve is part of the historical figure; this page makes no claim of superiority over a classical taper. ## Run the related example In a source checkout matching the linked revision, install the project's example dependencies and run its documented CPU setup: ```bash python -m pip install -e ".[optimization]" SMOKE=1 JAX_PLATFORMS=cpu PYTHONPATH=. python validation/tmtt_paper/waveguide_dielectric_taper.py ``` This short run produces an initial/optimized response and material-profile figure. It exercises the workflow; it does not reproduce these archived movies or the full-resolution paper result. Read its preflight output and realized dimensions. **Try changing:** the initial permittivity profile or number of sections. Compare the complete reflection curve and keep the guide, ports and frequency band fixed. See [Waveguide Ports](https://remilab.ai/rfx/guide/waveguide-ports/) and [Inverse Design](https://remilab.ai/rfx/design/optimization/) for the API path. ## Evidence and provenance The videos show a recorded optimization and retain the numbers drawn in their original frames. Their exact simulation source revision is unavailable in the public media bundle. The linked source is a related, versioned example, not a reproduction certificate for these assets. Raw iteration arrays are not included. Current accuracy and differentiation support follow [Support Boundaries](https://remilab.ai/rfx/support/), including mesh and time-record checks before making a new numerical claim. [Download the media catalog and SHA-256 hashes](https://remilab.ai/rfx/gallery/assets/showcase-20260927/media-catalog.json). Next: [tune a microstrip notch](https://remilab.ai/rfx/gallery/microstrip-notch/) or [read the visual manual](https://remilab.ai/rfx/gallery/visual-manual/). --- # Hollow rectangular waveguide (WR-90, TE10) Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/gallery/waveguide_wr90/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/gallery/waveguide_wr90.mdx A rectangular waveguide is a hollow metal pipe that carries one electromagnetic mode at a time above a cutoff frequency. This case drives the dominant TE10 mode down a standard WR-90 guide (X-band) and reads the two-port S-parameters. An ideal empty guide should pass the mode with almost no reflection — and because the matched empty-guide answer is known in closed form, it is a clean absolute check on the waveguide port. ## The structure ![The hollow rectangular waveguide](https://remilab.ai/rfx/gallery/assets/waveguide_wr90/geometry.png) A WR-90 guide: broad wall a = 22.86 mm, narrow wall b = 10.16 mm, metal (PEC) walls, hollow air inside. The mode is launched at the left port and collected at the right port. Both end faces are absorbing (CPML); the four side walls are PEC. ## What this is The dominant mode of a rectangular guide is TE10. It only propagates above the cutoff frequency set by the broad-wall width: `f_c = c / (2·a)`. For WR-90, `a = 22.86 mm` gives `f_c ≈ 6.557 GHz`, so the X-band (8.2–12.4 GHz) sits comfortably above cutoff and the TE10 mode travels cleanly. Below `f_c` the mode is evanescent and the guide rejects it. Filling the guide with a dielectric `εr` lowers the cutoff to `f_c,d = f_c / √εr` — the lever the autodiff section below differentiates through. ## What you'll learn - Launching the **TE10 dominant mode** with a mode-matched waveguide port (a one-sided mode injection, not a soft point source). - The **cutoff relation** `f_c = c/(2·a)` and why X-band propagates in WR-90 while lower frequencies would not. - Reading two-port **|S11| (reflection) and |S21| (transmission)** against the exact matched empty-guide answer (`|S11| = 0`, `|S21| = 1`). - Taking a **gradient through the FDTD solver** with `jax.grad`: how `|S21|` responds to the guide's dielectric fill, validated against finite differences. ## Geometry & materials ```python import numpy as np C0 = 299_792_458.0 a_wg = 22.86e-3 # WR-90 broad wall (sets the TE10 cutoff) b_wg = 10.16e-3 # WR-90 narrow wall f_c = C0 / (2 * a_wg) # TE10 cutoff: c / (2a) -> 6.557 GHz dx = 1.0e-3 # 1 mm cells -> ~30 cells/wavelength at 10 GHz freqs = np.linspace(8.2e9, 12.4e9, 21) # X-band sweep ``` The guide is hollow air inside PEC walls. The only material knob is the optional dielectric fill used in the gradient demonstration below. ## Source, ports, boundaries, mesh - **Two TE10 waveguide ports.** The left port (`direction="+x"`) launches the mode; the right port (`direction="-x"`) acts as a matched load. Each port injects the discrete Yee-grid mode profile, so the launcher back-reflection is minimal. - **CPML on the propagation axis only (20 cells).** Twenty layers keep the absorber's residual reflection of the guided mode to essentially zero (`|S11| ≈ 0`); a thinner absorber leaves a larger floor. Only the two x faces absorb. - **PEC side walls.** The four y/z walls are PEC — that is what defines a rectangular guide and its cutoff. - **Uniform 1 mm mesh.** Ten cells span the narrow wall, about 23 the broad wall, and roughly 30 cells per wavelength at the top of the band — enough to resolve the TE10 half-sine across `a` and keep numerical dispersion small. With the thick 20-layer CPML the empty-guide |S11| sits at essentially zero. With fewer layers the floor rises. That is absorber back-reflection, not a solver error: above cutoff the guide is nearly lossless, so any residual reflection is set by the CPML thickness. ## Geometry check (before running) The render above confirms the guide before committing compute: broad wall a, narrow wall b, the two port planes set back from the absorbers, and clearance to the CPML on both ends. An empty WR-90 run at 1 mm (about 46k cells, a few hundred steps) finishes in seconds on a CPU; peak memory is a few tens of megabytes. ## Run ```python import numpy as np, jax.numpy as jnp from rfx.api import Simulation from rfx.boundaries.spec import Boundary, BoundarySpec a_wg, b_wg, dx = 22.86e-3, 10.16e-3, 1.0e-3 freqs = np.linspace(8.2e9, 12.4e9, 21) f0 = float(freqs.mean()); bandwidth = 0.5 domain_x = 0.200 sim = Simulation( freq_max=float(freqs[-1]) * 1.1, domain=(domain_x, a_wg, b_wg), dx=dx, boundary=BoundarySpec(x=Boundary(lo="cpml", hi="cpml"), y=Boundary(lo="pec", hi="pec"), z=Boundary(lo="pec", hi="pec")), cpml_layers=20, ) port_freqs = jnp.asarray(freqs) sim.add_waveguide_port(0.040, direction="+x", mode=(1, 0), mode_type="TE", freqs=port_freqs, f0=f0, bandwidth=bandwidth, waveform="modulated_gaussian", reference_plane=0.050, name="left") sim.add_waveguide_port(domain_x - 0.040, direction="-x", mode=(1, 0), mode_type="TE", freqs=port_freqs, f0=f0, bandwidth=bandwidth, waveform="modulated_gaussian", reference_plane=domain_x - 0.050, name="right") result = sim.compute_waveguide_s_matrix(num_periods=200, normalize=True) ``` Run it from the command line: ```bash python scripts/precompute_gallery_artifacts.py --case waveguide_wr90 ``` ## Fields E_z on the broad-wall mid-plane of a source-fed empty guide, captured once the pulse has propagated to mid-guide. The field is a half-sine across the broad wall — peaking at the centre and vanishing at the PEC side walls — travelling in the +x direction, with the next half-cycle of opposite sign following behind. That is the TE10 mode moving down the guide. ![TE10 mode E_z on the broad-wall mid-plane of the WR-90 guide](https://remilab.ai/rfx/gallery/assets/waveguide_wr90/field_te10.png) The same mode in motion: the TE10 wave packet propagates in the +x direction down the guide and into the matched absorber, holding its half-sine profile across the broad wall as it travels. ![Animation: the TE10 mode travelling down the WR-90 guide](https://remilab.ai/rfx/gallery/assets/waveguide_wr90/field_anim.gif) ## Result Across the X-band |S21| sits at about 0 dB and `max|S11| = 0.0000` with the 20-layer CPML. An empty matched guide above cutoff should pass the TE10 mode with no reflection, and it does. ![Reflection and transmission vs frequency for the empty WR-90 guide](https://remilab.ai/rfx/gallery/assets/waveguide_wr90/sparams.png) [S-parameter data (Touchstone .s2p)](https://remilab.ai/rfx/gallery/assets/waveguide_wr90/sparams.s2p) ## Validation & limits The empty matched guide has an exact answer: `|S11| = 0` and `|S21| = 1` at every frequency above cutoff. The figure overlays the rfx values on that reference. | Observable | rfx | Reference | Tolerance | Pass | |---|---|---|---|---| | max\|S11\| | 0.0000 | 0 (analytic matched-load) | `< 0.02` | yes | | min\|S21\| | 1.0000 | 1 (analytic matched-load) | `> 0.97` | yes | ![|S11| and |S21| vs the exact matched empty-guide answer](https://remilab.ai/rfx/gallery/assets/waveguide_wr90/validation.png) Reference: analytic matched-load (`|S11| = 0`, `|S21| = 1` above cutoff), cross-checked against MEEP/OpenEMS/Palace. **Limit — absorber floor.** The residual `|S11|` is CPML back-reflection, not a solver error. The 20-layer absorber holds it at the `≈0` published here; a thinner absorber raises the floor. That floor tracks the absorber thickness, not the guide's reflection. These assets predate the 2.0 absorber correction, which lowered the CPML's reflection at every layer count. ## Autodiff Fill part of the guide with a dielectric and `|S21|` responds, and that response is differentiable. The design variable is the fill permittivity `εr`, entered through `eps_override`, and the scalar is `|S21|²`. A single `jax.value_and_grad` call returns `∂|S21|²/∂εr` straight through `compute_waveguide_s_matrix( normalize="flux", ...)` — the power-flux extraction is on the AD tape, so the gradient flows through the full modal S-matrix assembly. There is no closed form for the filled-guide `|S21|`, so the AD gradient is compared against central finite differences (`h = 0.05`), asking for agreement within 5% and a matching sign. | Check | AD value | Compared against | rel. error | Sign agrees | |---|---|---|---|---| | `∂\|S21\|²/∂εr` | `−0.2201` | central FD `−0.2197` (h = 0.05) | 0.0017 (0.17%) | yes | The gradient agrees to 0.17% with matching sign, inside the 5% this check asks for. It checks the differentiation of this one computation, not the whole waveguide family. The waveguide S-matrix is **limited** in the [support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/support_matrix.md): its gradient checks were closed with 64-bit JAX on the flux lane, and this 0.17% is a float32 gradient, which sits outside that closure. ![Transmission sensitivity to the dielectric fill — AD gradient vs central finite differences](https://remilab.ai/rfx/gallery/assets/waveguide_wr90/autodiff.png) The sign is physical: a dielectric fill lowers the cutoff toward the band through `f_c,d = f_c / √εr` (so `∂f_c,d/∂εr = −f_c / (2·εr^{3/2})`, about `−1.8 GHz`), which moves the band edge and reduces transmission. The closed-form cutoff relation points the same way the solver gradient does. **Limits.** The broad-wall width `a` is **not** a `jax.grad` input: `a/dx` is a discrete cell count and the analytic mode profile is rebuilt for each width, so its sensitivity comes from re-meshing and finite differences, alongside the analytic cutoff relation `f_c = c/(2·a)` — not from automatic differentiation. The differentiable lanes are `normalize=False` and `normalize="flux"`. `normalize=True` is the host-assembled two-run lane and is outside the differentiable chain: under `jax.grad`/`jax.jit`, a traced `eps_override`/`sigma_override` makes `compute_waveguide_s_matrix` raise `NotImplementedError` before any FDTD run. The Run block above passes a concrete `normalize=True` and runs forward; the same call under `jax.grad` raises. ## Next [Dielectric slab (normal incidence)](https://remilab.ai/rfx/gallery/multilayer_fresnel/) Computed with rfx 1.6.5 · 1eb551b · generated 2026-06-30T03:22:31Z --- # ADI Solver (Experimental) Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/guide/adi-solver/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/guide/adi-solver.mdx `solver="adi"` is an implicit time-stepping scheme that can take steps larger than the usual Courant limit. It is **experimental**. This page shows when it helps, what the larger step costs in accuracy, and what it cannot model. ## Why it exists The default `solver="yee"` is explicit. Its time step is set by the smallest cell anywhere in the mesh, so one thin layer or narrow gap forces tiny steps on the whole model. ADI (the Zheng–Chen–Zhang two-sub-step scheme) removes that stability limit, and `adi_cfl_factor` sets how many times larger than the Yee step it goes. Stability is free; accuracy is not. ADI adds a dispersion error that grows roughly as the square of the step. `adi_cfl_factor` is a speed setting, not an accuracy setting. ## Example: a PEC cavity three ways This resonates an empty 24 × 12 × 36 mm PEC cavity with explicit Yee and with ADI at factors 2 and 5, and compares the TE101 frequency with the closed form: ```python import numpy as np from rfx import GaussianPulse, Simulation C0 = 299_792_458.0 a, b, d = 24e-3, 12e-3, 36e-3 f_te101 = C0 / 2 * np.sqrt((1 / a) ** 2 + (1 / d) ** 2) def cavity(solver, factor=2.0): sim = Simulation(freq_max=12e9, domain=(a, b, d), dx=1.5e-3, boundary="pec", solver=solver, adi_cfl_factor=factor) sim.add_source((0.37 * a, 0.41 * b, 0.20 * d), "ey", waveform=GaussianPulse(f0=f_te101, bandwidth=0.9), amplitude_kind="field") # ADI implements field sources only sim.add_probe((0.63 * a, 0.58 * b, 0.80 * d), "ey") return sim for label, solver, factor in [("yee", "yee", 1.0), ("adi x2", "adi", 2.0), ("adi x5", "adi", 5.0)]: sim = cavity(solver, factor) dt = sim.run(n_steps=2, skip_preflight=True).dt n_steps = int(6.9e-9 / dt) # the same 6.9 ns record each time result = sim.run(n_steps=n_steps) modes = result.find_resonances(freq_range=(0.7 * f_te101, 1.5 * f_te101)) te101 = min(modes, key=lambda m: abs(m.freq - f_te101)) err = 100 * (te101.freq - f_te101) / f_te101 print(f"{label}: {n_steps} steps, TE101 error {err:+.2f}%") ``` The ADI runs take a half and a fifth of the Yee steps for the same record, and their TE101 error grows with the factor. The factor-5 run triggers the `adi_3d_accuracy` preflight advisory. ## Choosing `adi_cfl_factor` The committed check holds the 3-D lane at the default factor 2 within 2 % of the analytic eigenfrequency of an empty PEC cavity (`tests/unit/misc/test_review_tier1_validation_battery.py::test_optc1_adi_3d_cavity_eigenfrequency`). Larger factors are not judged; the error grows with the factor and shrinks with finer cells. **Use `adi_cfl_factor <= 2` when a frequency must be accurate.** The default is 2; a larger factor trades accuracy for larger steps on stiff meshes. ## What ADI can model ADI runs a small subset of rfx: - `mode="3d"` or `mode="2d_tmz"`, on a uniform mesh; - `boundary="pec"` only. Every absorbing boundary is refused when the `Simulation` is built, and the default boundary is `"cpml"`, so pass `boundary="pec"` explicitly; - a uniform, non-dispersive dielectric filling the whole domain. A dielectric or conductivity interface is refused; - soft sources with `amplitude_kind="field"` (`add_source`) and point probes. It refuses everything else before the first step: interior conductors of any kind (volumes, sheets, wires), ports, TFSF, waveguide, coaxial and Floquet ports, lumped RLC elements, dispersive materials, DFT planes, NTFF boxes, graded meshes and subgridding. `run()` also refuses `until_decay`, snapshots, checkpointing, subpixel smoothing and `conformal_pec`. **Interior PEC is refused at every factor.** A conductor inside the domain made the ADI fields grow without bound, even at factor 1. Preflight reports `adi_interior_pec_unsupported`, and `run()` and `forward()` raise even if you skip preflight. A PEC domain boundary is fine. For a model with metal inside, use `solver="yee"`. `solver="adi"` is differentiable: `jax.grad` flows through the implicit solves. ## Limits - **Experimental.** The 3-D lane is checked against an analytic PEC cavity (within 2 % at factor 2), and the 2-D TMz lane against a cavity resonance at factor 5. Neither contains a conductor or material interface, and ADI refuses both. - **No demonstrated speed-up.** The larger step is allowed, but no measured end-to-end speed and accuracy gain on a real stiff mesh has been published. Do not assume ADI is faster for your model. - **Accuracy falls with the factor.** Keep `adi_cfl_factor <= 2` for frequency-accurate results, or use Yee. - **No interior metal, ports or S-parameters.** Use `solver="yee"` for antennas, circuits and anything you will publish. - Status and scope: see the [support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/support_matrix.md). For a longer walk-through, run `examples/tutorials/adi_solver_demo.py` from a source checkout. --- # Simulation API Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/guide/api-reference/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/guide/api-reference.mdx This page is a map of the calls you use in almost every model and the objects they return. Each row links to the API page with the full signature and arguments. For every public symbol, see the [generated reference](https://remilab.ai/rfx/api/generated/). ## The workflow in calls ```text sim = Simulation(freq_max, domain, dx=..., boundary="cpml") # domain and mesh sim.add_material(...); sim.add(shape, material=...) # materials, geometry sim.add_source(...) or sim.add_port(...) # excitation sim.add_probe(...); sim.add_flux_monitor(...) # observables sim.preflight() # check result = sim.run(n_steps=...) # solve -> Result ``` ## Building a model | Call | Does | Reference | |---|---|---| | `Simulation(freq_max, domain, *, dx, boundary, cpml_layers, dz_profile, ...)` | domain, mesh, boundaries | [Simulation](https://remilab.ai/rfx/api/simulation/#constructor) | | `add_material(name, *, eps_r, sigma, mu_r, debye_poles, lorentz_poles)` | a named material | [Geometry & Materials](https://remilab.ai/rfx/api/geometry-materials/#materials) | | `add(shape, *, material)` | fill a `Box`, `Sphere`, `Cylinder`, `PolylineWire`, `Via`, `MeshShape`... | [Geometry & Materials](https://remilab.ai/rfx/api/geometry-materials/#placing-shapes) | | `add_thin_conductor(shape, *, surface_impedance_f0=None)` | a copper sheet | [Geometry & Materials](https://remilab.ai/rfx/api/geometry-materials/#metal-sheets) | | `add_source(position, component, *, waveform, amplitude_kind="current")` | point source, no impedance | [Sources & Ports](https://remilab.ai/rfx/api/sources-ports/#sources) | | `add_tfsf_source(...)` | plane wave | [Sources & Ports](https://remilab.ai/rfx/api/sources-ports/#sources) | | `add_port`, `add_msl_port`, `add_waveguide_port`, `add_coaxial_port` | ports for S-parameters | [Sources & Ports](https://remilab.ai/rfx/api/sources-ports/#ports) | | `add_probe`, `add_dft_plane_probe`, `add_flux_monitor`, `add_ntff_box` | observables | [Results & Observables](https://remilab.ai/rfx/api/results/#observables) | ## Checking and running | Call | Returns | Reference | |---|---|---| | `preflight()` | `PreflightReport` (read `.ok`, `.errors`, `.warnings`) | [Simulation](https://remilab.ai/rfx/api/simulation/#checking-before-you-run) | | `fidelity_report()` | where each shape landed on the grid | [Simulation](https://remilab.ai/rfx/api/simulation/#checking-before-you-run) | | `run(n_steps= / num_periods= / until_decay=, ...)` | `Result` | [Simulation](https://remilab.ai/rfx/api/simulation/#run) | | `forward(eps_override=..., ...)` | `ForwardResult`, differentiable with JAX | [Simulation](https://remilab.ai/rfx/api/simulation/#forward-the-differentiable-path) | | `plan_mesh(...)`, `Simulation.auto(...)` | a proposed mesh and domain | [Automation](https://remilab.ai/rfx/api/functions/) | ## S-parameters Each port family has its own calculator: | Ports | Call | Returns | |---|---|---| | lumped / wire `add_port` | `run(compute_s_params=True, s_param_freqs=...)` | `Result.s_params`, `Result.freqs` | | microstrip `add_msl_port` | `compute_msl_s_matrix()` | `MSLSMatrixResult` | | waveguide `add_waveguide_port` | `compute_waveguide_s_matrix()` | `WaveguideSMatrixResult` | | coaxial `add_coaxial_port` | `compute_coaxial_line_reflection()` or `compute_coaxial_two_port()` | `CoaxialLineReflectionResult`, `CoaxialTwoPortResult` | `sim.compute_s_matrix()` chooses the calculator from the registered ports, and `sim.s_matrix_lane()` tells you which one it would use. Microstrip and coaxial ports are not computed by `run(compute_s_params=True)`. Details: [Sources & Ports](https://remilab.ai/rfx/api/sources-ports/#s-parameter-calculators). ## Reading results | Object | Main contents | Reference | |---|---|---| | `Result` | `time_series`, `dt`, `settling_db`, `s_params`, `freqs`, `flux_monitors`, `dft_planes`, `ntff_data`, `snapshots`; `find_resonances()`, `s11()`, `s_db()` | [Results & Observables](https://remilab.ai/rfx/api/results/#result) | | `ForwardResult` | `time_series`, `s_params`, `dft_planes`, `ntff_data` | [Results & Observables](https://remilab.ai/rfx/api/results/#forwardresult) | | S-parameter results | `S` or `s_params` indexed `[receiver, driven, freq]` | [Results & Observables](https://remilab.ai/rfx/api/results/#s-parameter-results) | Before you trust a spectrum, check `result.settling_db` (below about −40 dB). For what is supported, limited or refused, see [Support Boundaries](https://remilab.ai/rfx/support/). --- # Benchmarks Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/guide/benchmarks/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/guide/benchmarks.mdx This page lists the comparisons between rfx and other solvers that ship with the repository. For each one it says what is compared, against which reference, what the committed run shows, and how to reproduce it. For how these fit with the analytic tests and your own checks, read [Cross-Validation and Accuracy](https://remilab.ai/rfx/guide/validation/). Each case checks one geometry, mesh, band, port and observable. A pass says rfx agrees with the reference there. It does not validate other geometries, meshes or a whole port family. ## Reference cases Each comparison is a test under `tests/crossval/`. It solves the structure in rfx and compares the result with a reference record frozen in the repository, so you do not need the other solver to re-check rfx. The records say which solver, version and mesh produced them. ### Probe-fed patch on RT/Duroid 5880 | | | |---|---| | Structure | A 39.7 × 49.25 mm patch on 3.175 mm RT/Duroid 5880 (εr 2.2, tan δ 0.001) over a 55.6 × 65.1 mm ground, fed by a 50 Ω probe 8.73 mm off centre. | | Reference | openEMS, the same board on three meshes, judged against its finest one. | | rfx meshes | a uniform ladder at h/4, h/8 and h/12 (4, 8 and 12 cells through the substrate). The board is drawn so all three rungs realize the same patch, and the test refuses a rung that does not. | | Judged | the TM010 resonance (where Re(Z_in) peaks) within 1 %, and the input resistance there within 2 %, on the finest rung; the last two rungs must agree within 1 % (mesh statement), and the record must ring down to −40 dB. | | Reported, not judged | the `\|S11\|` dip frequency and depth, the reactance at resonance and the −10 dB band edges. Neither solver declares a probe radius, so each feed cell sets the probe's series reactance, and that moves the dip. | | Result | pending for 2.0 (tracker 1471): the case is being re-measured under the current settling check, which judges each frequency bin a result reads; no agreement figure is quoted until that run is in the repository. The case was added in [bk-squared/rfx#1315](https://github.com/bk-squared/rfx/pull/1315). | | Test | `tests/crossval/rt5880_patch/test_rt5880_patch.py` | The case judges the resonance and the input resistance, not the `|S11|` curve, because the two solvers' probe models differ. It says the cavity, its fringing and its radiation loss agree with openEMS on this board. It does not say a patch on a different board or mesh is within 1 %. [Real and imaginary parts of the input impedance of the RT/Duroid 5880 probe-fed patch from rfx and from openEMS on its finest mesh, 2.0 to 2.9 GHz](https://remilab.cnu.ac.kr/share/7c02ad43c580/forward-patch/forward_curves_rt5880_patch_site.png) Input impedance of the RT/Duroid 5880 patch, rfx and openEMS on the same board. [The forward model, next to openEMS →](https://remilab.ai/rfx/showcase/forward-checks/) ## More comparisons The other comparisons under `tests/crossval/`. Most are long and marked `gpu` or `slow`. | Structure | Compared with | Test | |---|---|---| | Microstrip open-stub notch filter, `\|S21\|` | openEMS tutorial result and Palace FEM | `tests/crossval/msl_notch_filter/` | The notch filter is pending for 2.0 (tracker 1471): it is being re-measured under the current settling check, and its uniform-mesh ladder is not yet shown to converge on the drawn 600 µm line. No agreement figure is quoted until that run is in the repository. [|S21| and |S11| of the microstrip open-stub notch filter from rfx and from openEMS on its finest mesh, 2 to 7 GHz](https://remilab.cnu.ac.kr/share/7c02ad43c580/forward-notch/forward_curves_msl_notch_filter_site.png) |S21| and |S11| of the microstrip open-stub notch filter, rfx and openEMS on the same board. [The forward model, next to openEMS →](https://remilab.ai/rfx/showcase/forward-checks/) Structures with a closed form are checked against that closed form, not against another solver. Those tests are in `tests/oracle/`. ## Reproduce The comparisons are pytest tests. The long ladders are marked `gpu` and `slow`, so the pull-request suite never runs them. From a source checkout: ```bash # The fast checks: build each board, check what it realizes, read the records. pytest tests/crossval/rt5880_patch # The full ladder (a GPU; the h/12 rung is about 53 M cells). pytest tests/crossval/rt5880_patch -m gpu ``` `RFX_RT5880_PATCH_RUNGS=793.75e-6` runs only the coarsest rung. That is a diagnostic, not the case's verdict, because the mesh statement needs the ladder. ## Limits - **One board is not every patch.** The patch case covers one board and one probe feed. For your own patch, run a convergence study and compare with a reference, as in [Your First Patch Antenna](https://remilab.ai/rfx/examples/first-patch/). - **Magnitude comparisons do not cover phase.** The two filter cases compare `|S21|` only. On the notch filter, rfx also runs shorter feed lines than the reference, which moves the phase but not the magnitude. - **Scripts under `validation/research/` are not reference cases.** They are research checks for features outside the documented scope. --- # Your first run Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/guide/first-run/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/guide/first-run.mdx A small closed metal box is a useful installation check. A short electric-current pulse excites the field, the walls reflect it, and a probe records `Ez` over time. There is no port or absorbing boundary in this model. ## Install and run ```bash pip install rfx-fdtd ``` Select the documentation version matching your installation using the version links above. For this development documentation, install the source revision identified there if you need its exact API. Copy the following into a Python file and run it. This block is generated from the repository's `examples/quickstart/hello_world.py`; its builder and execution are checked by the existing example tests. ```python from rfx import GaussianPulse, Simulation sim = Simulation( freq_max=10e9, domain=(0.02, 0.02, 0.02), dx=2e-3, boundary="pec", ) sim.add_source( (0.01, 0.01, 0.01), "ez", waveform=GaussianPulse(f0=5e9, bandwidth=0.8), amplitude_kind="current", ) sim.add_probe((0.014, 0.01, 0.01), "ez") preflight = sim.preflight() print(preflight.format()) preflight.raise_for_failure() n_steps = 120 result = sim.run(n_steps=n_steps, compute_s_params=False) print(result.time_series.shape) ``` ## Read the result The model is a 20 mm vacuum cube with perfectly conducting (PEC) walls and 2 mm cells. Positions and dimensions are in metres. The waveform's centre frequency is in hertz, and `amplitude_kind="current"` makes the excitation a current in amperes. `result.time_series` has shape `(120, 1)`: one row per time step, one column per probe. The probe is two cells away from the source. `result.dt` is the time between rows. The pulse reflects in the lossless closed box, so continued oscillation is expected; this is not an absorption or decay demonstration. Preflight findings describe the model before it runs. Read warnings as well as errors. Completing these 120 steps checks the installation and API use; it does not establish an accurate resonance, Q, or a mesh-converged electromagnetic result. ## See what comes next - [Visual manual](https://remilab.ai/rfx/gallery/visual-manual/): connect structures, field movies and measurements. - [Quick start](https://remilab.ai/rfx/start/first-simulation/): add a material and absorbing boundaries, then record two probes. - [Examples](https://remilab.ai/rfx/examples/): choose a runnable model for your application. --- # Gradient Behavior in rfx Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/guide/gradient-behavior/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/guide/gradient-behavior.md An rfx gradient is the exact derivative of the discrete simulation you set up. Whether it is *useful* depends on the design variable, the objective and the run length. This page lists the cases that behave well, the ones that don't, and three checks to run before you trust a gradient. For the background, see [Autodiff and Adjoint Background](https://remilab.ai/rfx/design/gradients/). ## Three checks before you optimize The examples share one small model: a lossy dielectric slab between a soft source and a probe in open space, on a coarse 2 mm mesh so each check runs in seconds. ```python import jax import jax.numpy as jnp from rfx import Box, GaussianPulse, Simulation, gradient_record_length_witness sim = Simulation(freq_max=4e9, domain=(0.06, 0.05, 0.04), dx=2e-3, boundary="cpml", cpml_layers=6) sim.add_material("slab", eps_r=4.0, sigma=0.01) sim.add(Box((0.024, 0.019, 0.014), (0.036, 0.031, 0.026)), material="slab") sim.add_source((0.018, 0.025, 0.020), "ez", waveform=GaussianPulse(f0=3e9, bandwidth=0.8), amplitude_kind="current") sim.add_probe((0.042, 0.025, 0.020), "ez") grid = sim.run(n_steps=1).grid eps0 = jnp.full(grid.shape, 4.0, dtype=jnp.float32) slab_cell = grid.position_to_index((0.030, 0.025, 0.020)) def objective(eps_r, n_steps=200): out = sim.forward(eps_override=eps_r, n_steps=n_steps) return jnp.sum(out.time_series ** 2) ``` ### 1. Autodiff against finite differences Compare the autodiff gradient with a central difference at a few cells: ```python def fd_check(objective, eps_r, cell, h=1e-1): """Relative error between AD and a central finite difference at one cell.""" fd = (objective(eps_r.at[cell].add(h)) - objective(eps_r.at[cell].add(-h))) / (2 * h) ad = jax.grad(objective)(eps_r)[cell] rel_err = abs(ad - fd) / max(abs(fd), 1e-30) print(f"cell {cell}: AD={float(ad):.4e} FD={float(fd):.4e} " f"rel err={float(rel_err):.2e}") return rel_err cell = tuple(int(i) for i in slab_cell) fd_check(objective, eps0, cell, h=1e-2) fd_check(objective, eps0, cell, h=1e-1) ``` rfx runs in float32 by default, which resolves a loss to about `1e-7` of its value. Choose `h` so that the loss change, about `2 h |dL/dp|`, is well above that. Here the loss is large and `h = 1e-2` leaves an error of a few percent, while `h = 1e-1` agrees to about 0.2 %. For permittivity-like variables, start between `1e-2` and `1e-1`. If the check still fails, look at the loss scale, the run length and the monitor placement before touching optimizer settings. ### 2. The loss magnitude A passing finite-difference *witness is also not sufficient*. Finite differences and autodiff differentiate the same loss over the same observation window. If the window is empty (the reflection never reaches the probe during the run, or the monitor sits in the absorber), both agree on a gradient of numerical noise. So compare the loss **magnitude** with a physical expectation, not only the relative error. A reflected-energy proxy on a design that reflects a meaningful fraction of the pulse lands near `1e-2` to `1e-1`. A value like `~1e-7` signals an empty window, not a matched design. See the split-window rule in [Inverse Design](https://remilab.ai/rfx/design/optimization/). ### 3. The record length A resonance that is still ringing when the record ends barely changes the loss value, but it can change the gradient a lot. `gradient_record_length_witness` differentiates your objective at `n_steps` and at `factor * n_steps` (default 2x) and reports how much the gradient vector moved: ```python for n in (100, 200): w = gradient_record_length_witness(objective, eps0, n, tol=0.05) print(f"n_steps={n}: passed={w.passed}, " f"gradient change={w.worst:.3f}, value change={w.worst_value_rel_change:.3f}") ``` Here 100 steps fails, because the pulse has not finished passing the probe, and 200 steps passes. The objective must take `(params, n_steps)`. You choose `tol`; there is no default because the right bar depends on the structure's Q and on what the gradient is for. A few percent is a reasonable start. `w.cosine_by_bin` tells you whether the direction changed or only the step length. A failed witness means the record is too short for this gradient: lengthen the run and check again. The witness costs about `1 + factor` differentiated runs, so use it once per new setup, not every iteration. Pair it with the settling check in [Probes and S-Parameters](https://remilab.ai/rfx/measure/s-parameters/); it does not replace it. ## What behaves well - **Smooth dielectric variables.** Continuous `eps_r` in a fixed design region is the safest case. Use a bounded parameterization so the optimizer stays in the material range you mean: ```text eps_design = eps_min + (eps_max - eps_min) * jax.nn.sigmoid(latent) ``` `optimize()` does this for you through `DesignRegion(eps_range=...)`. - **Band-averaged objectives.** A mean over a band, such as `jnp.mean(jnp.abs(s11) ** 2)`, is less dominated by a single noisy frequency point than one sample. - **The differentiable port paths.** Lumped and wire ports through `forward(port_s11_freqs=...)`, lumped R/L/C values through `forward(rlc_values_override=...)`, and the waveguide and microstrip S-matrix calculators where they accept `eps_override`. Each is differentiable only within its own support entry; see the [S-parameter support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.md). ## What is noisy or stiff | Case | Symptom | What to do | |---|---|---| | A PEC edge or topology boundary that moves | stair-stepping: the gradient jumps or flips sign when an edge crosses a cell | keep topology fixed in a gradient run; use relaxed occupancy or dielectric variables with filters and a minimum feature size; verify the final discrete geometry | | Long records | weak signal, large dynamic range, round-off | use the shortest run that still captures the observable, and run the record-length witness | | Near cutoff or a high-Q resonance | one frequency sample is very sensitive to mesh, material and run length | keep the band away from cutoff unless that is the target; use broadband objectives; verify with a convergence study | | Float32 finite differences | FD disagrees with AD by a few percent | use a larger `h` (see above) | **Precision.** The default is `precision="float32"` (complex64 DFT buffers). The uniform single-device lane also accepts `precision="float64"` once JAX x64 is enabled (enabling x64 alone does not switch the fields to float64), and `precision="mixed"` (float16 field storage, float32 accumulators). Mixed precision with CPML has a higher absorber residual, so do not use it for low-reflection or S-parameter observables near that floor. The non-uniform, distributed and subgridded lanes refuse both non-default modes. ## What is not a design variable - **CPML cells.** The absorber is artificial. A gradient there says nothing about a device. Keep design regions out of the CPML. - **Integer and topology choices.** Grid size, CPML layer count, number of steps, inserting or removing a shape. Fix them per run, or search over them in an outer loop of separate differentiable problems. - **Subpixel smoothing.** It is a `run()` option and is not part of the differentiable `forward()` path, so gradients see the staircased permittivity. Don't use an interface position as a design variable and expect subpixel-accurate sensitivity. - **Unsupported combinations.** A finite gradient from a source, port or mesh combination outside the support matrix is not evidence of anything. rfx raises on the combinations it knows are unsupported. ## Limits - **AD and FD agree but the loss is tiny**: the observation window is empty. Fix the run length or monitor placement. - **The gradient changes a lot when you double the run**: the record is too short. Lengthen it until the record-length witness passes. - **The gradient flips sign as a metal edge moves**: stair-stepping. Use relaxed or dielectric variables and verify the final geometry. - **A gradient-optimized proxy is not a validated result**: re-run the final design through the port, resonance or far-field workflow on a converged mesh. --- # Learn rfx Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/guide/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/guide/index.md Start with a physical question, build a model, and decide what you will measure. The [visual manual](https://remilab.ai/rfx/gallery/visual-manual/) shows that sequence with fields, structures and response curves. New here? Read [Installation](https://remilab.ai/rfx/start/install/), [first runnable model](https://remilab.ai/rfx/guide/first-run/), [Quick Start](https://remilab.ai/rfx/start/first-simulation/), then [Core Concepts](https://remilab.ai/rfx/start/concepts/): cells, boundaries, conductors, sources and ports, and when a run is finished. | Your next step | Read or run | |---|---| | See what rfx can simulate | [Visual examples](https://remilab.ai/rfx/gallery/) | | Choose a material and draw a structure | [Materials & geometry](https://remilab.ai/rfx/model/materials/) | | Excite the model | [Sources & ports](https://remilab.ai/rfx/model/ports/) | | Inspect fields and export plots | [Visualization & analysis](https://remilab.ai/rfx/measure/plots-and-export/) | | Measure reflection and transmission | [Probes & S-parameters](https://remilab.ai/rfx/measure/s-parameters/) | | Build a real device | [Your First Patch Antenna](https://remilab.ai/rfx/examples/first-patch/) | | Refine the mesh and check the answer | [Convergence study](https://remilab.ai/rfx/evidence/mesh-convergence/) | | Change a design using derivatives | [Inverse design](https://remilab.ai/rfx/design/optimization/) and [gradient behavior](https://remilab.ai/rfx/guide/gradient-behavior/) | Use the [Examples hub](https://remilab.ai/rfx/examples/) to choose by structure or follow the ordered source tutorials. Check [Support Boundaries](https://remilab.ai/rfx/support/) for combinations of ports, materials, grids and runners; the [API](https://remilab.ai/rfx/api/) gives the exact calls. --- # Cross-Validation and Accuracy Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/guide/validation/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/guide/validation.mdx A finished run, a clean preflight and a smooth plot do not show that a number is right. This page answers the question you should ask before you report an rfx result: **how do I know it is correct?** It covers what rfx has already checked, what only you can check for your structure, and where the evidence lives. ## What rfx checks rfx tests its solver in three independent ways. | Kind of check | What it compares | Where it lives | When it runs | |---|---|---|---| | **Analytic** | rfx against a closed-form answer: cavity modes, slab reflection and transmission (lossless, lossy, Debye, Lorentz, Drude, oblique TE), a PEC sphere against the Mie series, empty, shorted and slab-loaded rectangular waveguides, a WR-90 inductive iris against mode matching, lumped R, L and C loads on a line, dipole far fields | `tests/oracle/` | most on every pull request | | **Cross-solver** | rfx against another solver (mostly openEMS; also Palace, Meep and a boundary-element solver) on structures with no closed form: a microstrip stub notch filter, a stepped-impedance low-pass filter, a patch antenna's resonance and far field, the RCS of a PEC cube, broadband waveguide sections | `tests/crossval/` | on demand and on GPU, against frozen reference records | | **Convergence** | rfx against itself as the mesh is refined. A result that moves when you refine the mesh is not converged yet | the same tests, and your own studies | as above | The cross-solver cases compare against reference records that are frozen in the repository, so no external solver is needed to re-check rfx against them. [Benchmarks](https://remilab.ai/rfx/guide/benchmarks/) lists the cases, what each one shows and how to reproduce it. Two limits apply to all of this evidence: - **A passing case covers its own setup.** The geometry, mesh, band, port family and observable of that case. It does not cover your structure. - **Some features have little or no evidence.** The support tables in [Support Boundaries](https://remilab.ai/rfx/support/) say which are *recommended*, which are *limited* to stated conditions, and which are outside the documented scope. The maintainer [support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/support_matrix.md) has the detail for each feature. ## What you must check yourself rfx cannot validate your structure for you. Before you report a number, work through this list. 1. **Preflight.** Read every warning from `sim.preflight()`. Each one names a condition that can make the result wrong without an error: a mesh too coarse for the material, a probe inside the absorber, geometry that lands off the grid, a lossless resonator. 2. **Run length.** Check `result.settling_witness["status"]`. It is `pass` when, at every frequency the run's spectral results read, the part of the value still ringing past the record's end is at or below −40 dB (`settling_db` is the worst of these). `fail`, `undetermined` and `absent` are not passes; above the bar, spectra, S-parameters and far fields still carry a cut-off transient. For gradients, the record must be longer still: `rfx.gradient_record_length_witness` checks that a gradient does not change when the record is doubled. 3. **Mesh convergence.** Refine the mesh and report how far your number moves, not only the finest answer. [Convergence Study](https://remilab.ai/rfx/evidence/mesh-convergence/) walks through one. 4. **Physical sanity.** A passive structure has `|S| ≤ 1`. A reciprocal one has `S21 = S12`. A lossless one conserves power, `|S11|² + |S21|² = 1` or `R + T = 1`. A breach means the extraction or the setup is wrong, not that the device is active. 5. **An independent reference.** Compare with a closed form where one exists, otherwise with another solver or a measurement of your structure. 6. **Stay inside the support boundaries.** Use the calculator that matches your port family, within its stated limits. See [Support Boundaries](https://remilab.ai/rfx/support/). ## Example: a cavity against its closed form A closed PEC box has an exact resonance, so it shows two of the checks above in a few seconds. The TM110 mode of an `a × b × d` box with `d` the shortest side resonates at `f = (c/2)·√((1/a)² + (1/b)²)`: ```python import numpy as np from rfx import Simulation, GaussianPulse C0 = 299_792_458.0 a, b, d = 0.024, 0.020, 0.010 # box size in metres f_exact = C0 / 2 * np.hypot(1 / a, 1 / b) # TM110, about 9.756 GHz def tm110(dx): sim = Simulation(freq_max=15e9, domain=(a, b, d), dx=dx, boundary="pec") sim.add_source((0.30 * a, 0.35 * b, d / 2), "ez", waveform=GaussianPulse(f0=f_exact, bandwidth=0.8), amplitude_kind="current") sim.add_probe((0.65 * a, 0.60 * b, d / 2), "ez") # The same physical record length on every mesh. result = sim.run(n_steps=round(3000 * 2e-3 / dx)) modes = result.find_resonances(freq_range=(0.8 * f_exact, 1.2 * f_exact)) return min(modes, key=lambda m: abs(m.freq - f_exact)).freq for dx in (2e-3, 1e-3, 0.5e-3): f = tm110(dx) print(f"dx = {dx * 1e3:.1f} mm: {f / 1e9:.4f} GHz, " f"error {100 * (f / f_exact - 1):+.3f} %") ``` On a CPU this prints errors of about −0.13 %, −0.03 % and −0.01 %. Each halving of the cell cuts the error by about four, the second-order convergence the Yee scheme should show. Your own study works the same way. Pick one number you care about, refine the mesh, and check that the number settles toward a limit. The box is lossless, so its physical Q is infinite. The frequency is the result here; a Q from this record would only measure the record length. A closed box never rings down, so the run uses a fixed `n_steps` rather than `until_decay`. ## Starting resolutions for port results Port results need finer meshes than field pictures. These are measured starting points for a convergence study. They are not a substitute for one. | Port family | Start from | What the coarser mesh costs | |---|---|---| | Lumped or wire port, one-port `S11` | about 60 cells per wavelength at the highest frequency | at 60 cells/λ, `\|S11\|` of resistive loads is within 0.54 dB of the closed form (shorts and opens exact); at 30 cells/λ magnitudes are within 1.1 dB and phase-crossing frequencies sit 1.7–2.9 % from the closed form (measured through `forward(port_s11_freqs=...)`) | | Microstrip line, `compute_msl_s_matrix` | at least 4 cells through the substrate (preflight warns below that) | the substrate thickness sets the cell size, not the wavelength | | Coaxial line | at least 4 cells across the annulus | refining shortens the probe span the phase fit uses, so give the probe array enough physical length | The microstrip row's cross-check, an open-stub notch filter against the openEMS tutorial (`tests/crossval/msl_notch_filter/`), is pending for 2.0 (tracker 1471): it is being re-measured under the current settling check, and no agreement figure is quoted until that run is in the repository. ## Where the evidence lives | Question | Page | |---|---| | Which cases compare rfx with another solver, and what do they show? | [Benchmarks](https://remilab.ai/rfx/guide/benchmarks/) | | Is this feature or combination supported? | [Support Boundaries](https://remilab.ai/rfx/support/) | | Where do I start, and which calculators have narrower limits? | [Recommended Configuration](https://remilab.ai/rfx/validation/recommended-configuration/) | | How do I run a comparison script, and what do its exit codes mean? | [Cross-Solver Validation](https://remilab.ai/rfx/validation/cross-solver/) | | What is currently known to be wrong? | [known limitations](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/known_limitations.md) on GitHub | ## Limits - **An exported file is not evidence.** Touchstone files, plots, HDF5 snapshots and reports record what a run produced. Pair them with the checks above before citing them. - **An optimizer's objective is a proxy.** A design that minimizes a differentiable objective still needs its final RF result checked on its own, with a converged forward run. - **A regression test is not an accuracy claim.** A test that pins a number guards against change at its own mesh and band. It does not say the number is right for yours. --- # Waveguide Ports Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/guide/waveguide-ports/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/guide/waveguide-ports.md A waveguide port launches one TE or TM mode of a rectangular aperture and splits the fields at its plane into incident and reflected mode amplitudes. This page shows how to get a rectangular-guide S-matrix, which normalization to pick, and how to handle junctions. The port aperture defines the guide. A port that spans the whole domain cross-section (no `y_range=` / `z_range=`, or ranges equal to it) uses the transverse domain faces as the guide walls, so those faces must be PEC: declare the absorber on the port axis only, for an x-directed guide `boundary={"x": "cpml", "y": "pec", "z": "pec"}`. On a uniform mesh `boundary="cpml"` on all faces is refused before the first step. If you leave `boundary` out, a uniform mesh solves those faces as PEC and warns once; a graded mesh refuses until you declare them. Ports need `mode="3d"`, and cannot share a model with lumped ports or a TFSF source. ## A two-port S-matrix Place two ports facing each other and call `compute_waveguide_s_matrix()`. It drives each port in turn and assembles the full matrix. ```python import jax.numpy as jnp import numpy as np from rfx import Simulation # A 40 x 20 mm guide: TE10 cuts off at 3.75 GHz, the next mode at 7.5 GHz. freqs = jnp.linspace(5e9, 6.5e9, 7) sim = Simulation(freq_max=10e9, domain=(0.10, 0.04, 0.02), boundary={"x": "cpml", "y": "pec", "z": "pec"}, cpml_layers=16, dx=0.002) sim.add_waveguide_port(0.01, direction="+x", mode=(1, 0), mode_type="TE", freqs=freqs, f0=6e9, name="left") sim.add_waveguide_port(0.09, direction="-x", mode=(1, 0), mode_type="TE", freqs=freqs, f0=6e9, name="right") result = sim.compute_waveguide_s_matrix(num_periods=40, normalize="flux") S = result.s_params # (n_ports, n_ports, n_freqs), S[receiver, driven] print(np.abs(S[1, 0]).round(3)) # |S21| of the empty guide: 1.0 across the band ``` The first argument is the port plane's coordinate along its normal axis, the axis named by `direction` (`"+x"`, `"-x"`, `"+y"`, `"-y"`, and so on). By default the aperture spans the whole domain cross-section; restrict it with `y_range=` / `z_range=` (or `x_range=` for a y-normal port). The 2 mm mesh is coarse, chosen so the example runs in seconds. rfx warns that the 16-layer absorber is thinner than it recommends at 5 GHz (see [Absorber depth](#absorber-depth)). Keep the measurement band inside the single-mode range: preflight warns when `freqs` reach toward the next mode's cutoff. Place the two ports as mirror images in the domain, as here, so both see the same grid. ## Choosing `normalize` | `normalize` | Runs | What it corrects | Use it for | |---|---|---|---| | `False` (default) | one per port | nothing; magnitudes carry a few-percent Yee impedance error | \|S11\| of strong reflectors (shorts, high-Q loads) on a uniform mesh | | `True` | two per port | one-way grid dispersion in transmission | S21 of a straight guide. Not for S11. | | `"flux"` | two per port | magnitude from Poynting flux, phase from the mode | general use; recommended on a graded mesh (`normalize=False` is refused there) | `normalize=True` divides by an empty-guide reference run. That fixes transmission but not reflection. `normalize=True` also cannot be differentiated; `False` and `"flux"` can. With `normalize=False`, a passive strong reflector can read slightly above |S| = 1. When it rises beyond the expected range, rfx prints an advisory. Switch to `"flux"` or refine the mesh rather than reporting that number. rfx also checks every result for passivity and reciprocity and warns when a column carries more power than it received. That warning almost always means the extraction is wrong, not the physics. ## A single port with `run()` A plain `run()` with one waveguide port gives that port's calibrated S11 and S21 in `result.waveguide_sparams`. Use it as a quick diagnostic. It is not the driven-in-turn matrix. ```python single = Simulation(freq_max=10e9, domain=(0.12, 0.04, 0.02), boundary={"x": "cpml", "y": "pec", "z": "pec"}, cpml_layers=10, dx=0.002) single.add_waveguide_port(0.01, mode=(1, 0), freqs=freqs, f0=6e9, name="input") res = single.run(n_steps=500) sp = res.waveguide_sparams["input"] # sp.freqs, sp.s11, sp.s21 ``` ## Reference planes Fields are sampled on grid planes, so rfx snaps each port's planes to the nearest one and reports where they landed. You choose where S is reported: ```python cal = Simulation(freq_max=10e9, domain=(0.12, 0.04, 0.02), boundary={"x": "cpml", "y": "pec", "z": "pec"}, cpml_layers=10, dx=0.002) # Report at the snapped measurement planes (the default). cal.add_waveguide_port(0.01, calibration_preset="measured", name="a") # S11 at the source plane, S21 from source to probe plane. cal.add_waveguide_port(0.01, calibration_preset="source_to_probe", name="b") # Explicit planes, in metres along the port normal, with de-embedding. cal.add_waveguide_port(0.01, reference_plane=0.012, probe_plane=0.034, name="c") ``` Three ports on one plane is only to show the options; a real model uses one. The result's `reference_planes` and each port's `measured_reference_plane` give the planes actually used. ## More port layouts Ports can face along any axis, and several can share one boundary: ```python # A y-directed guide. ydir = Simulation(freq_max=10e9, domain=(0.04, 0.12, 0.02), boundary={"x": "pec", "y": "cpml", "z": "pec"}, cpml_layers=10, dx=0.002) ydir.add_waveguide_port(0.01, direction="+y", name="bottom") ydir.add_waveguide_port(0.09, direction="-y", name="top") # Two parallel guides entering through the same x face. pair = Simulation(freq_max=10e9, domain=(0.12, 0.10, 0.02), boundary="cpml", cpml_layers=10, dx=0.002) pair.add_waveguide_port(0.01, y_range=(0.0, 0.04), direction="+x", name="lo") pair.add_waveguide_port(0.01, y_range=(0.06, 0.10), direction="+x", name="hi") ``` `n_modes > 1` records several modes per port. Multimode results are assembled outside the differentiable path. ## Junctions and interior walls The default references assume the guide walls are the domain boundary. A T-junction, branch or septum has interior PEC walls, and the empty reference then radiates into open space: every |S| inflates badly (max |S| near 10 on a compact T). For these, pass `normalize="flux"` with one reference simulation per port: the straight continuation of that port's guide, with no junction. ```python from rfx import Box port_kwargs = dict(mode=(1, 0), mode_type="TE", f0=6e9, freqs=jnp.linspace(4.5e9, 6.5e9, 4), z_range=(0.0, 0.02), ref_offset=3, probe_offset=15) def three_port(walls): """Same domain, mesh and ports each time; only the PEC walls change.""" s = Simulation(freq_max=10e9, domain=(0.12, 0.12, 0.02), boundary="cpml", cpml_layers=10, dx=0.002) for lo, hi in walls: s.add(Box(lo, hi), material="pec") s.add_waveguide_port(0.01, y_range=(0.04, 0.08), direction="+x", name="left", **port_kwargs) s.add_waveguide_port(0.11, y_range=(0.04, 0.08), direction="-x", name="right", **port_kwargs) s.add_waveguide_port(0.11, x_range=(0.04, 0.08), direction="-y", name="top", **port_kwargs) return s horizontal = [((0.0, 0.0, 0.0), (0.12, 0.04, 0.02)), ((0.0, 0.08, 0.0), (0.12, 0.12, 0.02))] vertical = [((0.0, 0.0, 0.0), (0.04, 0.12, 0.02)), ((0.08, 0.0, 0.0), (0.12, 0.12, 0.02))] tee = three_port([((0.0, 0.0, 0.0), (0.12, 0.04, 0.02)), ((0.0, 0.08, 0.0), (0.04, 0.12, 0.02)), ((0.08, 0.08, 0.0), (0.12, 0.12, 0.02))]) junction = tee.compute_waveguide_s_matrix( num_periods=20, normalize="flux", port_reference_sims=[three_port(horizontal), three_port(horizontal), three_port(vertical)]) ``` The references are necessary but not sufficient. This compact example still gives a non-physical matrix, and rfx warns about probe clearance and absorber depth. A physical junction S-matrix also needs: - each probe plane at least 5 decay lengths of the next higher mode away from the junction; - an absorber at least about 0.5 guide wavelengths deep at the lowest frequency; - a converged mesh. The walls here are PEC volumes, so the guide width the solver sees is the width you drew. See [How conductors land on the lattice](https://remilab.ai/rfx/model/materials/#how-conductors-land-on-the-lattice). ## Absorber depth A waveguide port's absorber must swallow the guided wave, and CPML does that worse near cutoff, where the guide wavelength is long. rfx warns when the absorber on a port's axis is thinner than 0.5 λ_g at the lowest measured frequency. Treat that as a floor, and end every guide at a port rather than running it into the absorber. ## Gradients and memory `compute_waveguide_s_matrix` accepts `eps_override=` / `sigma_override=` for differentiation with `normalize=False` or `"flux"`, single-mode only. For long runs under `jax.grad`, `checkpoint_segments=K` trades compute for memory. On a uniform mesh `K` must divide the number of time steps exactly; on a graded mesh it is a target segment count. ## Limits - **Validated scope** is uniform, single-mode, straight rectangular guides and same-guide junctions under the far-port conditions above. Phase is validated on fewer configurations than magnitude. - **Ports with different cross-sections** are outside the validated scope. S is a ratio of each port's own modal waves, so it equals a power-wave S only when all ports share one cross-section and mode. - **Compact junctions** give non-physical matrices even with `port_reference_sims`. Keep the far-port conditions. - **Graded meshes** refuse `normalize=False`; use `"flux"` (or `True`). Graded-mesh waveguide results outside the published WR-90 comparisons are experimental. - **`port_reference_sims`** needs `normalize="flux"`, single-mode ports, a uniform mesh and no material override. - Full scope, with the comparisons behind it, is in the [S-parameter support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.md). --- # rfx Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/index.mdx Open source · JAX · 3-D FDTD for RF and microwave The RF simulator that returns gradients. Write a loss on S-parameters, a radiation pattern or a field. rfx gives its derivative with respect to the permittivity of every cell in your design region from one forward and one reverse pass. On the patch antenna below, the value and gradient over 43,008 design cells take 1.4 times as long as the simulation alone. pip install rfx-fdtd [See the showcase →](https://remilab.ai/rfx/showcase/) [GitHub](https://github.com/bk-squared/rfx) ![Video poster](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/patch_sensitivity_poster.png) [source](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/patch_sensitivity.mp4) [Download the loop (MP4)](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/patch_sensitivity.mp4). One reverse pass through a 2.3 GHz patch antenna on RT/Duroid 5880: how much each of 20,592 substrate cells moves |S11| at 2.356 GHz. The map is strongest under the two radiating edges and nearly zero along the centre line, the field pattern of the TM010 mode. [Read the case →](https://remilab.ai/rfx/showcase/sensitivity-map/) 1.4× Value and gradient over 12,400 to 43,008 design cells, compared with one forward run on the same RTX A6000. Central differences would take 4.9 to 16.8 days. [What a gradient costs →](https://remilab.ai/rfx/showcase/gradient-cost/) 2×10⁻⁵ Largest relative difference between the reverse-mode gradient and central finite differences on the patch (float64). [How the gradient was checked →](https://remilab.ai/rfx/showcase/sensitivity-map/) ## See the gradient at work [![Map of the derivative of |S11|² with respect to the permittivity of each substrate cell under a patch antenna, beside its |S11| curve and its |E_z|² map](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/patch_sensitivity_site.png)](https://remilab.ai/rfx/showcase/sensitivity-map/) Which cells move the match? One reverse pass maps the sensitivity of a patch antenna's |S11| to every substrate cell. 20,592 cells, one pass [Read the case →](https://remilab.ai/rfx/showcase/sensitivity-map/) [![Bar chart of wall time on an RTX A6000 for a forward run, the value and gradient, and central differences, for three design regions](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/gradient_cost_site.png)](https://remilab.ai/rfx/showcase/gradient-cost/) What a gradient costs Time and memory for the value and gradient against a forward run, as the design region grows. 24 s vs 4.9–16.8 days [Read the case →](https://remilab.ai/rfx/showcase/gradient-cost/) [![Reflection of a three-layer anti-reflection coating, its layer permittivities over the Adam iterations, and the band-mean reflection against the transfer-matrix optimum](https://remilab.cnu.ac.kr/share/7c02ad43c580/ar-coating/ar_coating_site.png)](https://remilab.ai/rfx/showcase/descent-to-optimum/) Descend to the closed-form optimum Adam through the FDTD solve finds the three layer permittivities of a broadband anti-reflection coating. within 1.1 % of the transfer-matrix optimum [Read the case →](https://remilab.ai/rfx/showcase/descent-to-optimum/) [![Input impedance of the RT/Duroid 5880 probe-fed patch from rfx and from openEMS](https://remilab.cnu.ac.kr/share/7c02ad43c580/forward-patch/forward_curves_rt5880_patch_site.png)](https://remilab.ai/rfx/showcase/forward-checks/) The forward model next to openEMS Input impedance of the patch and |S21| of a microstrip notch filter, on the same boards as openEMS. being re-measured under the current settling check [Read the case →](https://remilab.ai/rfx/showcase/forward-checks/) ## What the code looks like The objective is an ordinary Python function of the permittivity in a design box. JAX differentiates the whole time-stepping loop. ```python def J(eps_box): res = board.sim.forward( design_box=corners, design_eps_override=eps_box, design_box_holds_ports=True, n_steps=n_steps, checkpoint=False, checkpoint_segments=checkpoint_segments, port_s11_freqs=freqs, skip_preflight=True) s = res.s_params s = s if s.ndim == 1 else s[0] return jnp.real(s[0]) ** 2 + jnp.imag(s[0]) ** 2 v, g = jax.jit(jax.value_and_grad(J))(eps_bg) ``` The full script builds the board, runs preflight once on it, and checks the gradient against finite differences before anything is plotted. [Full script →](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/scripts/showcase/patch_sensitivity.py) ## Start here - Install and run a first model: [Installation](https://remilab.ai/rfx/start/install/) · [Your first run](https://remilab.ai/rfx/guide/first-run/) - Watch fields move before measuring them: [The visual manual](https://remilab.ai/rfx/gallery/visual-manual/) (the teaching films) - Design with gradients: [Autodiff and adjoint](https://remilab.ai/rfx/design/gradients/) · [Inverse design](https://remilab.ai/rfx/design/optimization/) · [Gradient behavior](https://remilab.ai/rfx/guide/gradient-behavior/) - Look up a call: [API reference](https://remilab.ai/rfx/api/) ## Before you trust a number A result holds for the structure, mesh and observable it was checked on. Read [Support Boundaries](https://remilab.ai/rfx/support/) before combining features and [Evidence](https://remilab.ai/rfx/validation/) before reporting an RF result. Working with a coding assistant? Point it at [/rfx/llms.txt](https://remilab.ai/rfx/llms.txt). Every page has a Markdown version, and every showcase case has a result.json with its numbers, units, conditions and limits. --- # Far-Field & Radar Cross Section Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/measure/far-field-and-rcs/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/measure/far-field-and-rcs.md This page shows how to get radiation patterns and directivity from a near-to-far-field transform (NTFF), and how to compute radar cross section (RCS) with a plane wave. It also says which RCS numbers you can trust. ## Radiation pattern An NTFF box is a closed surface around your radiator. During the run rfx accumulates the tangential E and H on its six faces at the frequencies you ask for. Afterwards `compute_far_field` turns those into equivalent currents and integrates them to the far field. The pattern therefore comes from the recorded surface, not from probes in the near field. ```python import numpy as np from rfx import GaussianPulse, Simulation, compute_far_field, radiation_pattern sim = Simulation(freq_max=5e9, domain=(0.20, 0.20, 0.20), dx=4e-3, boundary="cpml", cpml_layers=8) sim.add_source((0.10, 0.10, 0.10), "ez", waveform=GaussianPulse(f0=3e9, bandwidth=0.5), amplitude_kind="current") # The box encloses the source with about half a wavelength of clearance. sim.add_ntff_box(corner_lo=(0.045, 0.045, 0.045), corner_hi=(0.155, 0.155, 0.155), freqs=np.array([3e9])) # A probe away from the source lets rfx check that the fields rang down. sim.add_probe((0.12, 0.10, 0.10), "ez") report = sim.preflight() if report.by_code("absorber_overlap") or report.by_code("ntff_near_field"): raise RuntimeError("move the NTFF box before running:\n" + report.format()) result = sim.run(n_steps=1000) theta = np.linspace(0, np.pi, 91) # radians phi = np.linspace(0, 2 * np.pi, 72, endpoint=False) ff = compute_far_field(result.ntff_data, result.ntff_box, result.grid, theta, phi) pattern_db = radiation_pattern(ff) # (n_freqs, n_theta, n_phi), peak = 0 dB ``` `compute_far_field` returns a `FarFieldResult` with `E_theta`, `E_phi`, `theta`, `phi` and `freqs`. Angles are in radians. The mesh here is coarse for speed; see [Limits](#limits) for what a real pattern needs. The far field is a Fourier transform of the recorded surface fields, so the run must last until the fields have died out. rfx judges that from your probe records: when a run has an NTFF box, it warns if a probe is still ringing at the end, and warns that the check is missing if you added no probe. Put the probe where the field is live but not on the source cell. ### Placing the box - Put every face between the radiator and the absorber, with clearance from both. Preflight's `ntff_near_field` check warns when a face is closer than λ/2 to a source, port or tangentially overlapping geometry, and more strongly below λ/4. `absorber_overlap` fires when the box reaches the absorber. - The near-field check does not see plane-wave (TFSF) boundaries or microstrip feed positions. Check those clearances yourself. - A small ground plane gives an `ntff_small_ground_plane` advisory when it is under about 1 λ across. Edge diffraction then shapes the pattern (a broadside dip, off-axis peaks). That is real physics, not a solver error; use a ground plane of about 1.4 λ or more if you want a clean broadside pattern. ## Directivity ```python from rfx import directivity D = directivity(ff) # (n_freqs,) in dBi print(f"Directivity: {D[0]:.2f} dBi") # a short dipole is about 1.76 dBi ``` `directivity` integrates the radiated power over the whole sphere (D = 4π U_max / P_rad). Sample `theta` over [0, π] and `phi` over [0, 2π), as above. A single cut gives the wrong P_rad. For optimization, `compute_far_field_jax(..., max_phase_bytes=4e9)` is the differentiable version. The byte budget splits the theta grid into chunks to bound memory; the result is the same. For a directivity objective use `maximize_directivity(theta_target, phi_target, log_ratio=True)`, so the gradient sees changes in total radiated power. See [Inverse Design](https://remilab.ai/rfx/design/optimization/). ## Radar cross section `compute_rcs` illuminates a target with a plane wave travelling along +x, records the scattered field on an NTFF box, and returns RCS(θ, φ) = 4π r² |E_scat|² / |E_inc|². It is a functional API: you pass a `Grid` and `MaterialArrays` holding the target, not a `Simulation`. ```python import jax.numpy as jnp from rfx import Box, Grid, compute_rcs from rfx.core.yee import MaterialArrays from rfx.geometry.csg import rasterize f0 = 3e9 # lambda = 0.1 m dx = 0.01 # lambda / 10: coarse, for speed grid = Grid(freq_max=f0 * 1.5, domain=(0.12, 0.12, 0.12), dx=dx, cpml_layers=8) # A 4 cm square plate, one cell thick, normal to x. compute_rcs takes material # arrays, so the plate is a high-conductivity fill (1e7 S/m), not a declared # PEC conductor. c = 0.06 plate = Box((c - dx / 2, c - 0.02, c - 0.02), (c + dx / 2, c + 0.02, c + 0.02)) eps_r, sigma = rasterize(grid, [(plate, 1.0, 1e7)]) materials = MaterialArrays(eps_r=eps_r, sigma=sigma, mu_r=jnp.ones(grid.shape, dtype=jnp.float32)) rcs = compute_rcs( grid, materials, n_steps=400, f0=f0, bandwidth=0.5, polarization="ez", theta_obs=np.linspace(0.01, np.pi - 0.01, 91), phi_obs=np.array([0.0, np.pi / 2]), freqs=np.array([f0]), subtract_incident_reference=True, # needed for the bistatic pattern ) print(f"Monostatic RCS: {rcs.monostatic_rcs[0]:.1f} dBsm") print(f"Bistatic range: {rcs.rcs_dbsm.min():.1f} to {rcs.rcs_dbsm.max():.1f} dBsm") ``` `RCSResult` holds `rcs_dbsm` and `rcs_linear` with shape `(n_freqs, n_theta, n_phi)`, the `freqs` / `theta` / `phi` axes, and `monostatic_rcs` (dBsm, one value per frequency). The monostatic value is evaluated exactly at the backscatter direction (θ = π/2, φ = π for +x incidence), so it does not depend on the observation grid you pass. ### Which RCS numbers to trust **Monostatic (backscatter).** `monostatic_rcs` is the quantity checked against the exact Mie series for a PEC sphere at ka ≈ 1 (λ/40 cells). On that committed case it reads 0.73 dB above Mie. It is always taken from the plain run, whatever you set for `subtract_incident_reference`. **Bistatic.** With the default `subtract_incident_reference=False`, the **bistatic pattern is not validated**: off-backscatter angles can be several dB to about 20 dB wrong, typically as a spurious forward-oblique lobe. The reason is geometric. The plane wave is injected between two x planes, so the region carrying the incident field is a slab that is infinite in y and z. The side faces of any closed NTFF box cut through that slab and record the incident field at full strength. Moving the box with a larger `ntff_offset` does not help, because every closed box has side faces in the slab. An empty run with no target shows the same lobe. Set `subtract_incident_reference=True` for bistatic work. rfx then repeats the run without the target and subtracts the two far fields (as complex values) before forming the RCS, which removes the incident-field contribution. It doubles the cost. On the Mie sphere it removes most of the forward-oblique lobe; that comparison covers one sphere, frequency, polarization and cut, not every target. After subtraction, the remaining error comes from staircasing of curved surfaces, sensitivity near deep pattern nulls, box placement and absorber reflection. For a new target: refine the mesh, lengthen the run, vary `ntff_offset` and `cpml_layers`, and compare with an analytic or independent result. `RCSResult` does not record whether subtraction was used, so keep your call settings with any saved result. ### Box placement and oblique incidence `compute_rcs` places the plane-wave boundary `tfsf_margin` cells in from the absorber and the NTFF box `ntff_offset` cells outside the plane-wave boundary. The box must enclose the whole injected region, and rfx refuses to run otherwise, naming the faces that don't clear it. - **Normal incidence** (`theta_inc=0`, the default): the defaults (`tfsf_margin=3`, `ntff_offset=1`) are valid. - **Oblique incidence** (`theta_inc` in degrees, tilted in the x-y plane): this uses an open-domain plane wave for `polarization="ez"` on a uniform grid only, with z-invariant targets. Its y faces are inset from the absorber, not from the injection planes, so the box encloses the injected region only for `1 <= ntff_offset <= tfsf_margin - 2`. Raise `tfsf_margin` if you need a larger offset. At oblique incidence the direction of the specular peak is checked; treat the absolute RCS as accurate to about ±2 dB. The oblique model is 2.5-D (periodic in z), so the returned RCS belongs to a strip of height 2 `dx`; scale by (L_z / (2 dx))² for a strip of height L_z. If you build an `NTFFBox` yourself around a plane-wave source, it too must surround the injected region; rfx refuses a box that doesn't. ### Complex scattered field with a phase reference For the scattered field's phase as well as its magnitude, pass `phase_reference=(x, y, z)` together with `subtract_incident_reference=True`. `compute_rcs` then returns a `ScatteringResponse` instead of an `RCSResult`: complex far-field amplitudes `F_theta` and `F_phi` in metres, the incident spectrum, and the physical point the phase is referenced to. For now this works on a uniform 3-D grid with symmetric CPML and normal incidence along +x, for either transverse polarization; other layouts are refused. The reference's x must lie on an E-node plane inside the total-field slab (rfx refuses rather than snaps it), and only frequencies where the incident spectrum is nonzero are accepted. The phase is only as good as the record length, source bandwidth and mesh, so check convergence as you would for the magnitude. ## Plotting ```python from rfx import plot_radiation_pattern, plot_rcs plot_radiation_pattern(ff, freq_idx=0, phi_idx=0) # E-plane cut at phi = 0 plot_rcs(rcs, freq_idx=0, polar=True) # polar cut, phi_idx = 0 plot_rcs(rcs, freq_idx=0, polar=False) # dBsm vs angle ``` Both helpers return a matplotlib figure for one frequency and one φ cut. See [Visualization & Result Analysis](https://remilab.ai/rfx/measure/plots-and-export/) for the other helpers. ## Limits - **Pattern changes when you run longer** → the record was cut. The ring-down check reads your probes, not the NTFF surface, so it is a guide, not a proof. Repeat with a longer run (or `until_decay=`) and compare the patterns. - **Directivity looks wrong** → sample the full sphere; a single cut gives the wrong total power. - **Box too close to the radiator or touching the absorber** → preflight warns; move the faces out. It does not check plane-wave boundaries or microstrip feeds for you. - **Bistatic RCS with a strong forward-oblique lobe** → you left `subtract_incident_reference=False`; turn it on. A larger `ntff_offset` does not fix it. - **RCS of a new target** → only the monostatic sphere case is checked against Mie. Run your own convergence study and compare with a reference. - **Oblique incidence refuses your setup** → it needs `polarization="ez"`, a uniform grid, a z-invariant target and `ntff_offset <= tfsf_margin - 2`. - **Reporting an RCS** → state the incident direction, polarization, observation angles and frequency band with it. The [support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/support_matrix.md) lists the current status of each far-field and RCS path. --- # Visualization & Result Analysis Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/measure/plots-and-export/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/measure/plots-and-export.md This page shows how to plot rfx results, record field snapshots with correct positions and times, post-process results with NumPy, and export them to other tools. rfx's plotting helpers use matplotlib and return a `Figure`. They take plain arrays (`result.s_params`, `result.freqs`, `result.time_series`) or the result objects of the analysis functions (`FarFieldResult`, `RCSResult`). ## A result to work with The examples below share one small run: two lumped ports, a probe and an NTFF box. The mesh is coarse so the page runs quickly. ```python import numpy as np from rfx import GaussianPulse, Simulation sim = Simulation(freq_max=15e9, domain=(0.06, 0.06, 0.06), dx=1.5e-3, boundary="cpml", cpml_layers=6) for x_port in (0.028, 0.032): sim.add_port((x_port, 0.030, 0.030), "ez", impedance=50.0, waveform=GaussianPulse(f0=8e9, bandwidth=0.8)) sim.add_probe((0.030, 0.036, 0.030), "ez") sim.add_ntff_box(corner_lo=(0.012, 0.012, 0.012), corner_hi=(0.048, 0.048, 0.048), freqs=np.array([12e9])) result = sim.run(n_steps=800, compute_s_params=True) ``` ## Plots ### S-parameters and Smith chart ```python from rfx import plot_s_params, plot_smith fig = plot_s_params(result.s_params, result.freqs, db=True) # every S_ij in dB fig = plot_smith(np.asarray(result.s_params[0, 0]), np.asarray(result.freqs), z0=50.0) ``` `result.plot_s_params()`, `result.plot_smith()` and `result.plot_time_series()` are shortcuts for the same functions. ### Probe time series ```python from rfx import plot_time_series fig = plot_time_series(result.time_series, result.dt, labels=["probe"]) ``` ### Field slice `plot_field_slice` plots one component of the final field state on a plane. `index` is a grid index along `axis`, counting the absorber cells: ```python from rfx import plot_field_slice grid = result.grid k_mid = grid.position_to_index((0.030, 0.030, 0.030))[2] fig = plot_field_slice(result.state, grid, component="ez", axis="z", index=k_mid, title="Ez at z = 30 mm") ``` `result.state` holds the fields after the last step only. To see how the field evolves, record snapshots (below). ### Radiation pattern and RCS ```python from rfx import compute_far_field, plot_radiation_pattern theta = np.linspace(0.0, np.pi, 181) # radians phi = np.array([0.0]) ff = compute_far_field(result.ntff_data, result.ntff_box, result.grid, theta, phi) fig = plot_radiation_pattern(ff, freq_idx=0) ``` `plot_rcs(rcs_result, freq_idx=0, polar=True)` plots the `RCSResult` from `compute_rcs`. Both are covered on [Far-Field & RCS](https://remilab.ai/rfx/measure/far-field-and-rcs/). ## Field snapshots Pass a `SnapshotSpec` to `run(snapshot=...)` to record fields during the run. A slice through one plane keeps memory small: ```python from rfx import SnapshotSpec snap_sim = Simulation(freq_max=10e9, domain=(0.04, 0.04, 0.02), dx=1e-3, boundary="cpml", cpml_layers=6) snap_sim.add_source((0.020, 0.020, 0.010), "ez", waveform=GaussianPulse(f0=5e9, bandwidth=0.8), amplitude_kind="current") # slice_index counts grid cells from the outer edge of the absorber, which # lies outside the domain: z = 0 is index cpml_layers. k = 6 + round(0.010 / 1e-3) spec = SnapshotSpec(interval=10, components=("ez",), slice_axis=2, slice_index=k) snap_run = snap_sim.run(n_steps=300, snapshot=spec) frames = np.asarray(snap_run.snapshots["ez"]) # (n_frames, nx, ny) axes = snap_run.snapshot_axes["ez"] print(axes.dims) # ('frame', 'x', 'y') print(axes.times_s[:3]) # seconds, one per frame assert k == snap_run.grid.position_to_index((0.020, 0.020, 0.010))[2] x_m, y_m = axes.coords["x"], axes.coords["y"] # sample positions (m) ``` Use `axes.times_s` and `axes.coords` for your plot axes rather than working them out yourself: - Frames are `interval` steps apart. With the default `interval=10`, frame `k` is the field after step `10 * (k + 1)`, not after step `k`. A run of N steps records `N // interval` frames. - H components are recorded half a time step earlier than E, and each component sits at its own staggered position. `times_s` and `coords` account for both. - `slice_index` and the coordinate arrays include the absorber cells. `run(until_decay=...)` records snapshots too; the frame count then depends on where the run stopped. To see the frame layout before running, call `rfx.snapshot_axes(grid, spec, n_steps)`. ## Analysis with NumPy Results are NumPy or JAX arrays, so the usual tools work directly. ### Spectrum of a probe ```python ts = np.asarray(result.time_series[:, 0]) spectrum = np.fft.rfft(ts) f_fft = np.fft.rfftfreq(len(ts), d=result.dt) print(f"spectral peak at {f_fft[np.argmax(np.abs(spectrum[1:])) + 1] / 1e9:.2f} GHz") ``` A plain FFT of a record that has not decayed smears the spectrum. For resonant frequencies and Q, use `result.find_resonances()`; see [Probes and S-Parameters](https://remilab.ai/rfx/measure/s-parameters/#resonances-harminv). ### S-parameter quantities ```python freqs = np.asarray(result.freqs) s11 = np.asarray(result.s_params[0, 0]) s21 = np.asarray(result.s_params[1, 0]) z_in = 50 * (1 + s11) / (1 - s11) # input impedance (ohm) return_loss_db = -20 * np.log10(np.abs(s11)) vswr = (1 + np.abs(s11)) / (1 - np.abs(s11)) phase = np.unwrap(np.angle(s21)) group_delay = -np.gradient(phase) / np.gradient(2 * np.pi * freqs) # seconds ``` ### Stored energy (rough estimate) ```python EPS_0 = 8.8541878128e-12 MU_0 = 1.25663706212e-6 st = result.state u_e = 0.5 * EPS_0 * (st.ex**2 + st.ey**2 + st.ez**2) u_h = 0.5 * MU_0 * (st.hx**2 + st.hy**2 + st.hz**2) stored_energy = float(np.sum(u_e + u_h) * grid.dx**3) ``` This uses vacuum permittivity everywhere (it undercounts energy in dielectrics), treats the staggered components as co-located, and assumes a uniform cubic grid. On a graded mesh, weight each cell by its own volume. ## Export ```python import os import tempfile from rfx import load_snapshots, save_snapshots, save_state, write_touchstone out = tempfile.mkdtemp() # Touchstone for circuit tools. Shape is (n_ports, n_ports, n_freqs). write_touchstone(os.path.join(out, "device.s2p"), result.s_params, result.freqs, z0=50.0) # HDF5: the final field state. save_state(os.path.join(out, "fields.h5"), result.state, grid=result.grid) # HDF5: snapshots, with their positions and frame times. save_snapshots(os.path.join(out, "snapshots.h5"), snap_run.snapshots, grid=snap_run.grid, dt=snap_run.dt, axes=snap_run.snapshot_axes) loaded, meta = load_snapshots(os.path.join(out, "snapshots.h5")) times = meta["axes"]["ez"].times_s ``` Always pass `axes=result.snapshot_axes` to `save_snapshots`. With only `dt=`, a reader would naturally assume one frame per time step, which is wrong unless `interval=1`. `load_snapshots` returns the saved axes as `meta["axes"]`. Touchstone 2.0 options and reading files back are on [Probes and S-Parameters](https://remilab.ai/rfx/measure/s-parameters/#touchstone-export). When you hand a figure or file to someone else, keep the script, the rfx version and the settings (mesh, run length, calculator options) with it, so the result can be reproduced. ## Limits - **Frame times off by a factor of `interval`** → read `times_s` from `result.snapshot_axes`; don't assume frame k is step k. - **Snapshots of full 3-D fields run out of memory** → record a slice (`slice_axis`, `slice_index`), fewer components or a larger `interval`. - **FFT peaks don't match `find_resonances`** → the record hasn't decayed; run longer or use Harminv. - **Energy estimate looks low in a dielectric** → the estimate ignores `eps_r`; weight by the per-cell permittivity for a better number. --- # Probes and fields Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/measure/probes-and-fields/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/measure/probes-and-fields.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Resonances Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/measure/resonances/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/measure/resonances.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Probes and S-Parameters Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/measure/s-parameters/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/measure/s-parameters.mdx This page covers everything you read out of a run: field probes, power flux, resonances and S-parameters. It also shows how to check that a spectrum can be trusted, and how to de-embed and export S-parameters. Every example uses a coarse mesh so the page runs in about a minute on a laptop CPU. A real study would refine the mesh and run longer; see [Convergence Study](https://remilab.ai/rfx/evidence/mesh-convergence/). ## Field probes A probe records one field component at one Yee cell at every time step. It has no impedance and does not load the structure. | Call | Records | Where the data ends up | |---|---|---| | `add_probe(position, component)` | one component (`"ex"` … `"hz"`) | one column of `result.time_series` | | `add_vector_probe(position)` | all six components | six columns, in the order Ex, Ey, Ez, Hx, Hy, Hz | | `add_dft_plane_probe(axis=, coordinate=, component=, freqs=)` | one component on a whole plane, at chosen frequencies | `result.dft_planes[name]` | ```python import numpy as np from rfx import GaussianPulse, Simulation sim = Simulation(freq_max=5e9, domain=(0.05, 0.05, 0.05), dx=2.5e-3, boundary="cpml", cpml_layers=6) sim.add_source((0.015, 0.025, 0.025), "ez", waveform=GaussianPulse(f0=2.5e9, bandwidth=0.8), amplitude_kind="current") sim.add_probe((0.035, 0.025, 0.025), "ez") # column 0 sim.add_vector_probe((0.025, 0.035, 0.025)) # columns 1-6 sim.add_dft_plane_probe(axis="z", coordinate=0.025, component="ez", freqs=np.array([2e9, 3e9]), name="ez_mid") result = sim.run(n_steps=400) # short: rfx warns the record is cut ts = np.asarray(result.time_series) # shape (n_steps, 7) t = np.arange(ts.shape[0]) * result.dt # seconds ez_probe = ts[:, 0] hy_vector = ts[:, 1 + 4] # Hy of the vector probe plane = result.dft_planes["ez_mid"] print(plane.accumulator.shape) # (n_freqs, nx, ny), complex ``` Columns follow registration order across all probe calls, so a vector probe added after two point probes occupies columns 2–7. A position snaps to the nearest grid cell, so a probe can read a field up to half a cell from the coordinate you gave. If you omit `freqs` on a DFT plane, rfx uses `n_freqs` points from `freq_max / 10` to `freq_max`. A DFT plane accumulates its spectrum during the run, which is much cheaper than saving full snapshots. It is still the transform of a finite record: if the fields have not rung down when the run stops, the spectrum is wrong. See [Is the run finished?](#is-the-run-finished) below. ## Power flux and R/T `add_flux_monitor` accumulates the Poynting flux through a plane, per frequency. `flux_spectrum(monitor)` returns the net power through it, positive along `+axis`. If it returns exact zeros at every frequency while the fields are clearly nonzero, the float32 `E × H*` products underflowed; outside `jit`/`grad` rfx warns when it detects this. That is not a physical null: pass `flux_spectrum(monitor, exact_f64=True)` to recompute the sum in float64, or increase the source amplitude. To get reflection and transmission of a slab or scatterer, run twice: once without the object (the **reference run**, which measures the incident power) and once with it. Transmission is the ratio of the transmitted fluxes. Reflection needs the incident field removed first, and it must be removed from the **fields**, not from the fluxes. Flux is a product of E and H, so subtracting two fluxes leaves cross terms. `subtract_flux_monitors` subtracts the field accumulators for you. This example measures a 10 mm, ε_r = 4 slab in 2-D with a plane wave: ```python from rfx import Box, flux_spectrum, subtract_flux_monitors dx = 1e-3 freqs_rt = np.linspace(4e9, 14e9, 11) slab_lo, slab_hi = 95 * dx, 105 * dx # 10 cells, on grid planes def slab_run(with_slab): s = Simulation(freq_max=16e9, domain=(0.2, 8e-3, dx), dx=dx, boundary="cpml", cpml_layers=10, mode="2d_tmz") if with_slab: s.add_material("slab", eps_r=4.0) # A plane wave makes the transverse axes periodic: span them fully. s.add(Box((slab_lo, -1, -1), (slab_hi, 1, 1)), material="slab") s.add_tfsf_source(f0=9e9, bandwidth=0.9, polarization="ez", direction="+x") s.add_flux_monitor(axis="x", coordinate=0.06, freqs=freqs_rt, name="refl") s.add_flux_monitor(axis="x", coordinate=0.14, freqs=freqs_rt, name="trans") return s.run(n_steps=4000, until_decay=1e-3) ref = slab_run(with_slab=False) dut = slab_run(with_slab=True) incident = np.asarray(flux_spectrum(ref.flux_monitors["trans"])) T = np.asarray(flux_spectrum(dut.flux_monitors["trans"])) / incident scattered = subtract_flux_monitors(dut.flux_monitors["refl"], ref.flux_monitors["refl"]) R = -np.asarray(flux_spectrum(scattered)) / incident # reflected power flows -x print(np.round(R + T, 3)) ``` For a lossless slab `R + T` should be 1, and here it is to three decimals. That checks energy bookkeeping, not accuracy: preflight warns that 1 mm cells give only about 9 cells per wavelength in the slab at 16 GHz, so R and T themselves need a finer mesh before you compare them with the Fresnel result. Two rules for this measurement: - Keep both runs identical except for the object: same mesh, source, monitors and record length. - Use `until_decay=` so each run stops when the fields have died out, not at an arbitrary step. `add_flux_monitor(size=, center=)` limits the monitor to a finite rectangle; the [flux regions contract](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/flux_regions.md) explains how that rectangle lands on the grid. ## Resonances (Harminv) `result.find_resonances(freq_range=...)` fits the ring-down of a probe record as a sum of damped sinusoids (harmonic inversion by the matrix pencil method). It returns `HarminvMode` objects, strongest first, with `freq` (Hz), `Q`, `decay` (1/s), `amplitude`, `phase` and a relative fit `error`. The example below is a small PEC cavity fed by a 50 Ω wire port. The port's resistor loads the cavity, so the modes have a finite Q. The same run is used for S-parameters later on this page. ```python C0 = 299_792_458.0 a, b, h = 0.030, 0.024, 0.012 # cavity size (m) f_tm110 = C0 / 2 * np.sqrt(1 / a**2 + 1 / b**2) # analytic TM110 cav = Simulation(freq_max=10e9, domain=(a, b, h), dx=1e-3, boundary="pec") cav.add_port((0.0075, 0.006, 0.0), "ez", impedance=50.0, extent=h) # floor to ceiling cav.add_probe((0.020, 0.016, 0.006), "ez") cav_run = cav.run(n_steps=1500, compute_s_params=True) modes = cav_run.find_resonances(freq_range=(6e9, 10e9)) if not modes: raise RuntimeError("no mode found: lengthen the run or widen freq_range") for m in modes[:3]: print(f"f = {m.freq / 1e9:.3f} GHz Q = {m.Q:.0f}") print(f"analytic TM110: {f_tm110 / 1e9:.3f} GHz") ``` The strongest mode need not sit at the empty-cavity TM110 value: the wire port is a metal post with a resistor inside the cavity, so this is no longer the empty cavity. Harminv only works on the free ring-down, so it skips the start of the record where the source is still on. When `source_decay_time=None`, rfx estimates that interval from the centre of `freq_range` and a fixed fractional bandwidth of 0.8; it does not inspect your source. Pass `source_decay_time=` (seconds) or `waveform=` when your pulse is longer than that estimate. To pick a physical mode, check its field pattern or compare with an analytic value; don't assume the strongest peak is the one you want. The standalone `harminv(signal, dt, f_min, f_max)` works on any sampled signal. ## S-parameters: pick the calculator for your port S-parameters need a **port**: something that injects a signal and terminates the line in a known impedance. Each port family has its own calculator, and only that calculator gives S-parameters for it: | Port | Calculator | Result | |---|---|---| | lumped `add_port(...)` (one cell) | `run(compute_s_params=True)` | `result.s_params`, `result.freqs` | | wire `add_port(..., extent=...)` | `run(compute_s_params=True)` | `result.s_params`, `result.freqs` | | lumped or wire, inside `jax.grad` | `forward(port_s11_freqs=...)` | S11 per port in `ForwardResult.s_params` | | microstrip `add_msl_port(...)` | `compute_msl_s_matrix()` | `MSLSMatrixResult` (`.S`, `.freqs`, `.reliable`, …) | | rectangular waveguide `add_waveguide_port(...)` | `compute_waveguide_s_matrix()` | `WaveguideSMatrixResult` (`.s_params`, `.freqs`, …) | | coaxial `add_coaxial_port(...)` | `compute_coaxial_line_reflection()` (one-port) or `compute_coaxial_two_port()` | their own result objects | Every full matrix has shape `(n_ports, n_ports, n_freqs)` and is indexed `S[receiver, driven, freq]`, so `S[1, 0]` is S21. If you are unsure which calculator applies, ask the simulation. `sim.s_matrix_lane()` names it without running anything, and `sim.compute_s_matrix(**kwargs)` calls it for you (except for lumped and wire ports, which use `run()`). Probes, DFT planes, flux monitors and plane-wave sources have no reference impedance, so none of them can give S-parameters. Setting up each port family is covered in [Sources and Ports](https://remilab.ai/rfx/model/ports/) and [Waveguide Ports](https://remilab.ai/rfx/guide/waveguide-ports/). ### Lumped and wire ports Pass `compute_s_params=True` to `run()`. S is normalized to each port's `impedance`: ```python lp = Simulation(freq_max=5e9, domain=(0.05, 0.04, 0.02), dx=2.5e-3, boundary="cpml", cpml_layers=6) lp.add_port((0.015, 0.02, 0.01), "ez", impedance=50.0) lp.add_port((0.035, 0.02, 0.01), "ez", impedance=50.0) lp.add_probe((0.025, 0.02, 0.01), "ez") lp_run = lp.run(n_steps=600, compute_s_params=True) S = np.asarray(lp_run.s_params) # (2, 2, n_freqs) freqs = np.asarray(lp_run.freqs) # Hz s21_db = 20 * np.log10(np.abs(S[1, 0]) + 1e-30) ``` rfx runs one simulation per driven port to fill the matrix. Use `s_param_freqs=` to choose the frequencies. For one-port S11, aim for about **60 cells per wavelength** at the highest frequency you care about. At 30 cells per wavelength, magnitudes can be off by about 1 dB and resonant frequencies move by 1–3 %. A one-cell port reads its voltage at its node and its current half a cell toward the line. ### Completing a record that was cut short A resonant structure can ring long after the pulse ends. Rather than running it out, `run(..., ringdown=RingdownSpec())` fits the slow decay in the last part of the record and adds the unrecorded tail of each port spectrum in closed form: ```python from rfx.ringdown import RingdownSpec cav_rd = cav.run(n_steps=1500, compute_s_params=True, ringdown=RingdownSpec()) report = cav_rd.ringdown.report print(report.summary()) if report.ok: s11_completed = cav_rd.ringdown.s_params[0, 0] # same bins as cav_rd.freqs ``` What to know before you rely on it: - It supports wire ports only (`add_port(..., extent=...)`), at a fixed record length. On a uniform mesh it handles one wire port; on a graded mesh it handles several. Other port families, plane waves, `until_decay`, `devices=` and nonlinear materials are refused with the reason. - The fit window is `[window_start * T, T]` (default: the second half of the record). The source must be off in that window, or `run()` raises and tells you to lengthen the record. - `report.ok` is True when the completion was produced and every judged check passed. The deciding check is `WE`: it completes the same record again using poles fitted on a window that starts twice as early, and compares the two answers. `W2` (a shorter second window) is reported but not judged. - None of these checks is an error bound. They tell you whether the completion is self-consistent, not how close it is to the true answer. - If completion cannot be produced, `cav_rd.ringdown.s_params` is `None` and `report.summary()` says why. `result.s_params` is always the plain, uncompleted answer. In this example the plain record is cut while the cavity still rings, and its |S11| exceeds 1, which a passive port cannot do. The report notes that, and the completed S11 is passive. ### Microstrip lines Microstrip ports use `compute_msl_s_matrix()`. It runs one simulation per driven port and extracts S from a ladder of voltage and current probes downstream of each feed: ```python from rfx.boundaries.spec import Boundary, BoundarySpec from rfx.mesh_edges import edge_aware_profiles h_sub, w_trace, L = 0.75e-3, 1.5e-3, 0.013 substrate = Box((0, 0, 0), (L, 0.008, h_sub)) # The trace runs from port to port, so the ports terminate it. trace = Box((0.0035, 0.004 - w_trace / 2, h_sub), (L - 0.0035, 0.004 + w_trace / 2, h_sub)) # x and y mesh lines placed so the trace is solved at its drawn size. profiles = edge_aware_profiles((L, 0.008, 0.003), 0.25e-3, sheets=[trace], solids=[substrate]) board = Simulation( freq_max=10e9, domain=(L, 0.008, 0.003), dx=0.25e-3, cpml_layers=6, # The z = 0 PEC wall is the ground plane. boundary=BoundarySpec(x="cpml", y="cpml", z=Boundary(lo="pec", hi="cpml")), **profiles, ) board.add_material("sub", eps_r=3.66) board.add(substrate, material="sub") board.add_thin_conductor(trace) for x_feed, direction, name in ((0.0035, "+x", "p1"), (L - 0.0035, "-x", "p2")): board.add_msl_port((x_feed, 0.004, 0.0), width=w_trace, height=h_sub, direction=direction, eps_r_sub=3.66, name=name) print(board.s_matrix_lane()) # compute_msl_s_matrix msl = board.compute_msl_s_matrix(n_freqs=21) S_msl = np.asarray(msl.S) # (2, 2, 21) ``` `num_periods=40` is the default and sets the record length; don't shorten it without checking `msl.settling_db`. The edge-aware mesh makes this a microstrip S-matrix on a graded mesh, which the [support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/support_matrix.md) lists as **experimental**; the microstrip S-matrix on a uniform mesh is the supported path. This board has only 3 cells through the substrate and 6 across the trace, so it runs quickly but prints several warnings. They are the ones you should expect on your own coarse runs: - preflight reports that 3 cells through the substrate is below its recommendation of 4, that the graded in-plane mesh reaches the absorber runway, and that the automatic probe-offset solve is skipped on this graded board, so the clearance from the deepest probe to the next reflector is not enforced (set `n_probe_offset` on the ports, or keep the propagation axis uniform, if that clearance matters). (On a plain uniform grid the trace would be solved wider than drawn, because a sheet's free edge acts a fraction of a cell beyond its last grid node; preflight reports that as an error and the run stops. The `edge_aware_profiles` mesh above avoids it.) - a **low-signal** warning names three bins at the top of the band, and `msl.reliable` is False there; - a **passivity** warning reports that the largest singular value of S is slightly above 1 (1.005 here). Refining the mesh is the fix for all of them. For microstrip the substrate thickness, not the wavelength, usually sets the cell size: a reasonable starting point is 4 cells through the substrate (preflight warns below that), then refine and check that your answer stops moving. End lines at their ports, as here, rather than running them into the absorber. CPML is a much poorer termination for a two-conductor line than for a plane wave. #### The `reliable` mask `msl.reliable` is a boolean array `(n_ports, n_freqs)`. `reliable[p, k]` is False when, in at least one of the drive runs, both the voltage and the current at port `p`'s probes were weak at frequency `k` (below 10 % of that record's median over the band). S is returned unchanged either way. Read it like this: - **False:** too little signal reached that port's probes at that frequency to trust the number. Because every entry of S at a frequency comes from one joint solve, treat the whole `S[:, :, k]` slice as suspect, not just one entry. A deep, real notch can also trigger it, so check whether you expect one there. - **True:** the low-signal check did not fire. It is not a guarantee of accuracy. To keep only the bins where no port was flagged: ```python if msl.reliable is not None: # None when the run was traced by JAX keep = np.all(np.asarray(msl.reliable), axis=0) S_kept, f_kept = S_msl[..., keep], np.asarray(msl.freqs)[keep] ``` The fitted `msl.Z0` and `msl.beta` are diagnostics. S is formed from the measured voltages and currents with the analytic line impedance as reference, so a warning that the fitted Z0 deviates from Hammerstad–Jensen does not by itself change S. Refine the mesh before quoting Z0. ### Waveguide and coaxial ports These families have their own set-up rules and their own calculators: - Waveguide: `compute_waveguide_s_matrix(normalize=...)`. The `normalize` choice matters for strong reflectors. See [Waveguide Ports](https://remilab.ai/rfx/guide/waveguide-ports/). - Coax: `compute_coaxial_line_reflection()` builds its own line and returns one-port S11; `compute_coaxial_two_port()` gives a through-line 2-port. Use at least 4 cells across the annulus. See [Sources and Ports](https://remilab.ai/rfx/model/ports/). The [S-parameter support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.md) lists what each calculator has been compared against. ## Checking an S-matrix `network_quality_metrics(S)` checks any `(n_ports, n_ports, n_freqs)` array for finiteness, passivity and reciprocity. It only reads the array: ```python from rfx import network_quality_metrics q = network_quality_metrics(S, passivity_tol=1e-4, reciprocity_tol=1e-3) print(q["is_passive"], q["passivity_excess"], q["reciprocity_error"]) ``` The default tolerances are 1e-12, which a float32 result will rarely meet, so pass tolerances that match your precision. Other keys include `finite`, `max_abs_s`, `max_column_power` and `is_reciprocal`. The calculators also warn by themselves, but the checks differ by family: | Calculator | Automatic checks | |---|---| | `run(compute_s_params=True)` | non-finite entries, any single `|S| > 1.1` (not column power) | | `compute_msl_s_matrix`, `compute_waveguide_s_matrix` | single entries and total column power | | `compute_coaxial_two_port` | the shared passivity check | | `compute_coaxial_line_reflection` | none: read its `status`, `fit_residual` and `recurrence_residual` | A warning never changes the returned numbers. No warning means only that no check fired; it does not mean the S-matrix is correct. ## Is the run finished? Every spectrum on this page is the Fourier transform of a finite record. If the record stops while the structure still rings, the spectrum is wrong, and no single check catches every case. - `result.settling_db` is the worst share, over the frequencies the run's spectral results read, of the value still ringing past the record's end, estimated from poles fitted after the source ends. At or below −40 dB the record is long enough. `result.settling_witness["status"]` is `pass`, `fail`, `undetermined` or `absent` (nothing recorded to judge); only `pass` is a pass. - `compute_msl_s_matrix`, `compute_waveguide_s_matrix` and the coax two-port calculators check each drive run against the same −40 dB bar and warn when a record ends too early. Their results carry `settling_db` per drive. - When a run has an NTFF box or a DFT plane, `run()` warns if a probe is still ringing at the end, and warns that the check is missing if there is no probe. ```python print(f"cavity settling: {cav_run.settling_db:.1f} dB") print("MSL settling per drive:", msl.settling_db) ``` The PEC cavity above is far from settled at 1500 steps, which is why ring-down completion is useful there. To fix a short record, use a longer `n_steps`, `until_decay=`, or `num_periods=` on the calculators. A structure with quality factor Q decays over roughly Q / (π f) seconds. [Recorded-probe settling](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/probe_settling.md) defines the check exactly. ## De-embedding `rfx.deembed` post-processes an S-matrix to move reference planes or remove a fixture: | Function | Removes | |---|---| | `deembed_port_extension(S, freqs, port_lengths, eps_eff=)` | the phase of a matched feed line at each port (reference plane moves inward) | | `deembed_thru(S, S_thru)` | a symmetric two-port fixture measured as a thru | | `deembed_series_impedance(S, freqs, series_z)` / `deembed_series_inductance(S, freqs, inductances)` | a known series element at each port of a 2-port | | `deembed_line_segment(S, freqs, segments)` | a short lossless line segment at each port of a 2-port | ```python from rfx import deembed_port_extension S_shifted = deembed_port_extension(S, freqs, port_lengths=[2.5e-3, 2.5e-3], eps_eff=1.0) ``` ## Touchstone export `write_touchstone` takes the `(n_ports, n_ports, n_freqs)` array and the frequencies in Hz. `read_touchstone` returns `(S, freqs, z0)`; `read_touchstone_full` also returns per-port reference impedances and metadata. ```python import os import tempfile from rfx import read_touchstone, read_touchstone_full, write_touchstone out = tempfile.mkdtemp() write_touchstone(os.path.join(out, "two_port.s2p"), S, freqs, z0=50.0) S_back, f_back, z0 = read_touchstone(os.path.join(out, "two_port.s2p")) # Touchstone 2.0 with explicit per-port reference impedances. write_touchstone(os.path.join(out, "two_port_v2.s2p"), S, freqs, version="2.0", layout="standard", port_z0=[50.0, 50.0]) network = read_touchstone_full(os.path.join(out, "two_port_v2.s2p")) ``` For plotting S-parameters, fields and time series, see [Visualization and Analysis](https://remilab.ai/rfx/measure/plots-and-export/). ## Limits - **Probe or flux spectrum looks noisy or has ripples** → the record ended before the fields decayed. Check `settling_db`, then run longer or use `until_decay=`. - **Reflection from two flux runs looks wrong** → subtract the fields with `subtract_flux_monitors`, not the flux values, and keep the two runs identical apart from the object. - **Lumped or wire S11 is off by around 1 dB** → the mesh is too coarse; aim for about 60 cells per wavelength at the top frequency. - **`reliable` is False at some bins** → don't plot, fit or optimize those bins as data; refine the mesh, lengthen the record, or check whether a real notch is there. - **Passivity warning (|S| or column power above 1)** → usually a record that ended too early or a mesh too coarse. The returned S is not clipped. - **Microstrip line runs into the absorber** → CPML reflects a guided line noticeably; end the line at its port instead. - **Coax refuses the model with a lateral-absorber message** → the line's outer wall is within one cell of a side absorber; widen the domain. - **Harminv returns nothing, or a mode you did not expect** → lengthen the record, narrow `freq_range`, and identify modes by field pattern. - **Ring-down completion refuses the run** → the source is still on in the fit window; lengthen the record or raise `window_start`. --- # Boundaries Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/model/boundaries/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/model/boundaries.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Conductors Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/model/conductors/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/model/conductors.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Non-Uniform Mesh Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/model/domain-mesh/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/model/domain-mesh.mdx A graded mesh uses small cells only where the geometry needs them, such as a thin substrate, and large cells in the air around it. This page shows how to build a cell-size profile, pass it to `Simulation`, and check that your geometry lands where you drew it. Start with a uniform grid. Switch to a graded one when a thin feature would make a uniform grid too large, and then compare the final result against a uniform run or another reference. ## When a graded mesh pays off A 1.6 mm substrate at 0.5 mm cells gets only 3 cells, which is too coarse. Shrinking a uniform cell to 0.2 mm resolves it, but it refines all three axes: about 15× more cells. A graded z profile puts 0.27 mm cells in the substrate and 1.5 mm cells in the air, and leaves x and y alone. | Situation | Mesh | |---|---| | substrate much thinner than a wavelength | graded z | | a stack of several thin layers | graded z, one fine band per layer | | patch or trace edges whose size matters | in-plane profiles from `edge_aware_profiles`, required unless `snap="declared"` | | bulk 3-D structure, no thin features | uniform | | far field or RCS of a bulk object | uniform | ## Building a z profile A `dz_profile` is a 1-D array of cell sizes in metres, from z = 0 upward. It covers the physical domain; rfx adds the absorber cells itself. The easiest way to build one is `make_band_profile`. Give it every interface, including both domain ends, and a target cell size per segment: ```python import numpy as np from rfx.nonuniform import make_band_profile h = 1.6e-3 # substrate thickness below = 20e-3 # air below the substrate above = 30e-3 # air above it dz_profile = make_band_profile( edges=[0.0, below, below + h, below + h + above], cell_sizes=[1.5e-3, h / 6, 1.5e-3], # 6 cells through the substrate protected=[False, True, False], # keep the substrate uniform max_ratio=1.3, ) print(len(dz_profile), dz_profile.min(), dz_profile.max()) ``` The result has every interface on a cell boundary (to 1e-12 m), every neighbouring-cell ratio at most `max_ratio`, and a sum equal to the domain height exactly. The ramps between sizes are placed in the unprotected segments. Rules of thumb: - **At least 4 cells** through a substrate; 6–8 when the field inside it matters. - **Neighbour ratio ≤ 1.4 in z** (the validated limit), and ≤ 1.3 in x and y. Preflight warns above that. - **Interfaces on cell boundaries.** `make_band_profile` does this for you. - **Uniform cells next to the absorber.** Leave room for `cpml_layers` full-size cells between the last ramp and each absorbing face. Here the 20 mm below and 30 mm above the substrate are enough for 8 layers. Preflight warns (`nu_grading_reaches_absorber`) when a ramp reaches into that runway. Two alternatives: - `make_z_profile(features, domain_z, dx_fine, dx_coarse, grading)` puts fine cells at each listed z plane and at both ends, with ramps between. - A hand-built array with `np.concatenate`. If you do this, `rfx.smooth_grading()` can insert ramp cells, but it shifts every interface above them, so check the cumulative edges afterwards. Neither builder needs it. ## Using the profile ```python from rfx import Box, Simulation from rfx.mesh_edges import edge_aware_profiles sub_lo, sub_hi = below, below + h board = Box((0.01, 0.01, sub_lo), (0.09, 0.07, sub_hi)) ground = Box((0.01, 0.01, sub_lo), (0.09, 0.07, sub_lo)) # This horizontal sheet needs x/y registration; keep the layered z profile. profiles = edge_aware_profiles( (0.10, 0.08, float(np.sum(dz_profile))), 1e-3, sheets=[ground], solids=[board], axes="xy", ) sim = Simulation( freq_max=4e9, domain=(0.10, 0.08, 0.0), # z = 0: use sum(dz_profile) dx=1e-3, dz_profile=dz_profile, **profiles, boundary="cpml", cpml_layers=8, ) z_edges = np.concatenate(([0.0], np.cumsum(dz_profile))) assert np.isclose(z_edges, sub_lo).any() and np.isclose(z_edges, sub_hi).any() sim.add(board, material="fr4") sim.add_thin_conductor(ground) report = sim.preflight() print(report.format()) ``` `domain[2] = 0` tells rfx to use `sum(dz_profile)` as the height. A positive value is kept as given and is not checked against the profile, so pass `float(np.sum(dz_profile))` if you set it yourself. Keep the interfaces on node planes, as the `assert` above checks. A sheet conductor lands on the nearest node plane, so a substrate face between nodes leaves a gap between the copper and the dielectric. Preflight's `graded_box_rasterization` warning fires when a thin `Box` gets far fewer cells than the fine spacing nearby implies; fix the profile rather than the warning. ### Grading x and y `dx_profile` and `dy_profile` work the same way, with one extra rule: both end cells must equal the scalar `dx`, so every absorber face sees the same cell. `make_band_profile(..., boundary_cell=dx)` does this. The same uniform-runway rule applies next to each absorbing face. Keep fine bands away from absorbing faces, or close those faces with PEC. ### Solving sheets at their drawn size A PEC sheet's in-plane edge is solved about 0.35 of a cell beyond its last grid node, so a patch or trace comes out slightly larger than drawn. A PEC sheet whose free-edge size is solved more than 1 % off its drawing is refused by `run()`, `forward()` and `compute_mixed_s_matrix()`. Preflight reports this as a `sheet_effective_size` error. Build profiles with `edge_aware_profiles` to place sheet edges where they solve at their drawn size. Use `axes="xyz"` and pass the returned `dz_profile` too for vertical sheets; the horizontal ground above only needs x/y registration and keeps its layered z profile. Alternatively, `Simulation(snap="declared")` accepts the difference as a warning and records the choice on `Simulation.realized_geometry()`; it changes neither the mesh nor the solved geometry. Ends at seams, overlaps and T-joins with other sheet metal, at or past a domain face, or joined to a realized PEC volume are not judged. A gap of one cell or more to a block still leaves a free edge. These rules and the tolerance are implemented in `rfx/mesh_edges.py::solved_sheet_span` and `rfx/preflight/pec_geometry.py::_warn_sheet_effective_size`. For a patch and board: ```python from rfx.mesh_edges import edge_aware_profiles LX, LY, LZ = 0.04, 0.03, 0.01 board = Box((0.0, 0.0, 0.0), (LX, LY, 1.6e-3)) patch = Box((0.0105, 0.0083, 1.6e-3), (0.0295, 0.0217, 1.6e-3)) profiles = edge_aware_profiles((LX, LY, LZ), 1e-3, sheets=[patch], solids=[board]) patch_sim = Simulation(freq_max=6e9, domain=(LX, LY, LZ), dx=1e-3, boundary="cpml", cpml_layers=8, **profiles) patch_sim.add(board, material="fr4") patch_sim.add_thin_conductor(patch) ``` Draw the geometry at its true size; do not shift it to compensate. ## Letting rfx choose the mesh `auto_configure` sizes the cell, the domain, the absorber and the run length from your geometry and band, and builds a `dz_profile` when it finds thin z features: ```python from rfx import auto_configure cfg = auto_configure( geometry=[(Box((0, 0, 0), (0.10, 0.08, 1.6e-3)), "fr4")], freq_range=(1e9, 4e9), materials={"fr4": {"eps_r": 4.4, "sigma": 0.025}}, accuracy="standard", ) print(cfg.summary()) auto_sim = Simulation(**cfg.to_sim_kwargs()) ``` `cfg.uses_nonuniform` tells you whether it chose a graded z mesh. Its profile can step by more than the 1.4 ratio (it does here, and the `Simulation` constructor warns), so treat it as a starting point. It plans from the dielectrics you pass. Add conductors to the simulation afterwards, as sheets or as volumes at least one cell thick. If you create a `Simulation` without `dx`, rfx picks a mesh from the registered geometry automatically; see [Materials and Geometry](https://remilab.ai/rfx/model/materials/#freeze-the-mesh-before-placing-geometry-from-it) for placing geometry on an automatic mesh. ## Time step and cost The time step follows the **smallest** cell anywhere in the grid: ```text dt = 0.99 / (c * sqrt(1/dx^2 + 1/dy^2 + 1/dz_min^2)) ``` With 1.5 mm cells in x and y and a 0.133 mm minimum in z, that is about 0.44 ps, against 2.86 ps for a uniform 1.5 mm grid. A graded mesh has far fewer cells but needs more steps. Whether it is faster overall depends on the model. To estimate memory before a run, especially for gradients: ```python plan = sim.plan_ad_memory(n_steps=10_000, available_memory_gb=8.0) mesh_report = sim.mesh_intelligence_report( n_steps=10_000, checkpoint_every=plan.checkpoint_every, available_memory_gb=8.0) print(plan.recommendation) print(f"{mesh_report.cell_savings_factor:.1f}x fewer cells than uniform-fine") ``` These are planning numbers. They say nothing about accuracy. ## What runs on a graded mesh | Feature | Graded mesh | |---|---| | point sources, probes | yes | | lumped RLC elements | in the field update; no S-parameters | | NTFF far field | limited: one dipole case checked against theory | | DFT planes, flux monitors | run; no general accuracy statement | | wire-port S-parameters (`add_port(..., extent=...)`) | experimental; compare with a uniform run | | one-cell lumped-port S-parameters | refused | | `compute_msl_s_matrix` | experimental; `mode="eigenmode"` refused | | `compute_waveguide_s_matrix` | single-mode, `normalize=True` or `"flux"` | | coaxial calculators | refused | | TFSF plane wave | normal incidence along ±x only | | Floquet ports, UPML | refused | Unsupported combinations raise before the first step instead of running with a feature silently dropped. On a graded mesh, `run()` refuses snapshots and `conformal_pec=True`, and on a closed (PEC) boundary it refuses `until_decay`; use a fixed `n_steps` there. ## Limits - **Accuracy is per observable.** A graded run that completes is not evidence that its S-parameters, flux or far field are right. Compare with a uniform run or an external reference. - **Grading ratios** above 1.4 in z, or 1.3 in x and y, are outside the validated range. Preflight warns. - **TFSF along a graded axis:** preflight warns, because the scattered-field region picks up a spurious field (about 0.7 of the total-field peak with no scatterer). Normal incidence along ±x needs a uniform x axis. - **Microstrip probes on a graded feed:** the automatic probe offset and spacing are counted in cells under the port, and recounted at run time. Keep the feed region uniform if you want predictable probe positions. - **Graded-mesh waveguide results** beyond the published WR-90 comparisons are experimental. A dz-only profile has no accuracy evidence yet. - **Subgridding (`add_refinement`)** is a different tool: it works only through `run()` on a uniform mesh, and `forward()`, `optimize()` and `topology_optimize()` refuse a refined model. - The full per-combination status is in the [support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/support_matrix.md). --- # Materials and Geometry Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/model/materials/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/model/materials.mdx A structure in rfx is a shape plus a material: pick a library material or register your own with `add_material()`, then attach it to a shape with `sim.add()`. This page covers materials, shapes, and exactly how metal is placed on the grid. For the short version of the three conductor kinds, see [Core Concepts](https://remilab.ai/rfx/start/concepts/). ## Library materials `MATERIAL_LIBRARY` holds common RF materials. Use them by name; no registration is needed. ```python from rfx import MATERIAL_LIBRARY, Box, Simulation print(list(MATERIAL_LIBRARY)) sim = Simulation(freq_max=10e9, domain=(0.05, 0.05, 0.02), boundary="cpml") sim.add(Box((0.005, 0.005, 0.005), (0.045, 0.045, 0.0066)), material="fr4") # A 1 mm PEC plate. It has real thickness, so it is a volume (see below). sim.add(Box((0.005, 0.005, 0.004), (0.045, 0.045, 0.005)), material="pec") ``` | Name | ε_r | σ (S/m) | Notes | |---|---|---|---| | `vacuum` | 1.0 | 0 | | | `air` | 1.0006 | 0 | | | `fr4` | 4.4 | 0.025 | lossy PCB laminate | | `rogers4003c` | 3.55 | 2.67×10⁻³ | σ from tan δ 0.0027 at 5 GHz | | `rogers4350b` | 3.48 | 7.16×10⁻³ | σ from tan δ 0.0037 at 10 GHz | | `rt_duroid_5880` | 2.20 | 1.10×10⁻³ | σ from tan δ 0.0009 at 10 GHz | | `alumina` | 9.8 | 0 | | | `silicon` | 11.9 | 0.01 | lightly doped | | `ptfe` | 2.1 | 0 | | | `copper` | 1.0 | 5.8×10⁷ | treated as PEC | | `aluminum` | 1.0 | 3.5×10⁷ | treated as PEC | | `pec` | 1.0 | 1×10¹⁰ | perfect conductor | | `water_20c` | 4.9 + one Debye pole | 0 | water at 20 °C | The laminate σ values are a constant conductivity that matches the datasheet loss tangent at one frequency. Loss away from that frequency scales as 1/f, so it is not a frequency-dependent loss model. When you omit `dx`, rfx picks the mesh from your geometry and materials, and `preflight()`, `fidelity_report()` and `run()` all use that same mesh. Adding geometry can change it. To differentiate with respect to geometry or mesh inputs, pass an explicit `dx` (and any profiles) up front. ## Custom materials ```python sim.add_material("my_ceramic", eps_r=9.0, sigma=0.001, mu_r=1.0) sim.add(Box((0.01, 0.01, 0.0), (0.04, 0.04, 0.002)), material="my_ceramic") ``` `add_material()` and `add()` return the simulation, so you can chain them: ```python (sim .add_material("sub", eps_r=3.55, sigma=0.0) .add_material("metal", eps_r=1.0, sigma=5.8e7) .add(Box((0, 0, 0), (0.05, 0.03, 0.001)), material="sub") .add(Box((0.005, 0.005, 0.001), (0.045, 0.025, 0.0015)), material="metal")) ``` Any material with σ ≥ 10⁶ S/m, such as `"metal"` above, becomes a perfect conductor. See [How conductors land on the lattice](#how-conductors-land-on-the-lattice). ## Dispersive materials ### Debye poles A Debye pole models dipolar relaxation (water, tissue): ε(ω) = ε_∞ + Δε / (1 + jωτ). `eps_r` is ε_∞. ```python from rfx import DebyePole sim.add_material("water_body", eps_r=4.9, debye_poles=[DebyePole(delta_eps=74.1, tau=8.3e-12)]) ``` `delta_eps` is εₛ − ε_∞ and `tau` is the relaxation time in seconds. ### Lorentz and Drude poles Build Lorentz and Drude poles with the helpers and pass them as `lorentz_poles=[...]`: ```python import numpy as np from rfx import drude_pole, lorentz_pole # Lorentz oscillator: strength, resonance (rad/s), damping (rad/s). sim.add_material("resonant", eps_r=1.0, lorentz_poles=[ lorentz_pole(delta_eps=2.0, omega_0=2 * np.pi * 10e9, delta=1e8)]) # Drude plasma: plasma frequency and collision rate, both in rad/s. sim.add_material("plasma", eps_r=1.0, lorentz_poles=[drude_pole(omega_p=2 * np.pi * 10e9, gamma=2 * np.pi * 1e9)]) ``` Both helpers return a `LorentzPole(omega_0, delta, kappa)`. Use the helpers rather than building that tuple yourself. A Drude or Lorentz medium has its own time-step limit, and a medium above it is refused when the model is assembled, with the largest stable step in the message. A metal at its optical plasma frequency (gold: ω_p ≈ 1.37×10¹⁶ rad/s) is far above that limit on any RF cell, and no stable step is found. Model RF metal as a conductor (`material="pec"`, a thin conductor, or a finite `sigma`) instead. ## Shapes Coordinates are in metres. Shapes are rasterized onto the grid when the simulation is built. ```python from rfx import Cylinder, Sphere slab = Box((0.0, 0.0, 0.0), (0.05, 0.05, 0.002)) # corner_lo, corner_hi ball = Sphere((0.025, 0.025, 0.010), radius=0.005) # center, radius via = Cylinder((0.025, 0.015, 0.001), radius=0.0005, # center, radius, height=0.002, axis="z") # height, axis sim.add(ball, material="alumina") sim.add(via, material="copper") ``` `Cylinder` takes its centre point, not its base: the via above spans z = 0 to 2 mm. rfx also provides `PolylineWire`, `Via` and `CurvedPatch`; see the [Geometry and Materials API](https://remilab.ai/rfx/api/geometry-materials/). ### CAD import `MeshShape` imports STL, OBJ and PLY files, and STEP/STP through the CAD extra (`pip install "rfx-fdtd[cad]"`). `scale` converts the file's length unit to metres: ```python import tempfile from pathlib import Path import trimesh from rfx import MeshShape # A stand-in CAD part: 30 x 20 x 2 mm, drawn in millimetres. with tempfile.TemporaryDirectory() as tmp: stl = Path(tmp) / "bracket.stl" trimesh.creation.box(extents=(30.0, 20.0, 2.0)).export(stl) part = MeshShape.from_file(str(stl), scale=1e-3, translate=(0.025, 0.025, 0.010)) sim.add(part, material="pec") ``` STL in millimetres needs `scale=1e-3`. STEP files are converted to metres on import, so use `scale=1.0`. `MeshShape` is not differentiable: it raises on traced coordinates. Use `Box`, `Sphere` or `Cylinder` for any shape that must be a design variable. ### Draw order There is no Boolean CSG. Shapes are rasterized in the order you add them, and where they overlap, a later dielectric overwrites an earlier one. To carve a hole in a dielectric, add the hole's material second: ```python sim.add(Box((0.010, 0.010, 0.0), (0.040, 0.040, 0.004)), material="alumina") sim.add(Box((0.020, 0.020, 0.0), (0.030, 0.030, 0.004)), material="air") ``` This works for dielectrics only. Conductors accumulate and are never removed, so a later shape cannot cut a hole in metal. Draw metal only where metal should be. ## How conductors land on the lattice This section is the full rule. Other pages link here instead of restating it. Any material with σ ≥ 10⁶ S/m is a **conductor**. rfx zeroes the tangential E on it instead of using σ in the update, which would be unstable. This covers `pec`, `copper` and `aluminum`, so all three are lossless. To model a lossy bulk material, keep σ below 10⁶ S/m. For conductor loss on a metal sheet, use `add_thin_conductor(..., surface_impedance_f0=f0)` (see [Thin conductors](#thin-conductors)). ### The rule **An E component is PEC if and only if its location lies inside the closed conductor region.** Every conductor is exactly one of three kinds, and your declaration decides which. rfx never infers the kind from how a shape happens to rasterize. | Kind | What it is | Declare it with | An E edge is PEC when | |---|---|---|---| | **volume** | a set of cells | `sim.add(shape, material="pec")` with `Box`, `Sphere`, `Cylinder` | the edge touches an occupied cell | | **sheet** | a footprint on one node plane, zero thickness | `sim.add_thin_conductor(shape)`, or a zero-thickness `Box` in `sim.add()` | the edge lies in the plane and both its end nodes are in the footprint | | **wire** | a path of edges | `PolylineWire` thinner than half a local cell | the edge lies on the path | ### Where things sit Node `i` is at `x_i = i·Δx`. Cell `i` spans `[x_i, x_{i+1}]`. `Ex[i,j,k]` sits at `(x_{i+½}, y_j, z_k)`, and `Ey`, `Ez` likewise are offset by half a cell along their own axis. - **Volumes** are sampled at cell centres: cell `i` is occupied when `lo ≤ x_{i+½} < hi`. A box drawn on node planes occupies exactly the cells between them. An off-lattice face rounds to the nearest node plane. - **Dielectrics** are sampled at nodes, half-open. - **Sheets** land on the node plane nearest the shape's mid-plane along its normal. An exact half-cell tie goes to the lower plane. ### What follows - A PEC `Box` from `z_a` to `z_b` on node planes has walls at **both** faces, and every edge between them is shorted. The realized thickness equals the drawn thickness, including for a one-cell plate. - A sheet owns no cell. It writes no ε_r or σ, and the E component normal to it stays live. - Sheets on the same plane are merged first, so abutting sheets join without a seam. Sheets on adjacent planes stay two separate films. - For a sheet to sit on a dielectric face, that face must be on a node plane. Node planes are integer multiples of `dx` from the domain origin. Choosing `dx = h_sub / n` sets the laminate's thickness in cells but does not put its face on a node; the face's offset must also be a whole number of cells. Otherwise the sheet snaps to the nearest node, and preflight reports the offset. - The rule does not depend on axis order or mirroring. `sim.fidelity_report()` prints each conductor's drawn extent next to the realized wall planes, in your units. Read it whenever placement matters. ### What rfx refuses These declarations are accepted by `sim.add()` or `add_thin_conductor()`, but `sim.preflight()` reports them as errors ("cannot be realized on this lattice"), and `run()` and `forward()` raise `ValueError`: - A PEC shape with `0 < thickness < one local cell` on any axis. It is neither a sheet nor a resolved volume. Declare a sheet, or resolve the thickness. - A PEC shape that rasterizes to zero cells, such as a sphere smaller than a cell. Metal never vanishes silently. - A PEC `Box` with two or three zero-thickness axes. A line or a point is not a conductor; use `PolylineWire`. - An `add_thin_conductor()` shape thicker than one cell along its normal. That is a volume; use `add()`. A PEC `Box` with exactly **one** zero-thickness axis is fine: it is a sheet, realized exactly as `add_thin_conductor()` would realize it. The removed `two_plane=True` keyword on `sim.add()` raises `TypeError` immediately. The volume rule already gives every plate both faces. ### Metal that is not a declared conductor Three things put metal in a domain outside this rule. Know which one a result came from: - **A PEC boundary** (`boundary="pec"` or a `BoundarySpec` face) zeroes tangential E on the domain face. It is part of the domain, not a body. - **A σ fill through the low-level `rasterize()`**, such as `rasterize(grid, [(shape, 1.0, 1e7)])`, writes a large conductivity into cells and lets the update damp the field. It is a lossy-volume model, not PEC: no edge is zeroed and `fidelity_report()` does not list it. Some scattering examples use it on purpose because TFSF illumination takes material arrays directly. - **Subpixel smoothing and conformal PEC** (`subpixel_smoothing="kottke_pec"`, `conformal_pec=True`) use their own weights near curved metal. Coax conductors built by the coaxial port are realized PEC, not a σ fill; see [Sources and Ports](https://remilab.ai/rfx/model/ports/#coaxial-ports). ### Freeze the mesh before placing geometry from it With an automatic mesh, adding geometry can refine the mesh and move node planes. If you place conductors using grid coordinates, first register the geometry that drives the mesh, then call `sim.freeze_mesh()`: ```python frozen = Simulation(freq_max=10e9, domain=(0.03, 0.03, 0.03), boundary="pec") grid = frozen.freeze_mesh() d = float(grid.dx) frozen.add(Box((5 * d, 5 * d, 5 * d), (9 * d, 9 * d, 6 * d)), material="pec") ``` Reading the grid or running preflight does not freeze it. After freezing, a feature the mesh cannot resolve is still refused. On a non-uniform grid, use the node coordinates, since `grid.dx` is only the boundary spacing. Register ports and boundaries before you keep array indices, because they can change the padding. Alternatively, pass explicit `dx=` and profiles from the start. ### Worked example: foil is a sheet 35 µm copper under a 0.787 mm RO4003C laminate, with `dx = h_sub / 4`. The copper is a fifth of a cell thick, so a `Box` in `add()` could not be realized: preflight flags it and `run()` raises. Declare it as a sheet, and put the laminate floor on a node plane by writing its position as a whole number of cells: ```python H_SUB = 0.787e-3 DX = H_SUB / 4 # 196.75 um: four cells through the laminate Z_GND = 20 * DX # node plane 20 XY_LO, XY_HI = 25 * DX, 175 * DX # nodes 25 and 175 board = Simulation(freq_max=12e9, domain=(0.04, 0.04, 0.012), dx=DX, boundary="cpml") board.add(Box((XY_LO, XY_LO, Z_GND), (XY_HI, XY_HI, Z_GND + H_SUB)), material="rogers4003c") board.add_thin_conductor(Box((XY_LO, XY_LO, Z_GND), (XY_HI, XY_HI, Z_GND))) board.fidelity_report() ``` The report has no findings. The z rows read: ```text geometry[0] 'rogers4003c' z: declared [3935.0, 4722.0] um -> realized [3935.0, 4722.0] um | face residuals (0.0, 0.0) um | extent 787.0 -> 787.0 um thin_conductor[0] sigma_bulk=5.8e+07 S/m -> pec — PEC sheet on node plane z = 3935.00 um (...) z: declared [3935.0, 3935.0] um -> realized [3935.0, 3935.0] um | face residuals (0.0, 0.0) um | extent 0.0 -> 0.0 um ``` A floor at a round `z = 4 mm` would not be a node plane (4 mm / 196.75 µm = 20.33). The sheet would land at node 20, 65 µm below the dielectric. Drawing the foil as a one-cell `Box` is also wrong: it becomes a volume that takes the laminate's bottom cell, and the report flags that 25 % of the dielectric's cells are also PEC. ### Worked example: a plate is a volume A 2 mm iris in a WR-90 guide at `dx = 1 mm`: ```python guide = Simulation(freq_max=12e9, domain=(0.30, 0.02286, 0.01016), dx=1e-3, boundary="cpml") guide.add(Box((0.145, 0.0, 0.0), (0.147, 0.02286, 0.01016)), material="pec") ``` It is realized with faces at x = 145 mm and 147 mm and everything between them shorted, so a closed-form model can use the drawn 2 mm directly. `fidelity_report()` lists three wall planes (145, 146 and 147 mm) because the interior plane is shorted too. `max − min` of those planes is the plate's outer span. To measure an aperture, take the gap between wall groups. ## Thin conductors Use `add_thin_conductor()` for printed copper and resistive films that are thinner than a cell: ```python trace = Box((0.010, 0.014, 0.0016), (0.040, 0.016, 0.0016)) sim.add_thin_conductor(trace, sigma_bulk=5.8e7, thickness=35e-6) ``` `sigma_bulk` and `surface_impedance_f0` choose the model: | Setting | Model | |---|---| | `sigma_bulk >= 1e6` (default copper) | lossless PEC sheet on one node plane. `thickness` is ignored. rfx warns. | | `sigma_bulk < 1e6` (resistive film, ink) | lossy sheet with conductivity `sigma_bulk * thickness / d`, preserving the sheet resistance 1/(σ·t) | | `surface_impedance_f0=f0` | resistive sheet with Rs = √(π f0 μ0 / σ_bulk), same footprint and edges as the PEC sheet | Here `d` is the cell size normal to the sheet. On a graded mesh it is `(d[k-1] + d[k]) / 2`, the average of the two cells either side of the sheet plane, so you get the sheet resistance you asked for wherever the sheet lands. The surface-impedance model is a thick-conductor (Leontovich) loss: - It adds loss only. It does not model the surface reactance, so resonances read slightly high. - Rs is fixed at `f0` for the whole run. Its error grows as |√(f/f0) − 1| away from `f0`. - It is valid for conductors at least about three skin depths thick. For thinner films, use the `sigma_bulk < 1e6` path instead. - The sheet can have any shape (discs, meanders, `MeshShape` outlines, ground planes with holes), but it must rasterize to exactly one cell layer. A surface-impedance sheet does not appear in `pec_mask` or in the σ array. To find all metal in a model, use `sim.conductor_mask()`. A sheet owns no cell, so it does not take the material of the cells around it. If you leave a vacuum slot for the foil between two dielectric boxes, the E edges on the sheet plane sit in vacuum and the stack-up gains a thin air layer. Extend the dielectric to the sheet plane; preflight flags the slot. ## Limits - **Metal loss.** Every metal in the library is lossless PEC. For conductor loss on a sheet, use `surface_impedance_f0`. There is no volumetric metal loss. - **Laminate loss is constant σ.** It matches tan δ at one frequency only. For broadband loss, fit Debye or Lorentz poles to your material data. - **Off-grid geometry moves.** A face between node planes lands on the nearest one, up to half a cell away. Check `fidelity_report()`, and refine instead of rounding your design to the grid. - **Surface-impedance sheets** run on the uniform and single-device graded-mesh `run()` and S-parameter paths. Distributed, subgridded, ADI, UPML, multimode-waveguide, MSL-junction and optimization paths refuse them. See the [support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/support_matrix.md). - **`MeshShape`** is not differentiable. --- # Sources and Ports Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/model/ports/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/model/ports.mdx This page shows how to excite a model. A **source** injects a field and has no impedance. A **port** also terminates the line in a known impedance, so rfx can compute S-parameters from it. [Core Concepts](https://remilab.ai/rfx/start/concepts/#sources-and-ports) explains the difference. Each port family has its own S-parameter calculator. Only that calculator gives S-parameters for it: | Structure | Port | Calculator | |---|---|---| | discrete feed, component terminals | `add_port(...)` (lumped; with `extent=` a wire port) | `run(compute_s_params=True)`, or `forward(port_s11_freqs=...)` for gradients | | microstrip line | `add_msl_port(...)` | `compute_msl_s_matrix()` | | rectangular waveguide | `add_waveguide_port(...)` | `compute_waveguide_s_matrix()` | | coaxial line | `add_coaxial_port(...)` | `compute_coaxial_line_reflection()`, `compute_coaxial_two_port()` | Port families cannot be mixed in one S-parameter calculation. If you are not sure which calculator a model needs, `sim.s_matrix_lane()` tells you without running anything, and `sim.compute_s_matrix(**kwargs)` calls it for you. The one exception is a lumped/wire-only model, which `compute_s_matrix` refuses: call `run(compute_s_params=True)` for those. ## Waveforms Point sources and lumped/wire ports take a waveform object. Waveguide and TFSF sources take `f0` and `bandwidth` directly. ```python import jax.numpy as jnp from rfx import CustomWaveform, CWSource, GaussianPulse, ModulatedGaussian pulse = GaussianPulse(f0=2.4e9, bandwidth=0.5) # broadband, no carrier burst = ModulatedGaussian(f0=3e9, bandwidth=0.6) # carrier with a Gaussian envelope cw = CWSource(f0=5.8e9, ramp_steps=50) # sinusoid, tapered on over 50 cycles def chirp(t): rate = (4e9 - 1e9) / 10e-9 return jnp.sin(2 * jnp.pi * (1e9 * t + 0.5 * rate * t**2)) sweep = CustomWaveform(func=chirp) # any JAX-traceable f(t) ``` | Waveform | Spectrum | Use it for | |---|---|---| | `GaussianPulse` | differentiated Gaussian; broad, extends well below `f0` | the default; broadband S-parameters | | `ModulatedGaussian` | Gaussian centred on `f0`, width `f0·bandwidth` | a band away from DC; resonance searches | | `CWSource` | one frequency | steady-state field pictures | | `CustomWaveform` | whatever `func` gives | chirps, measured drive signals | `bandwidth` is fractional. When you omit `waveform`, rfx uses `GaussianPulse(f0=freq_max/2, bandwidth=0.8)`. A pulse that turns on abruptly leaves a small net charge behind. It usually does not matter. If you stop a run on `until_decay` and a static remnant keeps it from stopping, raise `GaussianPulse(cutoff=4.5)` or use a `ModulatedGaussian` with `bandwidth <= 0.4`. ## Point sources `add_source` is a soft source at one grid edge. It does not load the structure, so a cavity's Q is unchanged. Use it for resonances, field patterns and far fields. ```python from rfx import Box, Simulation sim = Simulation(freq_max=6e9, domain=(0.05, 0.05, 0.02), dx=2.5e-3, boundary="cpml", cpml_layers=6) sim.add_material("substrate", eps_r=3.2, sigma=0.01) sim.add(Box((0.010, 0.010, 0.005), (0.040, 0.040, 0.015)), material="substrate") sim.add_source((0.025, 0.025, 0.010), "ez", waveform=GaussianPulse(f0=3e9), amplitude_kind="current") ``` Always pass `amplitude_kind`: - `"current"`: the amplitude is a current moment in A·m. The injected power does not change when you refine the mesh. Use this by default. - `"field"`: the amplitude is added to E each step, in V/m. Omitting it or passing `None` selects `"current"` and warns once per simulation. The default is boundary- and mesh-independent on Yee paths; ADI requires explicit `"field"`. Witness: `tests/unit/sources/test_source_amplitude_kind.py`. ### Polarized sources `add_polarized_source` drives two components together: ```python sim.add_polarized_source((0.025, 0.025, 0.010), polarization="slant45", amplitude_kind="current") sim.add_polarized_source((0.025, 0.025, 0.010), polarization=(1.0, -0.5), waveform=GaussianPulse(f0=3e9), amplitude_kind="current") ``` `"ex"`, `"ey"`, `"ez"`, `"slant45"` and real `(Ex, Ey)` tuples give linear polarization. `"circular"`, `"rhcp"`, `"lhcp"` and complex tuples are accepted, but their time-domain phasing has not been verified. Do not rely on them for axial ratio or handedness. ## Plane waves (TFSF) `add_tfsf_source` launches a plane wave inside a total-field region, for scattering and RCS. It travels along x (`"+x"` or `"-x"`) with `"ez"` or `"ey"` polarization, and needs a CPML boundary. ```python scatter = Simulation(freq_max=8e9, domain=(0.06, 0.06, 0.06), dx=3e-3, boundary="cpml", cpml_layers=6) scatter.add_tfsf_source(f0=5e9, bandwidth=0.4, polarization="ez", direction="+x") ``` For oblique incidence, set `angle_deg` and choose `method`: - `method="bloch"` (default): a periodic unit cell, for infinite arrays and surfaces. Its fields are complex, and frequency monitors do not account for that. - `method="methodB"`: an open domain around a compact scatterer. It needs `polarization="ez"`, `direction="+x"` and a uniform grid. TFSF cannot share a simulation with any port. It is not a port and gives no S-parameters. For RCS, see [Far-Field and RCS](https://remilab.ai/rfx/measure/far-field-and-rcs/). ## Lumped and wire ports `add_port` is a feed with a resistive load equal to `impedance`. It is the port to use for a discrete feed, a probe feed, or a component's terminals. - Without `extent`, it is a **lumped port**: one cell. - With `extent`, it is a **wire port** that spans `extent` metres along its component axis, from `position` to `position + extent`. Use it to bridge a gap between two conductors, such as a probe feed through a substrate. - `radius=` declares the wire port's probe-pin radius, within the local thin-probe mesh restrictions. If omitted, the effective pin radius follows the mesh and its series inductance changes under refinement. ```python sim = Simulation(freq_max=6e9, domain=(0.05, 0.05, 0.02), dx=2.5e-3, boundary="cpml", cpml_layers=6) sim.add_port((0.01, 0.025, 0.010), "ez", impedance=50.0, waveform=GaussianPulse(f0=3e9)) sim.add_port((0.04, 0.025, 0.010), "ez", impedance=50.0, waveform=GaussianPulse(f0=3e9)) result = sim.run(n_steps=300, compute_s_params=True, s_param_freqs=jnp.linspace(1e9, 5e9, 21)) s11 = result.s_params[0, 0, :] # S[receiver, driven, freq], complex s21 = result.s_params[1, 0, :] ``` With several ports, rfx drives each in turn and assembles the full matrix. All ports in one calculation must be lumped, or all wire. Without `s_param_freqs`, rfx uses 50 points from `freq_max/10` to `freq_max`. The run above is far too short to settle; it only shows the calls. [Probes and S-Parameters](https://remilab.ai/rfx/measure/s-parameters/) covers plotting and Touchstone export. ### Getting accurate S11 - **Resolve the wavelength.** Use about **60 cells per wavelength** at the highest frequency you care about. At 60 cells per wavelength, one-port |S11| lands within 0.54 dB of the closed form on resistive loads, and exactly on a short or open. At 30 cells per wavelength, magnitudes are within 1.1 dB, and the line's phase-crossing frequencies sit 1.7–2.9 % from the closed form. These figures come from the lumped/wire chain battery, which measures `forward(port_s11_freqs=...)` on a one-cell parallel-plate line. - **Know the reference plane.** A one-cell port reads its voltage at the port node and its current half a cell toward the line. - Lumped and wire ports give identical S11 on the same cells. ### Completing a short record A resonant structure can take far longer to ring down than you want to simulate. For wire ports, `run(..., ringdown=RingdownSpec())` fits the decaying tail of the record and adds the unrecorded part of each spectrum in closed form: ```python import numpy as np from rfx.ringdown import RingdownSpec MM = 1e-3 box = Simulation(freq_max=20e9, domain=(12 * MM, 11 * MM, 5 * MM), dx=1 * MM, boundary="pec") box.add_material("fill", eps_r=2.2, sigma=2 * np.pi * 12.5e9 * 8.854e-12 * 2.2 / 300) box.add(Box((0, 0, 0), (12 * MM, 11 * MM, 5 * MM)), material="fill") box.add_port((2 * MM, 2 * MM, 0.0), "ez", impedance=50.0, extent=1 * MM, waveform=GaussianPulse(f0=13e9, bandwidth=0.8, cutoff=4.5)) short = box.run(n_steps=1500, compute_s_params=True, s_param_freqs=np.linspace(8e9, 18e9, 101), ringdown=RingdownSpec()) print(short.ringdown.report.ok) # did the completion pass its checks? s11_completed = short.ringdown.s_params[0, 0, :] ``` `short.s_params` is still the plain, truncated result. The completed matrix is `short.ringdown.s_params`, and `short.ringdown.report` lists the fitted poles and the checks. `report.ok` is decided by comparing completions from two different windows of the record. These checks catch a bad fit. They are not an error bound. `ringdown=` works with wire ports on a fixed-length record: one port on a uniform mesh, or several on a graded mesh. The source must be off in the second half of the record. Lumped, microstrip, waveguide and coaxial ports, TFSF, `until_decay` and `devices=` are refused. `forward(..., ringdown=RingdownSpec())` puts the same completion inside the differentiable program, so `jax.grad` sees the ringing too; it is also refused with `distributed=True`. ### Ending a line at a port When a trace runs to the edge of the domain, rfx extends it into the absorber, as if the line continued forever. If the line is meant to end at your port, name its conductor with `terminates=`: ```python line = Simulation(freq_max=6e9, domain=(0.04, 0.02, 0.01), dx=1e-3, boundary="cpml", cpml_layers=6) line.add_thin_conductor(Box((0.0, 0.0, 0.001), (0.04, 0.02, 0.001))) # ground strip = Box((0.0, 0.009, 0.004), (0.04, 0.011, 0.004)) line.add_thin_conductor(strip) line.add_port((0.035, 0.010, 0.001), "ez", extent=3e-3, waveform=GaussianPulse(f0=3e9), terminates=strip) ``` Preflight warns (`port_conductor_continues`) when a port's conductor runs into the absorber and no port terminates it. A ground plane should continue; name only the line the port ends. Microstrip ports do this automatically for the trace under them. ### Lumped circuit elements `add_lumped_rlc` puts a resistor, inductor, capacitor or RLC combination on one grid edge. It is **not a port**: it has no reference impedance. To measure the reflection it presents, drive the line with `add_port(...)` and use the RLC element as the load. ```python sim.add_lumped_rlc((0.03, 0.02, 0.01), "ez", R=50.0, C=0.2e-12) ``` `topology` is `"series"` (default) or `"parallel"`. On the uniform, single-device `forward()` path, `rlc_values_override={0: {"R": R}}` makes a component value differentiable. The key is the element's registration order. ## Microstrip ports `add_msl_port` spans the full width under the trace and extracts the line's wave amplitudes from a row of probe planes downstream of the feed. Use `compute_msl_s_matrix()`, not `run(compute_s_params=True)`: ```python # Run from the repository root to use the tutorial's edge-aware board. from examples.tutorials.ports_and_sparams_101 import build_microstrip_ports board = build_microstrip_ports() msl = board.compute_msl_s_matrix(freqs=np.linspace(2e9, 6e9, 13), num_periods=20) print(np.abs(msl.S[1, 0])) # |S21| across the band ``` `position` is `(feed plane, trace centre, substrate bottom)`. `direction` may be `"+x"`, `"-x"`, `"+y"` or `"-y"`; the substrate normal is always z. `S` is returned as extracted. A warning names any bins where it is not passive; `enforce_passivity=True` projects it instead. `msl.reliable` flags bins with too little signal to trust. The builder in `examples/tutorials/ports_and_sparams_101.py` uses a 44 × 34 mm domain, edge-aware x/y profiles, and a uniform z profile of 16 cells of 0.25 mm. It builds and preflights this board without solving it in the tutorial; the calculation above runs it. The tutorial's live generic-port S11 examples use `S11_STEPS = 600` each. This board is coarse for speed, so check the fitted Z0 against the closed form. S11 and S21 use the measured V and I with the analytic Z0, not the fitted Z0. Refine before you quote Z0. Getting an accurate microstrip result: - **The substrate sets the cell size.** On a thin substrate the substrate thickness binds long before the wavelength does. Preflight warns below 4 cells through the substrate. Start there, then refine and check that your answer stops moving. - **End the line at its ports, not in the absorber.** CPML is a much poorer termination for a two-conductor line than for a plane wave. Put a port at each end of the line. - **Give the probes room.** The automatic probe offset and spacing are chosen in cells from the mesh under the port, and recounted at run time on graded or automatic meshes. On a short feed, rfx warns if there is no clean place for them. Check `msl.probe_clearance`, and lengthen the feed if it is insufficient. ## Waveguide ports `add_waveguide_port` launches a TE or TM mode of a rectangular aperture and decomposes the fields into that mode. Its aperture defines the guide. A port that spans the whole cross-section uses the transverse domain faces as the guide walls, so declare them PEC and put the absorber on the port axis only; on a uniform mesh `boundary="cpml"` on every face is refused. ```python guide = Simulation(freq_max=11e9, domain=(0.10, 0.023, 0.010), dx=1e-3, boundary={"x": "cpml", "y": "pec", "z": "pec"}, cpml_layers=8) guide.add_waveguide_port(0.01, mode=(1, 0), mode_type="TE", direction="+x", f0=10e9, bandwidth=0.5, name="port1") guide.add_waveguide_port(0.09, mode=(1, 0), mode_type="TE", direction="-x", f0=10e9, bandwidth=0.5, name="port2") print(guide.s_matrix_lane()) ``` Compute the matrix with `guide.compute_waveguide_s_matrix(normalize=...)`. [Waveguide Ports](https://remilab.ai/rfx/guide/waveguide-ports/) covers normalization, calibration planes and junctions. ## Coaxial ports The coaxial calculators build their own line: a coax along z with a matched feed, a TEM source, and a row of probe planes. You register one `add_coaxial_port` for the cross-section (its x/y centre, radii and waveform) and call a calculator: - `compute_coaxial_line_reflection(termination=...)`: one port. The line ends in `"short"` or a `"matched"` load; `dut_impedance=R` makes the load a resistor. - `compute_coaxial_two_port()`: a through line with a port at each end. ```python coax = Simulation(freq_max=40e9, domain=(0.008, 0.008, 0.040), boundary="cpml", cpml_layers=8) coax.add_coaxial_port((0.004, 0.004, 0.020), face="top", pin_radius=0.635e-3, outer_radius=2.055e-3, waveform=GaussianPulse(f0=8e9, bandwidth=1.2)) refl = coax.compute_coaxial_line_reflection( termination="short", n_steps=3000, freqs=np.linspace(4e9, 12e9, 5)) print(refl.status, f"{refl.annulus_cells:.1f} annulus cells") print(np.abs(refl.s11)) ``` The line's conductors are realized PEC. Their wall thickness is 1 mm, or the room to the nearest absorber less one cell if that is smaller; a line with less than a cell of room raises. What the calculators need: - A uniform, float32, 3-D Yee grid with CPML on all six faces. Exactly one coaxial port with `face="top"`, and nothing else in the model: no geometry, sheets, probes, monitors or other ports. The calculator refuses extras rather than ignoring them. - The port's z position, `pin_length` and `impedance` do not place the line or set its loads. Use `feed_impedance=` and `dut_impedance=`. Getting an accurate result: - **Use at least 4 cells across the annulus** (between pin and outer conductor). Raise `freq_max` to shrink the cell. `status` reports `"under_resolved"` below about 3.5. The example above uses 3.8 for speed. - **Keep the probe span physical.** Probe planes are placed in cells (`probe_start_cells`, `probe_spacing_cells`), so refining the mesh shortens the length the phase fit sees. Scale the spacing up when you refine. - **Leave room around the line.** Both coax calculators absorb on all three axes, so an open end radiates instead of ringing in a closed can. A line whose outer wall comes within one cell of a lateral absorber is refused. ## Limits - **Sources give no S-parameters.** `add_source`, polarized sources and TFSF have no impedance. Use a port and its calculator. - **One port family per calculation.** Mixed lumped + microstrip and coax-to-microstrip calculators exist but are experimental; measure each line with its own calculator instead. - **Circular polarization** from `add_polarized_source` is unverified. - **Lumped/wire ports** need about 60 cells per wavelength for S11 within about 0.5 dB. `forward(port_s11_freqs=...)` is uniform and single-device only. Wire ports on a graded mesh are experimental. - **Microstrip** `mode="eigenmode"` is unsupported. Graded-mesh microstrip has no external comparison yet. ADI, subgridding and TFSF are refused. - **Coax** lanes cover one geometry family, and only azimuthally symmetric discontinuities. - **TFSF on a graded axis:** preflight warns, because the scattered-field region picks up a spurious field (about 0.7 of the total-field peak with no scatterer). Normal incidence along ±x needs a uniform x axis. - Detailed scope for every port family is in the [S-parameter support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.md). --- # Sources Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/model/sources/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/model/sources.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Changelog Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/project/changelog/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/project/changelog.mdx This page summarizes user-visible changes: what moved, and what to change in your scripts. The repository's `CHANGELOG.md` has the full detail, including the measurements behind each entry. ## Unreleased — 2.0.0 2.0 changes how conductors are built, how several ports read their S-parameters, and how the absorber reflects. Expect results with conductors, lumped or coaxial ports, or open boundaries to move. Re-run anything you compare against a 1.x number. ### Breaking - **The coaxial calculators absorb on all three axes** (#1218). `compute_coaxial_line_reflection` and `compute_coaxial_two_port` now use `cpml_axes="xyz"`, the only accepted value; `cpml_axes="z"` raises. With absorbers on z only the line sat in a closed metal can, so an open end read |S11| above 1. Every coaxial S moves slightly, and a line whose outer wall comes within one cell of a side absorber is refused. - **A conductor is a volume, a sheet or a wire, and your declaration says which.** `sim.add(shape, material="pec")` is a volume, with walls at both drawn faces, so its realized thickness equals its drawn thickness. `sim.add_thin_conductor(...)`, or a zero-thickness `Box`, is a sheet on one grid plane that owns no cell. A `PolylineWire` is a wire. See [How conductors land on the lattice](https://remilab.ai/rfx/model/materials/#how-conductors-land-on-the-lattice). Recipes that drew a conductor one cell longer to make up for a missing face are no longer needed. Models without conductors give the same results as 1.x. - **The `two_plane=True` keyword on `sim.add()` is removed**, with no deprecation period. Passing it raises `TypeError`; the volume rule now gives every PEC body both faces. Design documents move to IR v2, and a v1 document carrying `two_plane` is refused with a message. - **Thin PEC shapes are checked against the grid.** When the grid is built, a PEC shape thinner than one cell (but not zero), a shape that rasterizes to no cell, a `Box` with two or three zero-length sides, and an `add_thin_conductor` shape thicker than one cell all raise. Declare a sheet or resolve the thickness. - **`run()` refuses arguments its path does not implement** (#1297). The graded-mesh, multi-device, subgridded and ADI paths used to warn and run without `until_decay`, snapshots, conformal walls, subpixel smoothing or checkpointing. They now raise `NotImplementedError` before the first step. `Boundary(conformal=True)` raises in `forward()` and `optimize()`. - **A driven lumped port reads `S11` from its terminal voltage and current** (#1162). The old reading returned the reciprocal of the physical reflection. Every lumped-port `|S11|`, and every lumped multi-port off-diagonal, changes. A one-cell lumped port and a one-cell wire port now return the same S-matrix. - **`compute_msl_s_matrix` returns the raw extracted `S`** (#1154). The passivity projection is opt-in with `enforce_passivity=True`. The new `sigma_max_excess` field reports any passivity breach, and a warning names it. - **The coaxial lanes build the pin and outer conductor as PEC**, with the outer wall at the declared outer radius (#1169). Every coaxial S-parameter moves. The wall is `min(1 mm, clearance to the nearest absorber − 1 cell)` thick, and a line with less than one cell of room raises. - **`compute_coaxial_s_matrix` is removed**, with `add_coaxial_matched_load`, `add_coaxial_open_termination` and `add_coaxial_pec_end_cap` (#1212). Use `compute_coaxial_line_reflection` for one port, `compute_coaxial_two_port` for a through line, or `compute_s_matrix(lane=...)`. - **`compute_coax_msl_transition` refuses a non-passive `S` by default** (#838). Pass `strict_passivity=False` to get the diagnostic matrix and a warning, as before. - **A far-field (NTFF) box must sit at least one cell inside the grid** on every axis, and a far-field box in a 2-D run raises (#1159). With an absorber, nothing changes. - **rfx Studio, the experiment service, the agent tools and their CLI commands leave the package** (#1150). `rfx run`, `rfx plot` and `rfx export-geometry` stay; the `studio` and `agent` extras are gone. ### Removed - **The `jax.pmap` multi-device runner** (#1296). Calling `rfx.runners.distributed.run_distributed` raises `RuntimeError`. Use `sim.run(devices=[...])`, which never used it. - **The scripts in `validation/crossval/` and its manifest** (#1156, #1157, #1160, #1165, #1168, #1170, #1175, #1315); the RT/Duroid 5880 patch case became `tests/crossval/rt5880_patch`. Structures with a closed form are now compared with it by the analytic tests in `tests/oracle/`, and comparisons with other solvers run against frozen reference records in `tests/crossval/`. See [Benchmarks](https://remilab.ai/rfx/guide/benchmarks/). ### Changed - **Multi-device runs** accept only `exchange_interval=1` (#1185). They now send only the four field faces the update reads, which makes the time loop several times faster across nodes (#1222, #1252), and they pass material arrays to the time loop as arguments, so no device keeps whole-domain copies (#1211, #1217). - **`run(ringdown=...)` judges its completion against a window that starts twice as early** (#1254). Some short records that read ok before now read not ok; the completed S-parameters themselves are unchanged. - **The ring-down W0 check reads the summed port accumulators at 1e-4 of each array's peak** instead of a count of float rounding steps, so a jitted `forward()` is no longer reported as not completed (#1318). `run(ringdown=...)`'s completed S can change in its last bits. ### Added - **Phase-referenced complex scattering**: `compute_rcs(..., phase_reference=(x, y, z), subtract_incident_reference=True)` returns a `ScatteringResponse` with complex `F_theta` / `F_phi` in metres (#1327). Normal +x incidence on a uniform 3-D grid with symmetric CPML for now; existing calls still return `RCSResult`. - **A design box can contain its own feed port** with `forward(design_box_holds_ports=True)` (#1319). The port's edges keep the drawn materials and the 50 Ω load, their gradient is zero, and `ForwardResult.design_box_held_edges` lists them. Without the option, a box holding a lumped or wire port still raises. - **`run(..., ringdown=RingdownSpec())`** completes a wire port's S-parameters when the record ends while the structure still rings (#1254). `result.ringdown.report` holds the fitted poles and the witnesses; none of them is an error bound. - **`forward(..., ringdown=RingdownSpec())`** puts the same completion inside the differentiable program, so `jax.grad` differentiates the ringing a short record has not reached (#1318). `rfx.ringdown.gradient_witness` compares the gradients from two completion windows; a failure makes the value and the gradient NaN. Refused with `distributed=True` and `stencil_order=4`, as well as wherever `run(ringdown=...)` is refused. - **Mesh lines that follow sheet edges**: `rfx.mesh_edges.edge_aware_profile` and `edge_aware_profiles` (#1138). A new preflight advisory, `sheet_effective_size`, reports a sheet solved more than 1 % off its drawn size. - **`rfx.gradient_record_length_witness`** checks that a gradient does not change when the record is doubled, the gradient counterpart of the −40 dB settling check (#1186). - **Design-box gradients.** `forward(design_box=..., design_eps_override=...)` differentiates the permittivity in one box without putting the whole grid on the AD tape (#1179). It also runs on graded meshes and takes a conductor occupancy (#1183), and `design_sigma_override` takes one conductivity per E component, so a design sheet is a sheet (#1216). - **`Simulation.add_pinned_sheet`** declares a sheet by grid node lines, so the mesh itself can be a design variable (#1199). A 50 Ω wire port runs on a traced mesh, and `Simulation(dt=..., dt_min_cell=...)` pins one time step across a family of deformed meshes (#1207). - **`forward(distributed=True)` runs across several processes** and takes its design variables as sharded arrays (#1247). - **`Simulation.boundary_model()`** reports each face's declared kind and plane (#1223). - **`result.snapshot_axes`** gives the position and time of every snapshot sample (#1258). ### Fixed - **A current source in material supplied by `eps_override` / `sigma_override` on the distributed graded `forward()`** was driven through the permittivity as drawn, and its permittivity gradient missed the drive's own derivative (#1317). It now uses the materials the field is stepped with, as the single-device lane does. - **The CPML reflects far less** (#1012). Its magnetic half now reads the loss profile at the magnetic field's own position. Every result that depends on an absorber moves. A thin absorber (4–6 layers) close to a source now holds low-frequency field longer; use 8 or more layers there. - **A conductor that reaches an absorbing boundary continues through the absorber**, as dielectrics already did (#801). A port ends the conductor you name with `terminates=`, and preflight names any conductor that runs into the absorber unterminated. - **Each E component averages the permittivity and conductivity of its four neighbouring cells** (#1210). A conductivity-defined volume's faces now sit where they were drawn, and dielectric interfaces converge at second order. - **Lumped elements** load only their own edge, not all three E components at their node (#1236). A series R+C, R+L or R+L+C now realizes its declared resistance and no longer drives low-resistance runs to NaN (#1163). A lumped inductor no longer carries a spurious series resistance (#1245). - **Far fields.** The near-to-far-field transform is second order in the cell size and stamps E at the right time, so directivities and RCS values move slightly (#1159). Normal-incidence `compute_rcs` read every σ about 1.1 dB low (#820). A far-field box that does not enclose the plane-wave injection region is refused instead of returning a wrong cross section (#1176). - **Snapshots.** `SnapshotSpec(interval=m)` records every m-th step, as documented, instead of every step, and `run(until_decay=...)` records snapshots (#1258). With `stencil_order=4`, `result.dt` is the step the solver actually took. - **Closed boxes and magnetic walls.** `forward()` on a closed PEC box applies the walls (#1193). A magnetic-wall face is no longer shorted by a PEC wall on the uniform and graded-mesh paths (#1164). The Kottke PEC occupancy lane places the conductor where the occupancy is (#1197). - **Graded meshes.** A port's drive includes its own load (#1256) and sees `eps_override` / `sigma_override` (#1267). With a Debye or Lorentz material present, ports terminate instead of acting as open circuits (#1283). A waveguide port inside a refined band injects at its own cell (#810), and a microstrip port's automatic probe ladder has the right length (#1278). Preflight checks read the cells at their own site (#1209). A 2-D `mode` on a graded mesh is refused instead of returning zero fields (#1148). - **`add_refinement`** is refused where it used to be ignored: on a graded mesh, and by `forward()`, `optimize()` and the S-parameter calculators (#1282). - **Tracing.** A graded-mesh run can be traced as a whole (#1188), and `jax.jit(jax.value_and_grad(...))` over `forward()` traces for wire-port boards, so `rfx.optimize(jit=True)` compiles once per call (#1225). - **Multi-device runs** refuse features they do not implement instead of dropping them: lumped RLC elements, MSL ports, refinements, Kerr materials, periodic axes, passive ports, flux monitors and far-field boxes (#1039, #1214, #1241). They also use far less memory on the first device (#1208, #1214, #1227) and return full results across processes (#1202). - **Smaller fixes.** The microstrip `Z0` guard and preflight agree on probe clearance (#726), the microstrip `beta` estimate no longer wobbles with its scan grid (#830), a boundary-touching dielectric continues into the waveguide lane's absorber (#1066), a rounding excess in `domain / dx` no longer adds an empty cell at an absorber face (#1070), and `eval_lorentz` evaluates a Drude pole correctly (#1158). ## v1.8.0 — September 6, 2026 - The rectangular-waveguide S-matrix closed its chain of checks for uniform single-mode guides on the `normalize=False` and `normalize="flux"` lanes. `normalize="flux"` now follows `JAX_ENABLE_X64`, and `normalize=True` rejects a traced `eps_override` up front. - The wire-port current carries the Yee half-step phase, so wire-port S-parameters rotate slightly in phase. - Finite-region `add_flux_monitor(size=...)` runs on graded meshes. - A face-touching dispersive material no longer gets an absorber surround that carries its static permittivity without its poles (#808). - Realized geometry no longer depends on `JAX_ENABLE_X64` or the solver path (#802, #807). - A `dz_profile`-only simulation reaches the graded-mesh waveguide S-matrix path instead of solving on a uniform grid (#811). - `add_port(reference_plane_cells=N)` rejects reference planes outside the uniform line or inside a microstrip port's probe ladder. - At this release, the `amplitude_kind` plan deferred the required argument to 1.9. The shipped 2.0 behavior resolves omission and `None` to `'current'` (#1442; `tests/unit/sources/test_source_amplitude_kind.py`). ## v1.7.0 — September 1, 2026 - Long solves can report step count, elapsed time, rate, and ETA with `report_every=N` on supported uniform and single-device non-uniform runs. Reporting is off by default and the reported and unreported results are byte-identity locked. - `Simulation.fidelity_report()` audits declared geometry against the cells and material realization the solver will use, before a run. It reports rasterization and realization only; it does not predict RF error. - `add_msl_port(direction=...)` supports `"+x"`, `"-x"`, `"+y"`, and `"-y"` for the Laplace/quasi-TEM path. The eigenmode path remains x-only and outside the public support envelope. - `add_thin_conductor(..., surface_impedance_f0=...)` accepts patterned shapes that rasterize to exactly one cell layer and at least one cell, including CAD-backed `MeshShape` objects. (Superseded at 2.0: a sheet is one node plane, not one cell layer.) Unsupported sheet realizations and execution lanes fail explicitly. - The uniform single-device solver offers `precision="float64"` when JAX x64 is enabled. `precision="mixed"` works with CPML, but its float16 field storage raises the absorber residual floor and is not a substitute for float32 in low-reflection or S-parameter work. - `compute_far_field_jax(..., max_phase_bytes=...)` bounds the NTFF phase-array allocation by splitting the theta grid. The split is exact and avoids materializing the full direction-by-surface array at once. - S-parameter settling warnings use the same -40 dB rule in `compute_msl_s_matrix`, `compute_waveguide_s_matrix`, `compute_mixed_s_matrix`, `compute_coaxial_two_port`, and `compute_coax_msl_transition`. - Multi-band graded-z meshes now preserve declared blocks through smoothing, with a measured z-axis adjacent-cell ratio cap of 1.4 and explicit preflight advisories outside the validated envelope. - New rasterized-slice and stack-profile views show the cells and material layers the solver actually builds. - Wire-port decomposition, MSL probe extraction, CPML material padding, graded-mesh NTFF integration, mesh import containment, and preflight diagnostics received correctness fixes without widening their documented validation envelopes. - MSL internal DFT planes now store only the extractor's required crop, reducing accumulator cells without changing the extracted matrix. - Differentiable `forward(...)` objectives are now safe to wrap in an outer `jax.jit`: both `jax.jit(loss)` and `jax.jit(jax.grad(loss))` over `forward(eps_override=...)` are regression-locked against the eager path. This fixes a tracer crash in material assembly without changing eager results. - `optimize(...)` gained default-off robustness knobs for difficult proxy losses: `n_starts`, `best_iterate`, `step_clamp`, and `seed`. The default single-start Adam path remains byte-identical. - `add_lumped_rlc(...)` elements now participate in the differentiable uniform `forward(...)` lane. Use `forward(rlc_values_override={...})` when R/L/C values themselves are scalar design variables; this is still a lumped circuit element, not a `Z0`-referenced port. - Waveguide S-matrix extraction now emits a soft advisory for passive `normalize=False` results whose column power is above the documented single-run over-unity envelope but below the hard unreliability limit. The physics and hard pass/fail thresholds are unchanged. - RCS and inverse-design docs now pin recent footguns: RCS validation is monostatic-only unless additional evidence is supplied, and a finite-difference gradient check does not prove an empty observation window is physically meaningful. - A standalone real multi-device witness (`scripts/diagnostics/distributed_witness/multigpu_witness.py`) now compares single-device and sharded PEC/CPML/lumped-port runs on machines with two or more JAX devices; it is a distributed-runner identity check, not a new physics-validation envelope. - [AD memory-planning docs](https://remilab.ai/rfx/run/memory-and-speed/) now tag each artifact with an evidence class — static estimate, conservative budget-fit plan, or explainability breakdown — so a planning estimate is never read as a runtime profiler measurement, a peak-memory guarantee, or RF validation evidence. The compiled-executable check reports the JAX compiler's memory *estimate*, not a runtime peak. - [Differentiable-design guidance](https://remilab.ai/rfx/design/gradients/) was tightened: it separates JAX-traced objectives and finite-difference gradient checks from the final port/resonance/far-field physics validation. - [Non-uniform mesh guidance](https://remilab.ai/rfx/model/domain-mesh/) now frames graded-z meshes as a bounded workflow for thin layered structures, stating the unsupported combinations and the caller's validation responsibilities up front. ## v1.6.7 — July 2026 - Open-domain oblique RCS is available through `compute_rcs(theta_inc != 0)` for its documented uniform-grid, `ez`-polarized Method-B TFSF envelope; unsupported combinations fail explicitly. - The opt-in plane-wave design lane adds complex-Bloch TFSF in `run()`, uniform `forward()` support, differentiable `mu_r_override`, and `compute_rcs_jax`. - `rfx.MeshShape` imports voxelized STL, OBJ, and PLY geometry; STEP support is available with the `cad` extra. Mesh import is not differentiable. - `rfx.interop` can emit a validated design document and a runnable openEMS script for representable simulations. - `run(until_decay=..., radiated_flux_box=...)` can stop on radiated-energy flux, and MSL extraction now resolves automatic probe offsets against both documented clearances. Returned MSL S-matrices enforce passivity by default while retaining raw results in `S_raw`. ## v1.6.6 — June 2026 - Maintenance-only release: a behavior-preserving rectangular-waveguide mode-solver refactor and shipped-documentation hygiene. There was no public API or user-visible behavior change. ## v1.6.5 — June 2026 - [Validation reporting](https://remilab.ai/rfx/guide/validation/) is stricter: a promoted port-family result now requires magnitude evidence, a phase-consistency witness, an AD-vs-finite-difference check where gradients are part of the claim, and committed (reproducible) artifacts. - Port-family support wording is clearer: users should match each source or port family to its documented calculator and evidence envelope instead of treating any S-parameter helper as universal. - `compute_msl_s_matrix` now reports a positive characteristic impedance on both ports. This fixes reported metadata while leaving the S-parameter calculation invariant. - Non-uniform mesh accuracy coverage now includes an analytic gated cavity case whose resonance frequency depends on the graded axis. - Public guides and support pages were tightened around maintained workflows, validation boundaries, and device-count-adaptive test behavior. ## v1.6.4 — June 2026 - Rectangular waveguide-port evidence now covers WR-band empty-guide, PEC-short, and dielectric-slab cases with analytic and external-reference comparisons. - `preflight()` and `preflight_sparameters()` now expose coded report objects for automation while preserving older list/string behavior. - Run/forward results, S-matrix calculators, sweeps, and optimizers now surface finite-data, passivity, setup, and NaN-gradient issues earlier. - `compute_coaxial_line_reflection(...)` is the current coaxial line-reflection path inside its documented transmission-line envelope. - `compute_waveguide_s_matrix(checkpoint_segments=...)` adds segmented checkpointing for uniform waveguide AD-heavy runs. - Finite-size flux monitors are regression-locked against the matching full-plane integrand window; the old finite-size instability caveat is superseded. - The directivity objective gained a log-ratio option for sign-correct gradients when design variables change total radiated power. ## v1.6 line - Advanced the differentiable design loops and validation-boundary reporting. - Added feature-boundary wording so repository code outside the documented support scope is not presented as the default public path. - Improved port and observable APIs for RF workflows. ## v1.5 line - Expanded the public guide structure for examples, API surfaces, and validation/support boundaries. - Added RF examples and clearer routing for patch, waveguide, and microstrip workflows. ## Earlier releases Earlier releases introduced the core JAX FDTD solver, material/geometry helpers, sources, probes, ports, far-field utilities, and inverse-design workflows. See the Git history and package releases for full implementation detail. --- # Citing rfx Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/project/citation/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/project/citation.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Contributing to rfx Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/project/contributing/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/project/contributing.md This page is for people who want to change rfx itself: fix a bug, add a feature or improve the docs. If you want to *use* rfx, start with the [Quick Start](https://remilab.ai/rfx/start/first-simulation/). ## Set up ```bash git clone https://github.com/bk-squared/rfx.git cd rfx pip install -e '.[dev]' ``` rfx needs Python 3.11 or newer and JAX and jaxlib 0.10.2 or newer. The `dev` extra installs `pytest`, `pytest-xdist`, `ruff` and the other development tools. Everything below runs on a CPU. To test on an NVIDIA GPU, install the JAX build that matches your CUDA stack first, then check that JAX sees the device: ```bash python -c "import jax; print(jax.devices())" ``` ## Run the tests The default test selection skips tests marked `slow`, `gpu`, `slow_physics` and `docs_consistency`, so a plain `pytest` runs the fast suite: ```bash # The tests for the area you changed (fastest feedback) python -m pytest tests/unit/ports -q # The whole fast suite, in parallel python -m pytest tests -q -n auto # One marked group on purpose, for example the slow tests of one file python -m pytest -o addopts="" -m slow tests/oracle/test_skin_depth_oracle.py -q ``` The whole fast suite takes a while on a laptop. Iterate on the narrowest set that covers your change, then run the wider suite before you open the pull request. Tests are grouped by kind: | Directory | What it holds | |---|---| | `tests/unit/` | behaviour of one module, grouped by area (`ports/`, `sparams/`, `boundaries/`, ...) | | `tests/oracle/` | comparisons with closed-form answers | | `tests/crossval/` | comparisons with frozen results from other solvers (mostly `gpu`/`slow`) | | `tests/locks/` | regression locks that pin existing behaviour | | `tests/contracts/` | repository rules: docs, manifests, CI configuration | ## Run the required checks `scripts/ci/local.sh` runs, in order, the short checks every pull request must pass: ruff, docs hygiene, the changelog fragment, the data budget, the PR body, workflow YAML and `tests/contracts`. It stops at the first failure. ```bash bash scripts/ci/local.sh # all checks except the PR body bash scripts/ci/local.sh /tmp/body.md # also check a PR body you wrote ``` The lint step on its own is `bash scripts/ci/lint.sh`, which runs ruff with the project's rule selection. The fast test suite is not part of `local.sh`; CI runs it when a pull request touches code. ## Add a changelog fragment Do not edit `CHANGELOG.md`. Add one file per pull request under `changelog.d/`: ```text changelog.d/..md ``` `` is your pull request number (or the issue number), and `` is one of `added`, `breaking`, `changed`, `deprecated`, `fixed` or `removed`. A change under `rfx/` must add a fragment; docs-only and test-only changes may. The file holds a heading and a few bullets, at most 12 lines: ```markdown ### Fixed — a lumped inductor no longer carries a spurious series resistance (#1245) - What changed for the user, the number they now get, and what to change in their script, if anything. ``` The heading uses an em dash and must include `#`. Check your fragment with: ```bash python scripts/changelog/assemble.py --check ``` `changelog.d/README.md` has the full format. ## Code conventions - Type hints and a docstring on every public function and class. - `dataclass(frozen=True)` for value objects such as shapes and configs. - JAX-friendly hot paths: `jnp` instead of `np`, no in-place mutation, and `jax.lax.scan` for loops that must stay JIT-compatible. - `snake_case` for functions and variables, `PascalCase` for classes, `UPPER_CASE` for module constants. - A change that affects users updates the matching page under `docs/public/`. A code block in a public page must run; check it with `python scripts/check_public_docs_blocks.py --only guide/`. ## Add a feature 1. **Write a failing test first**, in `tests/unit//test_.py`. 2. **Implement it** in the right module under `rfx/`. Export new public API from `rfx/__init__.py`. 3. **Prefer an error to a silent fallback.** If a combination is not supported, raise with a message that says what to do instead. 4. **Show it is right, not only that it runs.** For a physics change, compare with a closed form (`tests/oracle/`) or a trusted reference, and say how far the result moved. 5. **Update the docs** and add the changelog fragment. A new geometry primitive lives in `rfx/geometry/`. It implements the `Shape` protocol in `rfx/geometry/csg.py`: `mask(grid)` and `mask_on_coords(x, y, z)` return a boolean occupancy, and `bounding_box()` returns its extent. Test it with `Simulation.add()`, including zero-thickness and single-cell cases. ## Open a pull request 1. Fork the repository and create a branch, for example `git checkout -b fix/lumped-inductor-loss`. 2. Commit with a descriptive message. Prefixes such as `feat:`, `fix:`, `docs:`, `refactor:` and `test:` help. 3. Run `bash scripts/ci/local.sh` and the tests for your area. 4. Open the pull request against `main`. The template asks for two lines: `Lane:` names the area that owns the change (one of the repository's `lane:*` labels), and `Review:` records who reviewed it. Describe what changed, why, and the evidence: the test you added and any number that moved. 5. If a check fails, fix it and push again. ## Where things live ```text rfx/ __init__.py public API re-exports api/ Simulation, Result and the run / S-parameter entry points simulation.py compiled uniform-grid time loop runners/ uniform, non-uniform and multi-device runners core/ Yee update equations boundaries/ CPML, UPML, PEC and PMC handling sources/ waveforms, plane waves, waveguide and coaxial ports sparams/ S-parameter extraction for each port family geometry/ shapes, CAD mesh import, rasterization materials/ dispersive and thin-conductor models preflight/ the checks behind sim.preflight() farfield.py near-to-far-field transform rcs.py radar cross section optimize.py gradient-based inverse design tests/ the test suite (see above) examples/ the learning-path scripts validation/ cross-solver comparison scripts docs/public/ the public documentation source ``` ## Questions Open an issue on [GitHub](https://github.com/bk-squared/rfx/issues). --- # Reduce Memory Use Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/run/memory-and-speed/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/run/memory-and-speed.mdx A forward FDTD run needs memory for the fields and materials on the grid. A gradient needs much more: reverse-mode AD keeps field state from every time step for the backward pass, so its memory grows with `n_steps`. This page shows how to estimate that memory before you launch, how to cut it, and what each memory report can and cannot tell you. ## Ways to reduce AD memory | Option | What it does | Where | |---|---|---| | `forward(design_box=..., design_eps_override=..., checkpoint=False)` | keeps only the design box on the tape instead of whole-grid material arrays; same gradient | [Inverse Design](https://remilab.ai/rfx/design/optimization/) | | `checkpoint_segments=K` | uniform mesh: stores state at segment boundaries and replays one segment at a time, about `sqrt(n_steps)` memory for about 2x compute. `K` must divide `n_steps`. | `forward()`, `optimize()` | | `checkpoint_every=K` | the same on a non-uniform mesh; `K` is a chunk length in steps | `forward()`, `optimize()` | | a graded mesh | fewer cells when one thin feature needs fine cells along one axis | [Non-Uniform Mesh](https://remilab.ai/rfx/model/domain-mesh/) | | a shorter record | fewer steps on the tape; check the gradient still converges | [Gradient Behavior](https://remilab.ai/rfx/guide/gradient-behavior/) | | `n_warmup=K` | non-uniform mesh only: no tape for the first `K` steps. This **approximates** the gradient | `forward()` docstring | Don't pick a segment count by hand. Boundary storage shrinks as segments get longer while the live segment's replay tape grows, so memory is lowest near `sqrt(2 x n_steps)` steps per segment and rises toward either extreme. Let `plan_ad_memory(...)` choose. `n_warmup` cuts the gradient paths through the first `K` steps. That is nearly exact only while the wave has not yet reached the design region; beyond that the error grows. On a uniform mesh `forward(n_warmup=...)` raises `NotImplementedError`. Changing the mesh or checkpointing does not extend what a port, S-parameter or far-field calculator supports. Check the [support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/support_matrix.md) for your combination. ## Plan, then run `plan_ad_memory(n_steps, available_memory_gb)` estimates the memory of full reverse-mode AD and, if that does not fit, picks a checkpointing option that does. It does not run FDTD. The model below is tiny; the budget is set to 0.1 GB so the planner has to choose checkpointing: ```python import jax import jax.numpy as jnp from rfx import Box, GaussianPulse, Simulation sim = Simulation(freq_max=5e9, domain=(0.02, 0.02, 0.01), dx=2e-3, boundary="cpml", cpml_layers=4) sim.add_material("slab", eps_r=4.0, sigma=0.01) sim.add(Box((0.006, 0.006, 0.003), (0.014, 0.014, 0.007)), material="slab") sim.add_source((0.005, 0.010, 0.005), "ez", waveform=GaussianPulse(f0=3e9, bandwidth=0.8), amplitude_kind="current") sim.add_probe((0.015, 0.010, 0.005), "ez") N = 2_000 BUDGET_GB = 0.1 plan = sim.plan_ad_memory(n_steps=N, available_memory_gb=BUDGET_GB) print(plan.recommendation) if plan.full_ad_fits: checkpoint_kwargs = {} elif plan.segmented_fits and plan.checkpoint_mode == "checkpoint_segments": checkpoint_kwargs = {"checkpoint_segments": plan.checkpoint_segments} elif plan.segmented_fits and plan.checkpoint_mode == "checkpoint_every": checkpoint_kwargs = {"checkpoint_every": plan.checkpoint_every} else: raise RuntimeError(plan.recommendation) print(checkpoint_kwargs) eps = jnp.full(sim.run(n_steps=1).grid.shape, 4.0, dtype=jnp.float32) def loss(eps_r): out = sim.forward(eps_override=eps_r, n_steps=N, **checkpoint_kwargs) return jnp.sum(out.time_series ** 2) grad = jax.grad(loss)(eps) print(grad.shape) ``` How to read the plan: - `full_ad_fits=True`: ordinary AD fits. Both checkpoint values are `None` and `segmented_fits` is `False`, because no segmented option is needed. - Otherwise the plan proposes `checkpoint_segments` (uniform mesh) or `checkpoint_every` (non-uniform mesh) in `checkpoint_mode`. Use it only when `segmented_fits` is `True`. - If both flags are `False`, the returned candidate is the least-memory option and still does not fit. Follow `plan.recommendation` (a smaller mesh, fewer steps, a design box) instead of launching it. The estimate is multiplied by a safety factor (`plan.fit_safety_factor`, default 1.3) before the fit flags are set, so a plan near the limit stays conservative. ## The one-call preflight `ad_memory_preflight(...)` runs the planner together with `explain_ad_memory(...)`, `mesh_intelligence_report(...)` and, if you pass a function, the saved-residual diagnostic. It returns one report with a status, the selected checkpoint option and action hints: ```python preflight = sim.ad_memory_preflight(n_steps=N, available_memory_gb=BUDGET_GB) if preflight.full_ad_fits: checkpoint_kwargs = {} elif (preflight.checkpointing_fits and preflight.supported_checkpoint_mode == "checkpoint_every"): checkpoint_kwargs = {"checkpoint_every": preflight.checkpoint_every} elif (preflight.checkpointing_fits and preflight.supported_checkpoint_mode == "checkpoint_segments"): checkpoint_kwargs = {"checkpoint_segments": preflight.checkpoint_segments} else: raise RuntimeError(preflight.recommendation) print(preflight.status) print([hint.to_dict() for hint in preflight.action_hints]) preflight_json = preflight.to_json() # then: sim.forward(n_steps=N, **checkpoint_kwargs) ``` To include a JAX trace of what reverse mode saves, pass a small representative loss as `residual_fun`, with its arguments. `residual_context` is metadata copied into the report. It accepts JSON values, mappings, sequences, NumPy/JAX values (through `.tolist()`), and objects with a valid `to_dict()` or `to_json()`; anything else raises `TypeError`, and non-finite numbers raise `ValueError`. ```python def reduced_loss(params): return jnp.sum(jnp.sin(params) ** 2) preflight = sim.ad_memory_preflight( n_steps=N, available_memory_gb=BUDGET_GB, residual_fun=reduced_loss, residual_args=(jnp.ones((32,)),), residual_workflow="reduced-inverse-design-loss", residual_context={"case": "slab-demo"}, ) ``` The preflight report states its own evidence boundaries: - `static memory planning only` - `trace-time JAX saved-residual explainability only when residual_diagnostic is present` - `not a runtime peak-memory guarantee` - `not XLA memory analysis` - `not profiler evidence` - `not a certificate` - `not RF validation` In short: it is a planning estimate. A run that the plan says fits can still run out of memory. ## What the reports measure Current `estimate_ad_memory(...)`, `plan_ad_memory(...)`, and `explain_ad_memory(...)` artifacts are not certificates. Each report carries an `evidence_class` that says what kind of information it is: | `evidence_class` | Available | From | Meaning | |---|---:|---|---| | `static_estimate` | yes | `estimate_ad_memory(...)` | formula estimate from grid shape, dtype, monitors and active steps | | `calibrated_conservative_plan` | yes | `plan_ad_memory(...)` | budget check on the static estimate with a safety multiplier | | `static_ad_explainability` | yes | `explain_ad_memory(...)` | breakdown of the static estimate into its parts | | `jax_saved_residuals_inspection` | yes | `inspect_ad_saved_residuals(...)` | trace-time listing of what JAX saves; not an XLA memory report or a runtime profile | | `rfx_ad_saved_residuals_diagnostic` | yes | `diagnose_ad_saved_residuals(...)` | parsed and grouped residuals with recommendations; trace-time information, not profiling | | `composite_ad_memory_preflight` | yes | `ad_memory_preflight(...)` | the combined report above | | `static_action_hint` | yes | `ad_memory_preflight(...)` | a deterministic next action, such as `use_checkpoint_every` | | `bounded_certificate` | yes | `ad_memory_compiled_certificate(...)` | the compiler's memory estimate for one exact compiled executable and scope | ## Explain an estimate `explain_ad_memory(...)` breaks the selected estimate (`ad_full_gb` or `ad_segmented_gb`) into the full field tape, segment-boundary state, the live segment's replay tape, CPML and material state, and NTFF monitor state. `mesh_intelligence_report(...)` adds how many cells a graded mesh saves and any geometry or resolution warnings: ```python explain = sim.explain_ad_memory(n_steps=N, available_memory_gb=BUDGET_GB, **checkpoint_kwargs) print(explain.dominant_component) print(explain.recommendations) report = sim.mesh_intelligence_report(n_steps=N, available_memory_gb=BUDGET_GB, **checkpoint_kwargs) print(report.cell_savings_factor) print(report.recommendation) ``` Useful fields of the mesh report: - `cell_savings_factor`: cell count compared with a uniform grid at the smallest configured cell size; - `preflight_issues`: geometry, resolution and feature-support warnings; - `ad_memory.ad_segmented_gb` and `ad_memory.ad_segmented_active_segments`: the segmented estimate and number of active segments; - `recommendation`: the suggested next step. From a source checkout you can write the same planning artifact without running FDTD (`scripts/` is not installed with the wheel): ```bash python scripts/memory_reduction_planning_artifact.py \ --n-steps 10000 \ --available-memory-gb 8.0 \ --output /tmp/rfx-memory-plan.json ``` ## Inspect what JAX saves `diagnose_ad_saved_residuals(...)` traces a function and lists what JAX keeps for the backward pass. Use a small loss or a reduced problem: ```python from rfx import diagnose_ad_saved_residuals diagnostic = diagnose_ad_saved_residuals( reduced_loss, jnp.ones((32,)), workflow="reduced-inverse-design-loss", context={"n_steps": N}, artifacts={"memory_plan": plan.to_json()}, ) print(diagnostic.parser_health) print(diagnostic.top_residuals) print(diagnostic.recommendations) ``` The report keeps the raw output of `jax.ad_checkpoint.print_saved_residuals(...)` and adds each residual's dtype, shape and estimated bytes, grouped summaries, the JAX version, parser health and recommendations. `inspect_ad_saved_residuals(...)` returns only the low-level listing. Both are trace-time information: not profiling, and not XLA memory analysis. ## Check an exact compiled executable The static estimates are formulas. For the compiler's own estimate, compile the exact function you will launch and pass it to `ad_memory_compiled_certificate(...)` with the same checkpoint arguments and a description of its scope. It reads `Compiled.memory_analysis()` once and fails closed if the scope or the byte fields are incomplete: ```python grad_fn = jax.jit(jax.grad(loss)) compiled = grad_fn.lower(eps).compile() certificate = sim.ad_memory_compiled_certificate( compiled, n_steps=N, available_memory_gb=BUDGET_GB, target_fraction=0.85, **checkpoint_kwargs, precision="float32", input_signature={"eps_r": jax.ShapeDtypeStruct(eps.shape, eps.dtype)}, static_signature={"dx": 2e-3, "boundary": "cpml"}, compiled_object_id="slab-demo-grad-v1", runner_or_objective="slab-demo-probe-energy", preflight=preflight, ) print(certificate.status) ``` Signature dictionaries may hold NumPy/JAX arrays or `jax.ShapeDtypeStruct` leaves. rfx records their shape, dtype and weak-type metadata, not full array values. Add explicit fields such as `"role": "design"` when a label matters. | `status` | Meaning | |---|---| | `compiler_estimate_within_budget` | the compiler's estimate fits `available_memory_gb * target_fraction` for this exact scope | | `compiler_estimate_exceeds_budget` | it does not fit; don't launch this scope on this budget | | `scope_incomplete` | required scope metadata is missing or not JSON-safe | | `scope_mismatch` | the preflight metadata contradicts the supplied scope or the compiled object | | `analysis_unavailable` | `memory_analysis()` is missing, raises or returns `None` | | `analysis_incomplete` | a required byte field is missing or invalid | The required bytes are `temp + argument + output - alias`, from the fields `temp_size_in_bytes`, `argument_size_in_bytes`, `output_size_in_bytes` and `alias_size_in_bytes`. Read a pass narrowly. The compiler does not model allocator fragmentation or runtime scratch, so a fit at high utilization can still run out of memory. JAX memory analysis is estimated, version/backend dependent and may be unavailable. `scope_digest`, `config_digest` and `environment_digest` are audit identities over canonical JSON; they do not prove that your Python source corresponds to the compiled executable. A `bounded_certificate` is a compiler estimate for one exact scope, not profiler evidence, not RF validation, and not a runtime peak-memory guarantee. ## Restrict which cells carry a derivative Masking which cells carry a derivative does **not** save memory: JAX keeps whole arrays, not the masked part. (A former `forward(design_mask=...)` option was removed for that reason.) To save memory, use a design box or segmented checkpointing. To keep a gradient out of cells you don't want to design (for example near the absorber), stop it at the `eps_override` call site: ```python region_mask = jnp.zeros_like(eps, dtype=bool).at[4:-4, 4:-4, 4:-4].set(True) eps_masked = jnp.where(region_mask, eps, jax.lax.stop_gradient(eps)) result = sim.forward(eps_override=eps_masked, n_steps=200) ``` The forward values are unchanged. The gradient is exactly zero outside `region_mask` and unchanged inside it. ## Runtime invariant checks `checkify_invariants(...)` makes bounds checks survive `jit` and `grad`: ```python from rfx import check_bounds, checkify_invariants def bounded_loss(eps_r): check_bounds(eps_r, lower=1.0, upper=12.0, name="eps_r") return jnp.sum(eps_r) err, value = checkify_invariants(bounded_loss)(jnp.ones((16,))) assert err.get() is None ``` These catch invalid values at run time. They say nothing about the physics. ## Limits - **The plan fits but the run runs out of memory.** Static estimates are planning numbers, not a runtime peak-memory guarantee. Lower the budget you plan against, or compile the exact function and check it with `ad_memory_compiled_certificate(...)`. - **`checkpoint_segments` raises.** It must divide `n_steps`, and it works only on the uniform single-device path; use `checkpoint_every` on a non-uniform mesh. - **`n_warmup` changes the gradient.** It is an approximation, and it raises on a uniform mesh. - **Fewer steps save memory but can change the gradient.** Run the record-length check in [Gradient Behavior](https://remilab.ai/rfx/guide/gradient-behavior/). - **Keep the evidence with the result**: the plan and mesh report (`.to_json()`), the run command and rfx version, and the validation of the RF quantity you report. A memory report is not RF validation. --- # Multi-device runs Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/run/multi-device/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/run/multi-device.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Run and forward Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/run/run-and-forward/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/run/run-and-forward.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Descend through FDTD to a closed-form optimum Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/showcase/descent-to-optimum/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/showcase/descent-to-optimum.mdx A three-layer coating matches free space to a substrate of εr = 12 over 8–12 GHz. Each layer is 5.5 mm (11 cells) thick; the three permittivities are the design variables and the objective is the band-mean power reflection. The transfer-matrix method gives the exact optimum, so the question is whether gradient descent through a time-domain solve lands there. ![Video poster](https://remilab.cnu.ac.kr/share/7c02ad43c580/ar-coating/ar_coating_site.png) [source](https://remilab.cnu.ac.kr/share/7c02ad43c580/ar-coating/ar_coating.mp4) [Download the loop (MP4)](https://remilab.cnu.ac.kr/share/7c02ad43c580/ar-coating/ar_coating.mp4). 1.1 % largest layer-permittivity gap to the optimum 0.1 % closed-form reflection above the optimum 60 Adam steps [Reflection of the coating from rfx FDTD and the transfer matrix, the three layer permittivities over the Adam iterations, and the band-mean reflection against the transfer-matrix optimum](https://remilab.cnu.ac.kr/share/7c02ad43c580/ar-coating/ar_coating_site.png) ## What happened From the geometric-ladder start (1.861 / 3.464 / 6.447) the layer permittivities settle at 1.922 / 4.670 / 4.707 against the optimum 1.943 / 4.652 / 4.720, although the closed-form optimizer started from a different point. Evaluated with the transfer matrix, the design's band-mean |R|² is 0.03434 against the optimum's 0.03430, −14.6 dB, 3.0 dB below the start. FDTD and transfer-matrix |R|² of the final design differ by at most 1.5×10⁻³ over the 15 frequencies the cost uses (8.20–11.78 GHz). ## How the gradient was checked Against central differences at the start to 5.5×10⁻⁴, and against a 1.5× longer record to 4.8×10⁻⁴. A one-dimensional stack at normal incidence; it shows the optimizer and the gradient work together, not the accuracy of a 3-D radome. ## Downloads [iterations.npz](https://remilab.cnu.ac.kr/share/7c02ad43c580/ar-coating/iterations.npz) [tmm_curves.npz](https://remilab.cnu.ac.kr/share/7c02ad43c580/ar-coating/tmm_curves.npz) [result.json](https://remilab.cnu.ac.kr/share/7c02ad43c580/ar-coating/result.json) [ar_coating_social.mp4](https://remilab.cnu.ac.kr/share/7c02ad43c580/ar-coating/ar_coating_social.mp4) [ar_coating_social.png](https://remilab.cnu.ac.kr/share/7c02ad43c580/ar-coating/ar_coating_social.png) ## Run it ```bash python examples/inverse_design/multilayer_ar_coating.py ``` CPU, minutes; the showcase script `scripts/showcase/ar_coating.py` records every iterate. --- # The forward model, next to openEMS Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/showcase/forward-checks/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/showcase/forward-checks.mdx A gradient is only as useful as the simulation it differentiates. These two boards are solved by rfx and by openEMS, and each comparison is a test in the repository (`tests/crossval/rt5880_patch/` and `tests/crossval/msl_notch_filter/`). Pending for 2.0 (tracker 1471). The curves on this page were computed with rfx 1.8.0 (commit b46a8220), before the settling check judged each frequency bin a result reads. Both cases are being re-measured under the current check, so this page quotes no agreement figure until that run is in the repository. The notch filter's uniform-mesh ladder is not yet shown to converge on the drawn 600 µm line. ## Probe-fed patch on RT/Duroid 5880 [Real and imaginary parts of the input impedance of the RT/Duroid 5880 probe-fed patch from rfx and from openEMS on its finest mesh, 2.0 to 2.9 GHz](https://remilab.cnu.ac.kr/share/7c02ad43c580/forward-patch/forward_curves_rt5880_patch_site.png) The figure shows the real and imaginary parts of the input impedance from both solvers. Neither model declares a probe radius, so each feed cell sets the probe's series reactance; the two solvers' reactance curves differ for that reason, and the case judges the resonance and the input resistance, not the `|S11|` dip. [rfx_curves.npz](https://remilab.cnu.ac.kr/share/7c02ad43c580/forward-patch/rfx_curves.npz) [reference_curves.npz](https://remilab.cnu.ac.kr/share/7c02ad43c580/forward-patch/reference_curves.npz) [result.json](https://remilab.cnu.ac.kr/share/7c02ad43c580/forward-patch/result.json) ## Microstrip open-stub notch filter [|S21| and |S11| of the microstrip open-stub notch filter from rfx and from openEMS on its finest mesh, 2 to 7 GHz](https://remilab.cnu.ac.kr/share/7c02ad43c580/forward-notch/forward_curves_msl_notch_filter_site.png) A 12 mm open stub on a 50 Ω microstrip line (254 µm, εr 3.66) is a quarter wave near 3.7 GHz, where it shorts the line. The figure shows |S21| and |S11| from both solvers. [rfx_curves.npz](https://remilab.cnu.ac.kr/share/7c02ad43c580/forward-notch/rfx_curves.npz) [reference_curves.npz](https://remilab.cnu.ac.kr/share/7c02ad43c580/forward-notch/reference_curves.npz) [result.json](https://remilab.cnu.ac.kr/share/7c02ad43c580/forward-notch/result.json) Each comparison holds for its board, mesh, band and observable. --- # What does a gradient cost? Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/showcase/gradient-cost/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/showcase/gradient-cost.mdx The patch model above has 1.98 million cells and runs 9,912 time steps. On one RTX A6000 a forward run takes about 17 s. The value and the gradient with respect to every cell of the design region take about 24 s, whether the region holds 12,400, 20,592 or 43,008 cells. [Bar chart of wall time on an RTX A6000 for a forward run, the value and gradient, and central differences, for the patch footprint, the design box and the whole substrate layer](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/gradient_cost_site.png) | Box | Cells | Forward (s) | Value + gradient (s) | Peak memory (GB) | Central differences (derived, days) | |---|---:|---:|---:|---:|---:| | patch footprint | 12,400 | 17.06 | 23.97 | 4.74 | 4.9 | | design box | 20,592 | 16.74 | 23.68 | 7.63 | 8.0 | | whole substrate layer (incl. 19,436 vacuum cells) | 43,008 | 16.91 | 24.31 | 15.49 | 16.8 | ## What grows instead Memory. The reverse pass keeps a per-step record for the cells of the design box, so peak device memory rises from 0.44–0.45 GB for a forward run to 4.7, 7.6 and 15.5 GB. For larger regions, [Reduce Memory Use](https://remilab.ai/rfx/run/memory-and-speed/) explains checkpointing, which trades recomputation for memory. The central-difference column is not measured: it is 2 × N × the measured forward time. ## Downloads [result.json (patch-sensitivity)](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/result.json) [gradient_cost_social.png](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/gradient_cost_social.png) --- # Showcase Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/showcase/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/showcase/index.mdx Each case below was computed with the current rfx code on the lab's VESSL cluster and checked by something other than the gradient itself: finite differences, a closed-form optimum, or another solver on the same board. The figures, loops and data are downloadable, and each case has a machine-readable result.json. [![Map of the derivative of |S11|² with respect to the permittivity of each substrate cell under a patch antenna, beside its |S11| curve and its |E_z|² map](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/patch_sensitivity_site.png)](https://remilab.ai/rfx/showcase/sensitivity-map/) Which cells move the match? One reverse pass maps the sensitivity of a patch antenna's |S11| to every substrate cell. 20,592 cells, one pass [Read the case →](https://remilab.ai/rfx/showcase/sensitivity-map/) [![Bar chart of wall time on an RTX A6000 for a forward run, the value and gradient, and central differences, for three design regions](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/gradient_cost_site.png)](https://remilab.ai/rfx/showcase/gradient-cost/) What a gradient costs Time and memory for the value and gradient against a forward run, as the design region grows. 24 s vs 4.9–16.8 days [Read the case →](https://remilab.ai/rfx/showcase/gradient-cost/) [![Reflection of a three-layer anti-reflection coating, its layer permittivities over the Adam iterations, and the band-mean reflection against the transfer-matrix optimum](https://remilab.cnu.ac.kr/share/7c02ad43c580/ar-coating/ar_coating_site.png)](https://remilab.ai/rfx/showcase/descent-to-optimum/) Descend to the closed-form optimum Adam through the FDTD solve finds the three layer permittivities of a broadband anti-reflection coating. within 1.1 % of the transfer-matrix optimum [Read the case →](https://remilab.ai/rfx/showcase/descent-to-optimum/) [![Input impedance of the RT/Duroid 5880 probe-fed patch from rfx and from openEMS](https://remilab.cnu.ac.kr/share/7c02ad43c580/forward-patch/forward_curves_rt5880_patch_site.png)](https://remilab.ai/rfx/showcase/forward-checks/) The forward model next to openEMS Input impedance of the patch and |S21| of a microstrip notch filter, on the same boards as openEMS. being re-measured under the current settling check [Read the case →](https://remilab.ai/rfx/showcase/forward-checks/) More cases are being recomputed with the current code: the waveguide dielectric taper and the beam-steering dielectric cover from the 2026 paper. Their original films are kept in [the archive](https://remilab.ai/rfx/gallery/) until then. --- # Which cells move a patch antenna's match? Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/showcase/sensitivity-map/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/showcase/sensitivity-map.mdx A 39.7 × 49.25 mm patch on 3.175 mm RT/Duroid 5880 (εr 2.2), fed by a 50 Ω probe, resonates near 2.3 GHz. We ask how |S11| at 2.356 GHz, 1 % above the return-loss minimum, changes when the permittivity of any one substrate cell changes. rfx answers for all 20,592 cells under and around the patch with one forward and one reverse pass. ![Video poster](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/patch_sensitivity_poster.png) [source](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/patch_sensitivity.mp4) [Download the loop (MP4)](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/patch_sensitivity.mp4). 20,592 design cells 2×10⁻⁵ worst reverse-mode vs finite-difference difference (float64) 24 s value and gradient on one RTX A6000 (1.4× a forward run) [The sensitivity map, |S11| with f_r and f_t marked, and the |E_z|² panel](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/patch_sensitivity_site.png) ## What the map shows The derivative is positive wherever it is significant and concentrated in two bands, under and just outside the two radiating edges; along the centre line between them it is about 450 times weaker. That is what cavity perturbation predicts for the TM010 mode: a local permittivity change shifts the resonance in proportion to the local |E_z|², and E_z of this mode peaks at the radiating edges and vanishes on the centre line. The map and |E_z(2.356 GHz)|² from the same run correlate at r = 0.948. The sign is the expected one: raising εr lowers the resonance, which moves the return-loss minimum further below 2.356 GHz and raises |S11| there. ## How the gradient was checked - Finite differences: six blocks of cells chosen before the run (at the radiating edges, on the centre line, outside the patch). On the four large enough to judge, reverse mode and central differences agree to 2.4×10⁻⁶–1.9×10⁻⁵ relative, in float64. - Record length: repeating the gradient with a 1.5× longer time record changes those block sums by at most 5.4×10⁻⁴. - The same board's forward model agrees with openEMS (see [forward checks](https://remilab.ai/rfx/showcase/forward-checks/)). This map is for this board on a mesh of 4 cells through the substrate, whose resonance is 0.91 % below the finest mesh's; it is not a manufacturing tolerance or a statement about other boards. ## What the code looks like ```python def J(eps_box): res = board.sim.forward( design_box=corners, design_eps_override=eps_box, design_box_holds_ports=True, n_steps=n_steps, checkpoint=False, checkpoint_segments=checkpoint_segments, port_s11_freqs=freqs, skip_preflight=True) s = res.s_params s = s if s.ndim == 1 else s[0] return jnp.real(s[0]) ** 2 + jnp.imag(s[0]) ** 2 v, g = jax.jit(jax.value_and_grad(J))(eps_bg) ``` [Full script →](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/scripts/showcase/patch_sensitivity.py) ## Downloads [gradient_full.npz](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/gradient_full.npz) [ez2_mid.npz](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/ez2_mid.npz) [baseline_h4.s1p](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/baseline_h4.s1p) [result.json](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/result.json) [patch_sensitivity_social.mp4 (4:5)](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/patch_sensitivity_social.mp4) [patch_sensitivity_social.png](https://remilab.cnu.ac.kr/share/7c02ad43c580/patch-sensitivity/patch_sensitivity_social.png) Evidence rfx commit [655b1fc6](https://github.com/bk-squared/rfx/commit/655b1fc6c1989fed88fe80ffbeb19ffa0bcf1277) (checks) and [b46a8220](https://github.com/bk-squared/rfx/commit/b46a8220775b67f8720fb13c3e256c01cf7d6883) (board builder) the pre-declaration note [docs/design_notes/20260927_showcase_predeclaration.md](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/design_notes/20260927_showcase_predeclaration.md) f_t = 1.01 × the |S11| minimum (2.333099 GHz → 2.356430 GHz, |S11(f_t)| = −12.57 dB on the rising flank) FD step Δεr = 0.1 on blocks of 3 × 3 cells through the 4-cell substrate (36 cells, 2.4 × 2.4 mm); judged blocks are those with |FD| ≥ 10 % of the largest 418 of 20,592 cells have a negative derivative, the largest 0.30 % of the maximum issue [#1332](https://github.com/bk-squared/rfx/issues/1332), PR [#1337](https://github.com/bk-squared/rfx/pull/1337) --- # Migration Guide Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/start/coming-from-other-solvers/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/start/coming-from-other-solvers.md This page helps you move a model from **Meep** or **openEMS** to rfx. All three are Yee-cell FDTD solvers, so the physics is familiar; the API is different. It maps the concepts, translates three typical scripts and lists the differences that most often catch newcomers. Before you port a production model, check that its mesh, boundaries, ports and observables are inside the [Recommended Configuration](https://remilab.ai/rfx/validation/recommended-configuration/) and [Support Boundaries](https://remilab.ai/rfx/support/). ## Concept mapping | Concept | Meep | openEMS | rfx | |---------|------|---------|-----| | Grid setup | `Simulation(resolution=N)` | `InitCSX()` + `InitFDTD()` | `Simulation(freq_max=..., domain=...)` or `Simulation.auto(...)` | | Cell size | `resolution` (cells per unit length) | `SetDeltaUnit(1e-3)` and mesh lines | `dx=` in metres; derived from `freq_max` if omitted | | Source | `Source`, `EigenModeSource` | `AddExcitation` | `add_source()` (no impedance), `add_port()` and the other port types | | S-parameters | `add_flux()` + post-processing | `CalcPort` | one calculator per port family: lumped/wire `run(compute_s_params=True)`, microstrip `compute_msl_s_matrix()`, waveguide `compute_waveguide_s_matrix()`, coax `compute_coaxial_line_reflection()` / `compute_coaxial_two_port()` | | Resonances | `harminv(...)` | manual FFT | `result.find_resonances()` | | Auto-stop | `stop_when_fields_decayed` | `EndCriteria` | `run(until_decay=1e-3)` for open problems; a fixed `n_steps` for a closed PEC cavity | | Materials | `Medium(epsilon=...)` | `AddMaterial` | `sim.add_material(...)`, or a library name such as `"fr4"` | | Metal | `perfect_electric_conductor` | `AddMetal` | `sim.add(shape, material="pec")` for a **volume**; `sim.add_thin_conductor(...)` or a zero-thickness `Box` for a **sheet** | | Absorber | `PML(thickness)` | `AddPML` | `boundary="cpml"`, `cpml_layers=` | | Dispersive media | `LorentzianSusceptibility` | `AddLorentzMaterial` | `DebyePole`, `LorentzPole`, `drude_pole()` | | Gradients | adjoint solver for design-region objectives | not native | `jax.grad` through the solver | | Inverse design | `meep.adjoint.OptimizationProblem` | not native | `rfx.optimize(sim, region, objective)` | | Non-uniform mesh | not native | `SmoothMeshLines` | `dz_profile=` (and `dx_profile`/`dy_profile`) with the limits in [Non-Uniform Mesh](https://remilab.ai/rfx/model/domain-mesh/) | An openEMS `AddMetal` box with one zero-length side is a sheet, and a solid box is a volume. rfx follows the same split: the declaration decides it. See [Core Concepts](https://remilab.ai/rfx/start/concepts/). ## Translating scripts ### Meep: cavity resonance ```text # Meep (for comparison only) import meep as mp sim = mp.Simulation( cell_size=mp.Vector3(0.1, 0.1, 0.05), resolution=50, boundary_layers=[], ) sim.sources = [mp.Source( mp.GaussianSource(frequency=2.0, fwidth=0.5), component=mp.Ez, center=mp.Vector3(0.03, 0.03, 0.02), )] sim.run(until_after_sources=mp.stop_when_fields_decayed(50, mp.Ez, mp.Vector3(), 1e-6)) ``` ```python # rfx equivalent from rfx import Simulation sim = Simulation(freq_max=5e9, domain=(0.1, 0.1, 0.05), boundary="pec") sim.add_source(position=(0.03, 0.03, 0.02), component="ez", amplitude_kind="current") sim.add_probe(position=(0.06, 0.06, 0.02), component="ez") result = sim.run(n_steps=5000) modes = result.find_resonances() # list of modes with .freq, .Q, ... print([f"{m.freq / 1e9:.3f} GHz" for m in modes[:3]]) ``` Coordinates are in metres and `freq_max` in hertz; there are no normalized units. With no `dx`, rfx picks a cell size from `freq_max`, and with no waveform the source is a `GaussianPulse` centred at `freq_max / 2`. A closed PEC box has no loss, so its field never decays and `until_decay` is the wrong stop. Choose a fixed record long enough for the frequency resolution you need, repeat with a longer one, and keep only the modes whose frequency does not move. The Q of a lossless cavity is infinite; add material or conductor loss before you read a Q. ### openEMS: waveguide S-parameters ```matlab % openEMS (MATLAB, for comparison only) CSX = InitCSX(); FDTD = InitFDTD('EndCriteria', 1e-5); [CSX, port{1}] = AddRectWaveGuidePort(CSX, 0, 1, ...); RunOpenEMS(Sim_Path, Sim_CSX); port = calcPort(port, Sim_Path, freq); s11 = port{1}.uf.ref ./ port{1}.uf.inc; ``` ```python # rfx equivalent import jax.numpy as jnp from rfx import Simulation # WR-90: the transverse domain is the guide cross-section. Its faces are the # guide walls, declared PEC; the absorber sits on the port axis only. freqs = jnp.linspace(8e9, 11.5e9, 8) # above the 6.56 GHz TE10 cutoff sim = Simulation(freq_max=12e9, domain=(0.10, 0.02286, 0.01016), dx=2e-3, boundary={"x": "cpml", "y": "pec", "z": "pec"}, cpml_layers=16) sim.add_waveguide_port(0.024, direction="+x", freqs=freqs, f0=9.75e9, name="in") sim.add_waveguide_port(0.076, direction="-x", freqs=freqs, f0=9.75e9, name="out") result = sim.compute_waveguide_s_matrix(num_periods=45) s11 = result.s_params[0, 0, :] # shape (n_ports, n_ports, n_freqs) ``` The mesh here is coarse so the example runs quickly. The calculator's warnings name the absorber depth and record length it needs; 16 layers and 45 periods satisfy them here. `normalize=False` (the default) is the modal V/I decomposition; `normalize="flux"` gives a power-normalized transmission magnitude. There is no single `CalcPort` equivalent: `run(compute_s_params=True)` is only for lumped and wire `add_port(...)` ports. See [Probes and S-Parameters](https://remilab.ai/rfx/measure/s-parameters/). ### Meep adjoint: inverse design ```text # Meep (for comparison only) opt = mpa.OptimizationProblem(...) opt.update_design([design_params]) f, g = opt() # forward + adjoint ``` ```python # rfx: optimize() runs jax.grad through the solver; no adjoint code to write import jax.numpy as jnp from rfx import Simulation, Box, DesignRegion, GaussianPulse from rfx.optimize import optimize from rfx.optimize_objectives import minimize_s11_at_freq_wave_decomp sim = Simulation(freq_max=4e9, domain=(0.05, 0.05, 0.025), dx=2.5e-3, boundary="pec") sim.add_material("slab_init", eps_r=4.0) sim.add(Box((0.015, 0.015, 0.005), (0.035, 0.035, 0.020)), material="slab_init") sim.add_port((0.025, 0.025, 0.0025), "ez", # in the air below the slab waveform=GaussianPulse(f0=3e9, bandwidth=0.8)) region = DesignRegion( corner_lo=(0.015, 0.015, 0.005), corner_hi=(0.035, 0.035, 0.020), eps_range=(1.0, 12.0), ) objective = minimize_s11_at_freq_wave_decomp(target_freq=3e9, port_idx=0) # This objective needs the port DFT at the target frequency. opt = optimize(sim, region, objective, n_iters=3, lr=0.01, n_steps=400, port_s11_freqs=jnp.asarray([3e9]), verbose=False) print(opt.loss_history) # opt.eps_design holds the optimized permittivity ``` Three iterations on a coarse mesh keep the example short, and preflight warns that 2.5 mm cells are at the edge of phase accuracy in the slab. A real design needs a finer mesh, many more iterations and a converged forward run to check the final result. See [Inverse Design](https://remilab.ai/rfx/design/optimization/). ## What is different in rfx - **Everything runs in Python through JAX.** There is no solver binary and no file exchange. The same script runs on CPU, or on an NVIDIA GPU with a CUDA-enabled JAX build. - **Gradients come from the solver itself.** Supported workflows let `jax.grad` differentiate through the time stepping, so an ordinary Python loss function can drive an optimizer. Check the final design's RF result with a converged run; the loss is only a proxy. - **One `Simulation` object holds the model.** Add materials, shapes, sources, ports and observables to it, then call `run()` or a port calculator. `run()` computes lumped and wire port S-parameters automatically; other port families use their own calculators. - **Preflight checks the setup before you spend time on a solve.** Read `sim.preflight()`. `run()` repeats it and prints the warnings. - **Snapshots.** `run(snapshot=SnapshotSpec(...))` records field frames on the uniform grid, with a fixed `n_steps` or with `until_decay`. The frames are `interval` steps apart (default 10); read their times from `result.snapshot_axes`. A graded mesh, multi-device runs and the ADI solver do not record snapshots and raise if you ask for them. - **A built-in material library.** `fr4`, `rogers4003c`, `rogers4350b`, `rt_duroid_5880`, `alumina`, `ptfe`, `silicon`, `copper`, `aluminum`, `pec`, `air`, `vacuum` and `water_20c` work by name. Their values are nominal: use your laminate's data for a real design. - **Automatic defaults.** Without `dx`, the cell size comes from `freq_max`. The CPML is 16 layers unless you set `cpml_layers`. `Simulation.auto(freq_range=(f_min, f_max))` proposes a domain and mesh from a band. ## Common gotchas | Gotcha | What to do | |-------|----------| | Positions are in metres, not millimetres or cells | Write `0.012`, not `12` | | `freq_max` is in hertz, not normalized frequency | `freq_max=5e9` for 5 GHz | | The absorber cells are added outside `domain=` | `domain=` is the region you model. Geometry drawn to a face continues into the absorber, as a board running off the edge should | | `run()` returns a `Result`, not files | Read `result.time_series`, `result.s_params`, `result.find_resonances()` | | There is no mesh file | The solved grid is `result.grid`; `rfx.plan_simulation_mesh(sim)` inspects the mesh before a run | | An argument a path does not support raises | `run()` refuses, for example, `until_decay` on a closed graded mesh before the first step; the message says what to change | ## Next [Quick Start](https://remilab.ai/rfx/start/first-simulation/) runs a first simulation, and [Core Concepts](https://remilab.ai/rfx/start/concepts/) explains the model behind it. --- # Core Concepts Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/start/concepts/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/start/concepts.mdx rfx is a finite-difference time-domain (FDTD) solver. It divides space into small rectangular cells, steps Maxwell's equations forward in time, and records what you ask for. This page explains the choices a model makes and what each one does to your result. Read it once before you build your own device. ## The grid and the time step rfx uses the standard **Yee lattice**. Each field component lives at its own position in the cell. E components sit on cell edges and H components sit on cell faces. You rarely need the exact positions, but they explain two things you will see: - Geometry snaps to the lattice. A face drawn between two grid planes lands on the nearest plane. `sim.fidelity_report()` prints the declared and realized position of every face. - A probe or port reads a field at a grid location, which may be up to half a cell away from the coordinate you gave. **Cell size.** The cell must resolve the shortest wavelength *inside the material*, λ_eff = c / (f_max √ε_r). Aim for at least 15–20 cells per λ_eff for propagation, and more for anything phase-sensitive. Preflight warns when a dielectric has fewer than 15. The thinnest feature that matters also needs a few cells, such as a substrate or a gap. On a uniform grid, those two requirements together set `dx`. When they conflict, use a graded mesh; see [Non-Uniform Mesh](https://remilab.ai/rfx/model/domain-mesh/). **Time step.** rfx picks the largest stable step from the cell size (the Courant limit). You normally leave `dt` alone. A run of N steps covers `N * result.dt` seconds. **Cost.** Memory and time per step scale with the number of cells. Halving `dx` in 3-D gives 8× the cells and about 2× the steps, so roughly 16× the work. Start coarse, then refine. ## Boundaries The boundary decides what happens at the edge of the domain. | Boundary | Use it for | Notes | |---|---|---| | `"cpml"` | open problems: antennas, scattering, lines that leave the box | an absorber `cpml_layers` cells thick (default 16), added **outside** `domain` | | `"pec"` | closed problems: cavities, shielded boxes | energy cannot leave, so resonances ring indefinitely | | `"pmc"` (via `BoundarySpec`) | a symmetry plane, which halves the problem | only where the fields really are symmetric | | `"periodic"` (via `BoundarySpec`) | infinite arrays and unit cells | the paired faces go together | | `"upml"` | legacy absorber | reflects noticeably more than CPML; prefer CPML | A single string (`"cpml"`, `"pec"` or `"upml"`) applies to all six faces. Use `BoundarySpec` to set each face separately, and for `"pmc"` and `"periodic"`: ```python from rfx import Simulation from rfx.boundaries.spec import Boundary, BoundarySpec # Open on all sides except an infinite PEC ground at z = 0. spec = BoundarySpec(x="cpml", y="cpml", z=Boundary(lo="pec", hi="cpml")) sim = Simulation(freq_max=10e9, domain=(0.02, 0.02, 0.01), dx=0.5e-3, boundary=spec, cpml_layers=8) print(spec.to_dict()) ``` Don't mix roles. A cavity with one absorbing wall is neither a cavity nor an antenna, and its Q means nothing. A PEC **boundary** is also infinite in its own plane. For a finite ground plane, draw a metal sheet inside an all-CPML domain instead, as described next. ## Materials A material has a relative permittivity `eps_r`, a conductivity `sigma` (S/m) and optionally a permeability `mu_r`. Frequency-dependent materials use Debye or Lorentz poles. rfx ships a small library that includes `"fr4"`, `"rogers4350b"`, `"ptfe"`, `"copper"` and `"pec"`. See [Materials and Geometry](https://remilab.ai/rfx/model/materials/). Any material with `sigma >= 1e6` S/m is treated as a **perfect conductor**. Its tangential E is forced to zero. The finite conductivity is not used, so library `copper` behaves as lossless PEC. For conductor loss on a metal sheet, use `add_thin_conductor(..., surface_impedance_f0=f0)`. ## Three kinds of conductor Metal inside the domain is always exactly one of three things. **Your declaration decides which**, not the shape's size on the mesh: | Kind | Real-world object | Declare it with | What the solver builds | |---|---|---|---| | **sheet** | etched copper: ground plane, patch, trace | `sim.add_thin_conductor(Box(...))` with zero thickness on one axis | one grid plane with in-plane E zeroed; occupies no cell | | **volume** | plate, iris, post, machined wall | `sim.add(shape, material="pec")` | walls at both drawn faces, interior shorted | | **wire** | thin filament | `PolylineWire` thinner than half a cell | a path of zeroed E edges | ```python from rfx import Box from rfx.mesh_edges import edge_aware_profiles substrate = Box((0.005, 0.005, 0.004), (0.015, 0.015, 0.005)) foil = Box((0.005, 0.005, 0.004), (0.015, 0.015, 0.004)) # zero thickness in z block = Box((0.007, 0.007, 0.006), (0.009, 0.009, 0.007)) # x and y mesh lines placed so the foil's solved edges fall on its drawn edges. profiles = edge_aware_profiles((0.02, 0.02, 0.01), 0.5e-3, sheets=[foil], solids=[substrate, block]) sim = Simulation(freq_max=10e9, domain=(0.02, 0.02, 0.01), dx=0.5e-3, boundary="cpml", cpml_layers=8, **profiles) sim.add_material("substrate", eps_r=3.66) sim.add(substrate, material="substrate") # 35 um copper foil on the substrate floor: a sheet. sim.add_thin_conductor(foil) # A 2 x 2 x 1 mm metal block: a volume. sim.add(block, material="pec") sim.fidelity_report() # declared vs realized planes for every entity ``` The first `add_thin_conductor` call warns that the sheet is lossless PEC. That warning is expected: see the materials note above. A sheet is solved as if it reached about 0.35 of a cell past its last mesh node, so on a plain uniform grid this 10 mm foil is solved wider than drawn. Preflight reports a sheet solved more than 1 % off its drawing as an error, so `run()` and `forward()` stop before the first step. Build the in-plane mesh with `rfx.mesh_edges.edge_aware_profiles`, as above, so the solved edges fall on the drawn ones; or pass `Simulation(..., snap="declared")` to accept the difference as a warning, recorded in `sim.realized_geometry()`. With the graded in-plane mesh, preflight also notes that the grading reaches the absorber runway. A PEC shape that is thinner than one cell but not zero is neither a sheet nor a resolved volume. Preflight reports it as an error and `run()` refuses it, so you must choose one or the other. The full rule, with the index conventions, is in [How conductors land on the lattice](https://remilab.ai/rfx/model/materials/#how-conductors-land-on-the-lattice). ## Sources and ports - A **source** (`add_source`, `add_tfsf_source`, `add_polarized_source`) injects a field. It has no impedance and does not load the structure. Use it for fields, resonances, far-field patterns and scattering. - A **port** injects a signal *and* terminates the line in a known impedance, so rfx can compute S-parameters. Each port family has its own calculator, and only that calculator gives S-parameters for it: | Structure | Port | S-parameter calculator | |---|---|---| | discrete feed, lumped component terminals | `add_port(...)` (lumped; `extent=` makes a wire port) | `run(compute_s_params=True)` or `forward(port_s11_freqs=...)` | | microstrip line | `add_msl_port(...)` | `compute_msl_s_matrix()` | | rectangular waveguide | `add_waveguide_port(...)` | `compute_waveguide_s_matrix()` | | coaxial line | `add_coaxial_port(...)` | `compute_coaxial_line_reflection()`, `compute_coaxial_two_port()` | Probes, flux monitors and DFT planes record fields. They do not define a reference impedance or a reference plane, so they cannot produce S-parameters. [Sources and Ports](https://remilab.ai/rfx/model/ports/) covers every family. ## Observables | Want | Use | |---|---| | a field value over time at a point | `add_probe`, `add_vector_probe` | | a field on a plane at chosen frequencies | `add_dft_plane_probe` | | power through a plane (R, T, absorption) | `add_flux_monitor` | | radiation pattern, directivity, RCS | `add_ntff_box`, then `compute_far_field` / `compute_rcs` | | resonant frequencies and Q | a probe plus `result.find_resonances()` (Harminv) | | S-parameters | a port family and its calculator (table above) | ## Is the run finished? A pulse must ring down before its spectrum means anything. Every spectral quantity is a Fourier transform of a finite record: S-parameters, flux, far fields, DFT planes and resonances. If the record stops while energy is still bouncing around, the result is wrong in a way that no warning can fully catch. - Check `result.settling_witness["status"]`. For every frequency a spectral result reads, rfx estimates from the poles still ringing after the source ends how much of the value lies past the record's end; `settling_db` is the worst share in dB, and at or below −40 dB the status is `pass`. `fail`, `undetermined` and `absent` (nothing recorded to judge) are not passes. - The S-parameter calculators warn when a drive's record ends too early. - High-Q structures need long records. A resonator with quality factor Q decays over roughly Q / (π f) seconds. ## Preflight and fidelity Two checks run without a solve: - `sim.preflight()` returns a report of errors and warnings about the setup. Examples are cell size against wavelength, structures touching the absorber, far-field boxes too close to the radiator, graded-mesh problems and lossless resonators. `run()` repeats it automatically. - `sim.fidelity_report()` prints where each shape actually landed on the grid. Neither check is an accuracy guarantee. A clean preflight means rfx found none of the mistakes it knows about. Accuracy comes from a convergence study and a comparison with a reference; see [Convergence Study](https://remilab.ai/rfx/evidence/mesh-convergence/) and [Validation](https://remilab.ai/rfx/guide/validation/). ## `run()` versus `forward()` - `sim.run(...)` is the general entry point. It returns a `Result` with probe time series, S-parameters, DFT planes, flux and far-field data. - `sim.forward(...)` is the differentiable entry point. Wrap it in `jax.grad` to get gradients with respect to material overrides or design variables. It supports a narrower set of features. See [Autodiff and Adjoint](https://remilab.ai/rfx/design/gradients/) and [Inverse Design](https://remilab.ai/rfx/design/optimization/). ## Precision The default is `precision="float32"`, which suits most work and is what the GPU runs fastest. `"float64"` needs JAX 64-bit mode enabled before you build the simulation. `"mixed"` stores fields in float16. Both are limited to the uniform, single-device solver. Mixed precision raises the CPML's reflection floor, so avoid it for low-reflection or S-parameter work. --- # Quick Start Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/start/first-simulation/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/start/first-simulation.mdx This page walks through one complete simulation: a Gaussian pulse passing through a dielectric slab in open space. Every rfx model follows the same six steps: 1. **Domain and mesh.** Create a `Simulation` with its size, cell size and boundaries. 2. **Materials and geometry.** Register materials and place shapes. 3. **Excitation.** Add a source, or a port if you want S-parameters. 4. **Observables.** Add probes, monitors or far-field boxes. 5. **Preflight.** Let rfx check the setup before you spend time on a solve. 6. **Run and read.** Call `run()` and read the returned `Result`. The example runs in a few seconds on a laptop CPU. ## 1. Domain and mesh ```python import numpy as np from rfx import Simulation, Box, GaussianPulse sim = Simulation( freq_max=5e9, # highest frequency you care about (Hz) domain=(0.06, 0.04, 0.04), # x, y, z size in metres dx=2e-3, # cubic cell size in metres boundary="cpml", # absorbing (open) boundary on every face cpml_layers=8, # absorber thickness in cells ) ``` - All lengths are in **metres**, frequencies in **hertz**, and conductivity in **S/m**. - `freq_max` does two things. It sets the default time step, and it is the frequency preflight uses to judge whether the mesh is fine enough. - The CPML absorber is added **outside** the `domain` you give, so `domain` is the region you model. Structures that reach the domain edge continue into the absorber, which is how an infinite slab or a line leaving the box is modelled. Keep sources and probes a few cells in from the edge. ## 2. Materials and geometry ```python sim.add_material("slab", eps_r=3.0, sigma=0.005) sim.add(Box((0.028, 0.0, 0.0), (0.036, 0.04, 0.04)), material="slab") ``` `Box(corner_lo, corner_hi)` is an axis-aligned block. Here the slab is 8 mm thick along x and spans the whole cross-section. Library materials such as `"fr4"`, `"rogers4350b"` and `"pec"` need no `add_material` call. See [Materials and Geometry](https://remilab.ai/rfx/model/materials/). ## 3. Excitation ```python sim.add_source( (0.014, 0.02, 0.02), "ez", waveform=GaussianPulse(f0=3e9, bandwidth=0.8), amplitude_kind="current", ) ``` This is a soft point source that drives `Ez` in one cell. `amplitude_kind="current"` means the waveform amplitude is a current in amperes. Always pass it explicitly. A point source has no impedance, so it gives fields, resonances and far fields but **not** S-parameters. Use a port for those; see [Sources and Ports](https://remilab.ai/rfx/model/ports/). ## 4. Observables ```python sim.add_probe((0.020, 0.02, 0.02), "ez") # probe 0: between source and slab sim.add_probe((0.046, 0.02, 0.02), "ez") # probe 1: behind the slab ``` A probe records one field component at one point for every time step. ## 5. Preflight ```python report = sim.preflight() print(report.format()) report.raise_for_failure() ``` Preflight looks for mistakes that give a wrong answer without crashing: - a mesh too coarse for the material and `freq_max`; - structures or probes inside the absorber; - geometry that does not land where you drew it; - a lossless resonator whose Q can never converge. Errors stop the run. Warnings are advisory, but read every one of them. `run()` repeats the check and prints any warnings. Try `eps_r=4.0` on the slab. At 2 mm cells that leaves exactly 15 cells per wavelength inside the dielectric, and preflight will warn about phase accuracy. ## 6. Run and read the result ```python result = sim.run(n_steps=1000) ts = np.asarray(result.time_series) # shape (n_steps, n_probes) print(ts.shape, f"dt = {result.dt:.3e} s") spectrum = np.abs(np.fft.rfft(ts, axis=0)) freqs = np.fft.rfftfreq(ts.shape[0], d=result.dt) in_band = (freqs > 1e9) & (freqs < 5e9) ratio = spectrum[in_band, 1] / spectrum[in_band, 0] print("settling:", result.settling_witness["status"]) # "absent" here: nothing to judge ``` - `result.time_series` has one column per probe, in registration order. - `result.settling_db` judges whether the record was long enough for the spectral results the run computed (S-parameters, DFT planes, flux monitors, far fields). After the source ends, rfx fits the poles still ringing in the record and estimates, at every frequency those results read, how much of the value comes from what rings on past the record's end. `settling_db` is the worst of those shares in dB; at or below −40 dB the record is long enough. `result.settling_witness["status"]` says `pass`, `fail`, `undetermined` (the source was still on, or the fit could not be judged) or `absent` (nothing the run reads was recorded to judge, as here). Only `pass` is a pass. - This run reads only probe time series, so the check is `absent` and the FFT above is yours to judge: if the probe signals have not decayed to a small fraction of their peak by the last step, run longer. The ratio above only compares the spectra at two points. It is **not** a transmission coefficient, because probe 0 sees the incident and reflected waves together. For reflection and transmission, use two flux monitors and a reference run without the slab. The [Probes and S-Parameters](https://remilab.ai/rfx/measure/s-parameters/) guide shows how. ## Choosing the run length `run()` offers three ways to decide how long to step: | Argument | Meaning | |---|---| | `n_steps=N` | exactly N time steps | | `num_periods=P` | P periods at `freq_max` (default 20) | | `until_decay=1e-3` | step until the field energy has decayed to that fraction of its peak, up to `decay_max_steps` | With absorbing boundaries, `until_decay` watches the total energy inside the domain, which is a reliable stop for spectra, flux and S-parameters. A closed PEC box never loses energy, so there it falls back to a single field sample (on a graded mesh, a closed-boundary `until_decay` is refused instead). That fallback is only reliable for lossy or radiating ring-down, so use a fixed `n_steps` in a closed structure. Also use a fixed `n_steps` when you compare runs, for example in a convergence study. ## Where to go next - [Core Concepts](https://remilab.ai/rfx/start/concepts/): the mental model behind the six steps, including cells, boundaries, conductors, sources and ports. - [Your First Patch Antenna](https://remilab.ai/rfx/examples/first-patch/): a real device on a graded mesh. - [Examples](https://remilab.ai/rfx/examples/): the ordered learning path of runnable scripts. --- # Installation Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/start/install/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/start/install.mdx ## Install rfx needs Python 3.11 or newer. The PyPI package is `rfx-fdtd`, and the import name is `rfx`. ```bash pip install rfx-fdtd ``` This installs JAX and jaxlib 0.10.2 or newer (CPU), NumPy, SciPy, matplotlib and h5py. ### GPU rfx runs on whatever device JAX finds. For an NVIDIA GPU, install the CUDA build of JAX: ```bash pip install "jax[cuda12]" rfx-fdtd python -c "import jax; print(jax.devices())" # should list a CUDA device ``` See the [JAX installation guide](https://docs.jax.dev/en/latest/installation.html) for CUDA and driver requirements. ### Optional extras | Extra | Adds | Needed for | |---|---|---| | `rfx-fdtd[optimization]` | optax | gradient-based optimizers in inverse design | | `rfx-fdtd[visualization]` | matplotlib, pillow | plots and field animations | | `rfx-fdtd[cad]` | trimesh, rtree, cascadio | importing STL/STEP geometry with `MeshShape` | | `rfx-fdtd[dashboard]` | streamlit, pandas | the `rfx-dashboard` browser UI | | `rfx-fdtd[all]` | everything above plus the dev tools | development | ## Check the install ```bash rfx-diagnose ``` This prints a PASS / WARN / FAIL line for Python, rfx, the JAX backend and devices, the core dependencies and the optional extras. It then runs a tiny FDTD simulation. It exits with code 0 when every critical check passes. Two warnings are normal: - **CPU-only backend.** Everything works, but large 3-D runs are slow. - **x64 disabled.** This is harmless for the default `precision="float32"`. If you want `precision="float64"`, enable JAX 64-bit mode **before** you build the simulation: ```python import jax jax.config.update("jax_enable_x64", True) ``` ## Development install ```bash git clone https://github.com/bk-squared/rfx.git cd rfx pip install -e ".[dev]" python examples/quickstart/hello_world.py ``` `hello_world.py` runs a 20 mm PEC box with one source and one probe. It finishes in seconds. The scripts in `examples/` come only with a source checkout; the PyPI wheel does not include them. To contribute, see [Contributing](https://remilab.ai/rfx/project/contributing/). ## Hardware - Memory: 4 GB is enough for the examples. Use 16 GB or more for large 3-D models. On a GPU, device memory is usually the limit, and gradients need much more of it than a plain forward run; see [Memory Reduction](https://remilab.ai/rfx/run/memory-and-speed/). - GPU: an NVIDIA card with CUDA 12 or newer. Absorbing boundaries cost more per cell than PEC, so budget for them in open-boundary work. ## Next Continue with the [Quick Start](https://remilab.ai/rfx/start/first-simulation/). --- # rfx terms and their field equivalents Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/start/terms/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/start/terms.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Upgrading to 2.0 Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/start/upgrading/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/start/upgrading.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Support Boundaries Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/support/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/support/index.mdx This page says what rfx supports today, feature by feature and port family by port family, and which combinations are refused. The maintained contracts, with their full evidence, are the [support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/support_matrix.md) and the [S-parameter support matrix](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/sparameter_support_matrix.md) on GitHub. Defects you can hit inside these limits are listed in [known limitations](https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/guides/known_limitations.md). ## Status terms | Status | Meaning | |---|---| | **supported** | Documented for routine use within the stated limits. | | **limited** | Documented only for the geometry, mode, mesh or observable stated. Outside that, you have to validate it yourself. | | **experimental** | Runs and has regression tests, but there is not enough RF evidence to rely on the numbers. | | **not documented** | Code exists, but there is no public workflow. | | **unsupported** | Refused with an error. | A clean preflight or a completed run does not move a feature up this scale. ## Features | Feature | Status | Limit | |---|---|---| | Uniform Cartesian Yee grid | **supported** | The baseline. Each source and observable still has its own limits. | | `boundary="cpml"` / `"pec"` | **supported** | | | `boundary="upml"` | **supported** | Reflects about 25 dB more than CPML at the same layer count. Prefer CPML where the reflection floor matters. | | `precision="float32"` | **supported** | The default. | | `precision="float64"` / `"mixed"` | **limited** | Uniform single-device solver only. `"mixed"` raises the CPML reflection floor; not for S-parameters. | | Isotropic, conductive and Debye/Lorentz materials | **supported** | Dispersive models within the validated subsets. `sigma >= 1e6` S/m is lossless PEC. | | Metal sheets, `add_thin_conductor` | **supported** | Lossless PEC sheet on one grid plane. | | Lossy sheets, `surface_impedance_f0=` | **limited** | Band-centre surface resistance on its documented uniform and graded lanes. Not a general dispersive conductor model. | | Probes and flux monitors | **supported** | On a uniform grid. See the graded-mesh table below. | | Harminv resonances (`find_resonances`) | **supported** | Needs a rung-down record and some loss. | | Far field / RCS (NTFF) | **limited** | Benchmarked cases in [Far-Field and RCS](https://remilab.ai/rfx/measure/far-field-and-rcs/). | | Plane wave (`add_tfsf_source`) | **limited** | x-directed, 3-D or 2-D (TMz/TEz), CPML. | | Graded meshes (`dx/dy/dz_profile`) | **limited by feature** | See [Graded meshes](#graded-meshes). | | ADI solver (`solver="adi"`) | **experimental** | 3-D PEC-cavity frequency within 2 % at `adi_cfl_factor=2`. Use Yee for results you report. | | Subgridding (`add_refinement`) | **not documented** | `run()` on a uniform mesh only. | | Multi-device runs (`devices=`) | **not documented** | Speed, not a separate accuracy claim. | | Floquet / Bloch ports | **experimental** | No high-level S-parameter API. | ## Port families and S-parameters | Port | Calculator | Status | Limit | |---|---|---|---| | lumped `add_port(...)` | `run(compute_s_params=True)` | **limited** | One-cell feed model. About 60 cells per wavelength for one-port S11. | | wire `add_port(..., extent=...)` | `run(compute_s_params=True)` | **limited** | Magnitude evidence is stronger than absolute calibration. Graded-mesh use is experimental. | | lumped / wire | `forward(port_s11_freqs=...)` | **limited** | S11 vectors on the uniform single-device path. | | `add_msl_port(...)` | `compute_msl_s_matrix()` | **limited** | Quasi-TEM (`mode="laplace"`), directions ±x and ±y. At least 4 cells through the substrate (preflight warns below that). Graded mesh experimental; `mode="eigenmode"` outside scope. | | `add_waveguide_port(...)` | `compute_waveguide_s_matrix()` | **limited** | Uniform single-mode magnitude has broad evidence; phase, junctions and multimode less. S is a power-wave S only when all ports share one cross-section. Gradients on `normalize=False` and `"flux"`. Graded mesh experimental. | | `add_waveguide_port(...)` | `run(...)` → `waveguide_sparams` | **limited** | Per-port diagnostic, not the full matrix. | | `add_coaxial_port(...)` | `compute_coaxial_line_reflection(...)` | **limited** | One `face="top"` port, self-contained line, float32 uniform grid. At least 4 cells across the annulus. The line absorbs on all three axes. | | `add_coaxial_port(...)` | `compute_coaxial_two_port(...)` | **limited** | One SMA-class through-line family, symmetric discontinuities only. | | lumped/wire + microstrip | `compute_mixed_s_matrix()` | **experimental** | Magnitude not validated; the default passivity projection alters the diagonal. | | coax + microstrip | `compute_coax_msl_transition()` | **experimental** | Diagnostic only; the matrix is not passive and is refused by default. | | `add_floquet_port(...)` | none | **experimental** | Diagnostic helpers only. | | sources, TFSF, probes, DFT planes, flux monitors | none | **not a port** | No reference impedance, so no S-parameters. | Each family has one calculator. `run(compute_s_params=True)` handles lumped and wire ports only. `sim.compute_s_matrix()` picks the right calculator for you, except for a lumped/wire-only model, which it refuses (call `run(compute_s_params=True)`); see [Sources & Ports](https://remilab.ai/rfx/api/sources-ports/#s-parameter-calculators). ## Graded meshes Support on a graded mesh is decided per combination. A graded run completing says nothing about whether its S-parameters, flux or far field are accurate. | Combination | Status | Limit | |---|---|---| | Multi-band graded profile, ratio ≤ 1.4 on z (1.3 in-plane) | **limited** | Solver-level evidence (energy, transition reflection, order, gradients). Keep grading out of the absorber. | | NTFF + graded z | **limited** | Short-dipole directivity within about 0.05 dB of theory; other cases need their own validation. | | Lumped R/L/C + graded mesh | **limited** | Elements take part in the update; S-parameters and component-value gradients not documented. | | DFT plane or flux monitor + graded z | **experimental** | Runs; no general accuracy statement. | | Plane wave + graded mesh | **experimental** | Normal incidence along ±x only, with x uniform. Preflight warns when the propagation axis is graded. | | Waveguide S-matrix + graded mesh | **experimental** | Single mode, `normalize=True` or `"flux"`. One passed magnitude comparison (graded y, WR-90). No dz-graded accuracy evidence. | | Microstrip S-matrix + graded mesh | **experimental** | `mode="laplace"`/`"uniform"`; internal checks only. | | Wire-port S-parameters + graded mesh | **experimental** | | | Lumped-port (one-cell) S-parameters + graded mesh | **unsupported** | | | Coaxial port + graded mesh | **unsupported** | | | UPML + graded mesh | **unsupported** | | | Periodic / Floquet port + graded mesh | **unsupported** | | ## Combinations that raise rfx refuses these before the first time step instead of returning a number: - one-cell lumped-port S-parameters, coaxial ports, `boundary="upml"`, or a Floquet port on a graded mesh; - `mode="eigenmode"` microstrip ports on a graded mesh; - a plane wave on a graded mesh at oblique incidence or along ±z; - a calculator called with the wrong port family, for example `compute_msl_s_matrix()` without microstrip ports; `run(compute_s_params=True)` computes lumped and wire ports only, and a coaxial port is not handled by `run()` at all; - `compute_coaxial_line_reflection(...)` with anything else registered, a graded mesh, non-float32 precision, or a boundary other than CPML on all six faces; - `compute_coax_msl_transition()` with a non-passive result, unless you pass `strict_passivity=False`; - `compute_waveguide_s_matrix(normalize=True)` with a traced `eps_override`/`sigma_override`; - a waveguide reference-plane shift that crosses cells of more than one size on a graded mesh; - `precision="float64"` or `"mixed"` on a graded mesh, with `devices=`, or with subgridding; - `stencil_order=4` with CPML/UPML, a graded mesh, ADI, dispersion or the other features it does not support; - the ADI solver with interior metal (sheets, wires or volumes); - any `run()` argument the selected solver path does not implement, for example snapshots on a graded mesh or `until_decay` with ADI; - `add_refinement` combined with `forward()`, `optimize()` or `topology_optimize()`; - `devices=` with `exchange_interval` other than 1; - `run(ringdown=...)` with anything but wire ports, or with `until_decay` or `devices=`. Some combinations off the main path still ignore a setting without raising, for example a Kerr material on a non-uniform `run()`, or `mu_r` and Kerr on the ADI solver. These are tracked as open issues on [GitHub](https://github.com/bk-squared/rfx/issues). If you need a feature on a path other than the uniform Yee solver, check there first. Using a probe, DFT plane or flux monitor as if it were an impedance-defined port is unsupported for the same reason: it has no reference impedance or reference plane. ## Reporting a result Exported files (Touchstone, HDF5, CSV, JSON, plots) record what a run produced. They do not change its status. When you report a number, state the API and port family, the mesh (uniform or graded, cell size), the frequency range, the normalization, the rfx version and the comparison you made. Outside a listed range, run a [convergence study](https://remilab.ai/rfx/evidence/mesh-convergence/) and compare against an analytic or independent reference first. --- # Known limitations Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/support/known-limitations/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/support/known-limitations.mdx Being written for 2.0 (tracker 1171). ## What it is for ## How to use it ## What is checked ## What is refused, and the way through ## Accuracy ## See also --- # Cross-Solver Validation Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/validation/cross-solver/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/validation/cross-solver.mdx This page is about running a comparison yourself. The cases and what they show are on [Benchmarks](https://remilab.ai/rfx/guide/benchmarks/). ## Run a comparison The comparisons are pytest tests under `tests/crossval/` in a source checkout; they are not installed with the wheel. Each one compares rfx with a reference record frozen in the repository, so you do not need the other solver. ```bash # Fast checks: build the board, check what it realizes, read the record. pytest tests/crossval/rt5880_patch # The full comparison. The long ladders are marked gpu and slow, which the # default test selection skips; a GPU is recommended. pytest tests/crossval/rt5880_patch -m gpu python -m pytest -o addopts="" tests/crossval/msl_notch_filter -q ``` ## Read the result A pass means every judged quantity was inside its bar on the rungs the test names. The test output also prints the quantities it reports but does not judge; read them before citing the case. A skip usually means the test needs a GPU or an optional dependency, which is not a pass. ## Cite the result A comparison holds for the geometry, mesh, band, source, port, materials, boundaries and observable of that test. When you cite one, give the test path, the commit, those settings, the reference and the thresholds it used. Do not carry the result over to a different port, material model, boundary or mesh without a comparison for that setup. --- # Validation Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/validation/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/validation/index.mdx This section collects the pages that tell you how far to trust an rfx result. Start with the first one. | Page | Read it when you want to know | |---|---| | [Cross-Validation and Accuracy](https://remilab.ai/rfx/guide/validation/) | how rfx is checked, and what you must check yourself before reporting a number | | [Recommended Configuration](https://remilab.ai/rfx/validation/recommended-configuration/) | which setup to start from, and which calculator fits your port | | [Support Boundaries](https://remilab.ai/rfx/support/) | whether a feature or combination is supported, limited or refused | | [Benchmarks](https://remilab.ai/rfx/guide/benchmarks/) | which cases compare rfx with another solver, and what they show | | [Cross-Solver Validation](https://remilab.ai/rfx/validation/cross-solver/) | how to run a comparison script and read its result | A plot, a Touchstone file or a report records what a run produced. It becomes evidence only next to a closed-form result, an independent solver, a measurement or a convergence study for the same setup. --- # Recommended Configuration Channel: development; package: 2.0.0rc1; source: 2c32b7cf79b796fae825bfa08fc73d9914e61723 Page: https://remilab.ai/rfx/validation/recommended-configuration/ Source: https://github.com/bk-squared/rfx/blob/2c32b7cf79b796fae825bfa08fc73d9914e61723/docs/public/validation/recommended-configuration.mdx Use this page to choose a starting setup and the right calculator for your port. Each calculator has its own limits, listed here. What you must check before reporting any result is on [Cross-Validation and Accuracy](https://remilab.ai/rfx/guide/validation/). ## Start here For ordinary RF work, start from the **uniform Cartesian Yee** grid: - `boundary="cpml"` for open problems and `"pec"` for closed ones; - point sources, and the port family that matches your structure; - probes, flux monitors, Harminv resonances, the port calculators below and the documented far-field workflows. This is the configuration with the broadest evidence. A graded mesh, the ADI solver and multi-device runs each carry narrower limits, listed below and in [Support Boundaries](https://remilab.ai/rfx/support/). ## Pick the calculator for your port A port calculator only works for its own port family. Do not substitute one for another. | Port | Calculator | Start from | Limits | |---|---|---|---| | Lumped or wire `add_port(...)` | `run(compute_s_params=True)`, or `forward(port_s11_freqs=...)` for a differentiable `S11` | about 60 cells per wavelength at the highest frequency | A discrete feed across one gap, not a modal line port. V is read at the port node and I half a cell toward the line, so a two-port `S21` phase is referenced to the port separation less half a cell. Lumped and wire ports return the same `S11`. | | Microstrip `add_msl_port(...)` | `compute_msl_s_matrix(...)` | at least 4 cells through the substrate (preflight warns below that) | Returns the raw extracted `S`; read `sigma_max_excess` for any passivity breach, or pass `enforce_passivity=True` to project it. Check `probe_clearance` for the probe layout and `settling_db` for the record length. | | Rectangular waveguide `add_waveguide_port(...)` | `compute_waveguide_s_matrix(...)` | the documented rectangular-guide modes and bands | Single-mode magnitudes have the broadest evidence; phase, junctions, multimode and graded meshes have less. Only `normalize=False` and `normalize="flux"` carry a gradient. | | Coaxial `add_coaxial_port(...)` | `compute_coaxial_line_reflection(...)` for one port, `compute_coaxial_two_port(...)` for a through line | at least 4 cells across the annulus | Exactly one coaxial port with `face="top"`, `precision="float32"`, a uniform 3-D grid and CPML on all six faces. The calculator builds its own line, source and probes; it is not a general coaxial-network solver. | Field sources, plane waves, probes, DFT planes and flux monitors have no port impedance or reference plane. They give fields and power, not S-parameters. ## End a line with its port A transmission line should end at a port that absorbs it, not in the CPML. Name the conductor a port ends with `terminates=` on `add_port`, `add_msl_port` or `add_coaxial_port`; a microstrip port ends the strip under it by default. The CPML terminates a guided line much less well than a plane wave. Preflight names any conductor that runs into the absorber unterminated. ## Non-default configurations | Configuration | What to know | |---|---| | Graded (non-uniform) mesh | Check the limit for each source, port and observable in [Non-Uniform Mesh](https://remilab.ai/rfx/model/domain-mesh/). A plane wave along a graded axis leaks into the scattered-field region (preflight warns); normal incidence along x needs a uniform x axis. `add_refinement` is refused on a graded mesh. | | Local refinement, `add_refinement` | Takes effect only through `run()` on a uniform mesh. `forward()`, `optimize()`, `topology_optimize()` and the S-parameter calculators refuse a refined model. | | Multi-device, `run(devices=[...])` | Uses the shard_map runner with `exchange_interval=1`. Lumped RLC elements, MSL ports, refinements, Kerr materials and DFT planes are refused. | | Differentiable design | The objective is a proxy. Check the final design with a converged forward run of the RF quantity you report. | When a run argument is not implemented on the path your model takes (for example `until_decay`, snapshots or conformal walls on some paths), `run()` raises before the first step. Change the argument or the configuration; the message names both. ## Limits - **A completed run is not a validated one.** No warning does not mean converged, passive or correctly referenced. Work through the checklist on [Cross-Validation and Accuracy](https://remilab.ai/rfx/guide/validation/). - **Examples and tests cover their own setup.** A committed test pins behaviour at its own mesh, band and configuration. - **Coaxial results computed before the lanes absorbed on all three axes** (`cpml_axes="z"`, now refused) came from a line in a closed metal can and do not reproduce.