Parametric Sweeps
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.
1. Define the parameter grid
Section titled “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.
from rfx.batch import ParameterSweep
sweep = ParameterSweep( patch_width=[0.028, 0.030, 0.032], eps_r=[3.8, 4.2],)
print(sweep.keys) # ['patch_width', 'eps_r']print(sweep.total) # 6
for params in sweep.combinations(): print(params)2. Build a simulation factory
Section titled “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.
from rfx import Simulation, Box, GaussianPulse
def make_patch_sim(patch_width: float, eps_r: float) -> Simulation: sim = Simulation(freq_max=4e9, domain=(0.08, 0.06, 0.025), boundary="cpml") sim.add_material("substrate", eps_r=eps_r) sim.add(Box((0.0, 0.0, 0.0), (0.08, 0.06, 0.0016)), material="substrate") sim.add(Box((0.024, 0.015, 0.0016), (0.052, 0.015 + patch_width, 0.0016)), material="pec") sim.add_source((0.029, 0.030, 0.0008), "ez", waveform=GaussianPulse(2.4e9, 0.8)) sim.add_probe((0.029, 0.030, 0.0008), "ez") return sim3. Run a sequential batch
Section titled “3. Run a sequential batch”run_batch(...) calls sim_factory(**params) for each parameter combination and
returns a list of (params, result) tuples.
from rfx.batch import run_batch
results = run_batch( make_patch_sim, sweep, run_kwargs={"n_steps": 500}, # use a short smoke first)
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
Section titled “4. Aggregate metrics into a dataset”SimulationDataset.from_results(...) converts batch outputs into rectangular
arrays. The output_fn should return a one-dimensional numeric feature vector.
import numpy as npfrom 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=["patch_width", "eps_r"], output_fn=output_fn,)
X, y = dataset.to_numpy()dataset.to_csv("sweep_metrics.csv")5. S-parameter and Touchstone sweeps
Section titled “5. S-parameter and Touchstone sweeps”If the sweep uses a documented S-parameter calculator, keep the port family and support envelope with the output files.
from pathlib import Pathfrom 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
Section titled “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.
import jaximport 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
Section titled “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
import jsonfrom pathlib import Pathimport numpy as np
from rfx.batch import ( ParameterSweep, run_batch_with_manifest, summarize_batch_manifest,)
sweep = ParameterSweep(amplitude=[0.5, 1.0, 2.0])
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_simulation, sweep, "runs/amplitude-sweep", run_kwargs={"n_steps": 200}, metric_fn=metric_fn, artifact_fn=artifact_fn, resume=True,)
summary = summarize_batch_manifest("runs/amplitude-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
Section titled “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?