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