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
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( 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.
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 sim3. 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_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.
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=["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.
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
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
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 ( 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?
