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