# Parametric Sweeps

Channel: release; package: 1.8.0; source: 7d1e97331656dd74ad0f5471653ac0c5effdff70

Page: https://remilab.ai/rfx/versions/v1.8.0/guide/parametric-sweeps/
Source: https://github.com/bk-squared/rfx/blob/7d1e97331656dd74ad0f5471653ac0c5effdff70/docs/public/guide/parametric-sweeps.mdx

Parametric sweeps are the safest way to explore a small RF design space before
committing to a long optimization run. Start with a sequential sweep that you can
inspect, then move to vectorized or accelerator-heavy evaluation only after the
metric is stable.

  A sweep multiplies the assumptions of the underlying simulation. If a single
  run uses a limited or unsupported combination, repeating it in a sweep does
  not make the result more reliable. Check every source, port, mesh, and
  observable against the relevant support matrix.

## 1. Define the parameter grid

`rfx.batch.ParameterSweep` stores a Cartesian product of scalar parameter
values. It is intentionally small and explicit so every case can be inspected.

```python
from rfx.batch import ParameterSweep

sweep = ParameterSweep(
    insert_width=[0.008, 0.012, 0.016],
    eps_r=[3.8, 4.2],
)

print(sweep.keys)   # ['insert_width', 'eps_r']
print(sweep.total)  # 6

for params in sweep.combinations():
    print(params)
```

## 2. Build a simulation factory

Keep the factory pure: all parameters needed to construct the case should come
from the `params` dictionary. Store generated plots, Touchstone files, and HDF5
snapshots outside git unless they are curated docs assets.

```python
from rfx import Simulation, Box, GaussianPulse

def make_inclusion_sim(insert_width: float, eps_r: float) -> Simulation:
    sim = Simulation(
        freq_max=4e9,
        domain=(0.08, 0.06, 0.04),
        boundary="cpml",
        cpml_layers=8,
        dx=2e-3,
    )
    sim.add_material("insert", eps_r=eps_r, sigma=0.01)
    y0 = 0.030 - insert_width / 2
    y1 = 0.030 + insert_width / 2
    sim.add(Box((0.032, y0, 0.018), (0.048, y1, 0.022)), material="insert")
    sim.add_source((0.024, 0.030, 0.020), "ez", waveform=GaussianPulse(2.4e9, 0.8),
                   amplitude_kind="current")
    sim.add_probe((0.056, 0.030, 0.020), "ez")
    return sim
```

## 3. Run a sequential batch

`run_batch(...)` calls `sim_factory(**params)` for each parameter combination and
returns a list of `(params, result)` tuples.

```python
from rfx.batch import run_batch

results = run_batch(
    make_inclusion_sim,
    sweep,
    run_kwargs={"n_steps": 500},  # short trial; increase for the final study
)

for params, result in results:
    print(params, result.time_series.shape)
```

For long runs, include a case identifier in your output path and write a manifest
with the input parameters, command line, git SHA, support status, and metric used
to rank the case. That manifest is what lets reviewers reproduce a plotted sweep.

## 4. Aggregate metrics into a dataset

`SimulationDataset.from_results(...)` converts batch outputs into rectangular
arrays; it is a `rfx.batch` helper rather than part of the documented
star-import API surface. The `output_fn` should return a one-dimensional
numeric feature vector.

```python
import numpy as np
from rfx.batch import SimulationDataset

def output_fn(result):
    # Example diagnostic metric: peak absolute probe signal.
    probe = np.asarray(result.time_series[:, 0])
    return np.array([np.max(np.abs(probe))])

dataset = SimulationDataset.from_results(
    results,
    input_keys=["insert_width", "eps_r"],
    output_fn=output_fn,
)

X, y = dataset.to_numpy()
dataset.to_csv("sweep_metrics.csv")
```

## 5. S-parameter and Touchstone sweeps

If the sweep uses a documented S-parameter calculator, keep the port family and
its applicable configuration limits with the output files.

```python
from pathlib import Path
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:
        continue
    path = out_dir / f"case_{i:03d}.s2p"
    write_touchstone(path, result.s_params, result.freqs, z0=50.0)
```

Touchstone export is an interoperability format, not validation by itself. Link
any S-parameter claims to the current port-family evidence in
`docs/guides/sparameter_support_matrix.md`.

## 6. When to use `jax.vmap`

Use `jax.vmap` when the computation is already a pure array function, such as a
surrogate model, a closed-form RF approximation, or a differentiable objective
that has been written without Python-side side effects. Most full `Simulation`
object construction is easier to inspect and debug with explicit Python loops.

```python
import jax
import jax.numpy as jnp

def quarter_wave_freq(length_m, eps_eff):
    c0 = 299_792_458.0
    return c0 / (4.0 * length_m * jnp.sqrt(eps_eff))

lengths = jnp.array([0.026, 0.028, 0.030])
eps_eff = jnp.array([3.0, 3.2, 3.4])
freqs = jax.vmap(quarter_wave_freq)(lengths, eps_eff)
```

## Manifest-backed sweeps

For validated or long-running sweeps, prefer `run_batch_with_manifest()`.
It writes `manifest.json` under the output directory with:

- schema version
- sweep keys and total case count
- stable case IDs derived from parameter values
- per-case status, timing, metrics, parameter digest, and artifact paths
- artifact file size/hash metadata for resume integrity
- summary counts, last-run status, and lightweight Python/platform metadata
- resume-safe completed-case records

```python
import json
from pathlib import Path
import numpy as np

from rfx.batch import (
    run_batch_with_manifest,
    summarize_batch_manifest,
)

# The factory and the sweep from sections 2 and 1 are reused unchanged.

def metric_fn(result):
    state = result.state
    peak_ez = float(np.max(np.abs(np.asarray(state.ez))))
    return {"peak_ez": peak_ez}

def artifact_fn(case_dir: Path, params, result):
    path = case_dir / "metric.json"
    path.write_text(json.dumps(metric_fn(result), sort_keys=True))
    # Case-local paths are normalized into output-root-relative manifest paths.
    return {"metric": "metric.json"}

records = run_batch_with_manifest(
    make_inclusion_sim,
    sweep,
    "runs/inclusion-sweep",
    run_kwargs={"n_steps": 200},
    metric_fn=metric_fn,
    artifact_fn=artifact_fn,
    resume=True,
)

summary = summarize_batch_manifest("runs/inclusion-sweep/manifest.json")
print(summary["status_counts"])
```

`resume=True` skips only records that are marked `completed`, match the current
case parameters, include at least one metric, and still have all declared
artifact files on disk with matching size/hash metadata. Missing or modified
artifacts, empty metrics, interrupted `running` cases, and previous failures are
not silently treated as valid completed runs.

`run_kwargs` are fingerprinted into each case record. Reusing the same output
directory with a different run budget, such as a changed `n_steps`, forces a
rerun instead of returning outdated cases. If the simulation factory, metric
definition, mesh source, or any external input changes while `run_kwargs` stay
the same, pass a new `run_fingerprint="..."` value to invalidate old cases.

By default the manifest-backed runner is fail-fast. For exploratory grids where
later cases are still useful after one case fails, pass
`continue_on_error=True`; failed cases are recorded with `status="failed"`.

## Checklist before scaling up

- Did a single case run and produce physically plausible fields/probes?
- Is the selected source/port family documented for the claimed metric?
- Are generated files outside git or intentionally curated docs assets?
- Does every case record parameters, git SHA, command, support status, and metric?
- If results are public-facing, do they cite the support matrix rather than only
  saying that tests passed?
