Skip to content

Migration Guide

This guide helps users coming from Meep or OpenEMS translate their workflows into rfx. rfx follows the same Yee-cell FDTD physics, so the core concepts are familiar — the API is different. Before translating a production model, check the Recommended Configuration and Support Boundaries for the exact mesh, boundary, source or port, and observable restrictions.


ConceptMeepOpenEMSrfx
Grid setupSimulation(resolution=N)InitCSX() + InitFDTD()Simulation(freq_max=...) or Simulation.auto(...)
Cell sizeresolution (cells/unit)SetDeltaUnit(1e-3)dx= in meters (auto-calculated from freq_max)
SourceEigenModeSource, SourceAddExcitationadd_port(), add_source()
S-parametersadd_flux() + post-processingCalcPortport-family-specific: lumped/wire run(compute_s_params=True), MSL compute_msl_s_matrix(), waveguide compute_waveguide_s_matrix()
Resonance findingharminv(...)Manual FFTresult.find_resonances()
Auto-stopstop_when_fields_decayedEndCriteriarun(until_decay=1e-3) on the uniform and non-uniform CPML/UPML runners; use fixed n_steps for a closed PEC cavity
MaterialsMedium(epsilon=...)AddMaterialsim.add(shape, material="fr4") or sim.add_material(...)
PECPerfectElectricalConductorAddMetalmaterial="pec"
PML / ABCPML(thickness)AddPMLboundary="cpml"
Dispersive mediaLorentzianSusceptibilityAddLorentzMaterialDebyePole, LorentzPole, drude_pole()
Differentiableadjoint-solver workflows for selected design-region objectivesnot nativejax.grad(loss_fn)(params) on supported JAX-traced workflows
Inverse designmeep.adjoint / OptimizationProblemnot nativerfx.optimize(sim, design_region, objective)
Non-uniform meshNot nativeSmoothMeshLineslimited graded-z dz_profile / auto_configure() workflows

# Meep
import meep as mp
sim = mp.Simulation(
cell_size=mp.Vector3(0.1, 0.1, 0.05),
resolution=50,
boundary_layers=[],
)
sim.sources = [mp.Source(
mp.GaussianSource(frequency=2.0, fwidth=0.5),
component=mp.Ez,
center=mp.Vector3(0.03, 0.03, 0.02),
)]
sim.run(mp.at_beginning(mp.output_epsilon),
until_after_sources=mp.stop_when_fields_decayed(50, mp.Ez, mp.Vector3(), 1e-6))
# rfx equivalent
from rfx import Simulation
sim = Simulation(freq_max=5e9, domain=(0.1, 0.1, 0.05), boundary="pec")
sim.add_source(position=(0.03, 0.03, 0.02), component="ez")
sim.add_probe(position=(0.06, 0.06, 0.02), component="ez")
result = sim.run(n_steps=20_000)
modes = result.find_resonances() # list of HarminvMode (fields: freq, decay, Q, ...)

A closed PEC cavity has no boundary loss, so until_decay is not an appropriate stop condition. Its fallback monitor can cross a field null before the modal record is long enough. Choose a fixed window for the required frequency resolution, repeat with a longer window, and retain only modes that remain stable in frequency. The physical Q is infinite in this idealized model, so a finite-window Q is not meaningful. Add realistic material, conductor, or load loss—or use an open boundary where radiation loss is part of the model— before interpreting Q.

% OpenEMS (MATLAB)
CSX = InitCSX();
FDTD = InitFDTD('EndCriteria', 1e-5);
CSX = AddMetal(CSX, 'PEC');
CSX = AddBox(CSX, 'PEC', 10, [0 0 0], [40 20 10]);
[CSX, port{1}] = AddRectWaveGuidePort(CSX, 0, 1, ...);
RunOpenEMS(Sim_Path, Sim_CSX, '--engine=multithreaded');
port = calcPort(port, Sim_Path, freq);
s11 = port{1}.uf.ref ./ port{1}.uf.inc;
# rfx equivalent (waveguide modal-port family)
import jax.numpy as jnp
from rfx import Simulation
# WR-90 rectangular guide. The guide walls are implied by the modal port
# and the transverse (y, z) domain extents -- no explicit PEC fill is needed.
freqs = jnp.linspace(8e9, 11.5e9, 8) # above the ~6.56 GHz TE10 cutoff
sim = Simulation(freq_max=12e9, domain=(0.10, 0.02286, 0.01016),
dx=2e-3, boundary="cpml", cpml_layers=8)
sim.add_waveguide_port(0.024, direction="+x", freqs=freqs, f0=9.75e9, name="in")
sim.add_waveguide_port(0.076, direction="-x", freqs=freqs, f0=9.75e9, name="out")
# Modal V/I decomposition (the default, normalize=False). For dispersion-
# corrected transmission magnitude, pass normalize="flux".
result = sim.compute_waveguide_s_matrix(num_periods=30, normalize=False)
s11 = result.s_params[0, 0, :] # s_params shape: (n_ports, n_ports, n_freqs)

Use the waveguide calculation only for the rectangular-guide modes, frequency ranges, normalization, reference planes, and port placement listed in Support Boundaries and the S-parameter guide. Do not treat run(compute_s_params=True) as a universal OpenEMS CalcPort equivalent; it is the lumped/wire add_port(...) calculator only.

# Meep (uses the meep.adjoint module)
opt = mpa.OptimizationProblem(...)
opt.update_design([design_params])
f, g = opt() # forward + adjoint
# rfx: gradient-based inverse design (optimize() runs jax.grad through the
# differentiable sim.forward() internally -- no adjoint code to hand-write)
import jax.numpy as jnp
from rfx import Simulation, Box, DesignRegion, GaussianPulse
from rfx.optimize import optimize
from rfx.optimize_objectives import minimize_s11_at_freq_wave_decomp
sim = Simulation(freq_max=5e9, domain=(0.05, 0.05, 0.025), dx=2.5e-3, boundary="pec")
sim.add_material("slab_init", eps_r=4.0)
sim.add(Box((0.015, 0.015, 0.005), (0.035, 0.035, 0.020)), material="slab_init")
sim.add_port((0.025, 0.025, 0.0125), "ez",
waveform=GaussianPulse(f0=3e9, bandwidth=0.8))
region = DesignRegion(
corner_lo=(0.015, 0.015, 0.005),
corner_hi=(0.035, 0.035, 0.020),
eps_range=(1.0, 12.0),
)
objective = minimize_s11_at_freq_wave_decomp(target_freq=3e9, port_idx=0)
# port_s11_freqs is required by this objective (it accumulates per-port V/I
# DFTs at those frequencies inside the JIT scan body).
result = optimize(sim, region, objective,
n_iters=50, lr=0.01,
port_s11_freqs=jnp.asarray([3e9]))
# result.eps_design (optimized permittivity), result.loss_history (per-iter)

rfx executes through JAX without a separate solver binary or file exchange with a C++ engine. The base installation uses the standard CPU JAX packages; install a CUDA-enabled JAX build to run supported calculations on an NVIDIA GPU. The standard fixed-step uniform runner uses a JIT-compiled jax.lax.scan for its main time-stepping loop. Decay-controlled and non-uniform runners use different loop structures. The README throughput number is a hardware-specific RTX 4090 measurement, not a portable performance guarantee.

Supported rfx optimization workflows are written so jax.grad can propagate through the implemented discrete time-stepping, sources, probes, and objective post-processing. This enables gradient-based inverse design from ordinary Python loss functions. Validate the final reported RF observable with the applicable convergence study and analytic or independent reference.

rfx uses a single Simulation object that accumulates geometry, materials, sources, and probes. run() computes S-parameters automatically when lumped/wire add_port(...) entries exist (compute_s_params defaults to true for that family). SnapshotSpec records interval snapshots only on the standard fixed-step uniform runner. It is ignored by the until_decay Python loop, and non-uniform, distributed, or subgrid paths can warn that it was dropped; ADI rejects it. Check preflight and confirm result.snapshots before relying on an archive. Waveguide, MSL, and coaxial ports use their documented family-specific calculators; a Floquet port has no high-level S-parameter result. Resonances are extracted on demand from the returned Result with result.find_resonances().

A built-in material library covers common RF substrates and conductors (fr4, rogers4003c, rogers4350b, rt_duroid_5880, alumina, ptfe, silicon, copper, aluminum, pec, air, vacuum, water_20c) by name, so you do not have to look up permittivity tables for standard materials.

When you do not pass dx, rfx derives the cell size from freq_max (and the time step from the CFL limit) at run time, and point sources default to a GaussianPulse centered at freq_max/2. CPML uses a 16-layer pad by default (cpml_layers=16). For most antenna and waveguide problems you only need freq_max and domain; Simulation.auto(freq_range=(f_min, f_max)) additionally proposes a domain and mesh from a target band.

For PCB and patch-style problems, graded z meshing via the dz_profile= constructor argument or auto_configure() can reduce cell count when the thin feature is primarily along z. Before using it, check the permitted source, port, and observable combinations in Support Boundaries, then compare the final observable with a uniform-grid refinement or independent RF reference as applicable. See Non-Uniform Mesh for the exact mesh restrictions.


IssueSolution
Coordinates are in meters, not mm or cell unitsAll positions use SI meters
freq_max is in Hz, not normalized frequencyUse freq_max=5e9 for 5 GHz
PML is outside the domain you specifydomain= is the physical region; CPML pads are added automatically
run() returns a Result objectAccess fields via result.s_params, result.time_series, result.find_resonances()
No mesh file exportrfx solves in-process; the solved Grid is on result.grid, and rfx.plan_simulation_mesh(sim) audits the mesh before a run