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 surface is different.


ConceptMeepOpenEMSrfx
Grid setupSimulation(resolution=N)InitCSX() + InitFDTD()Simulation(freq_max=...) or Simulation.auto(...)
Cell sizeresolution (cells/unit)SetDeltaUnit(1e-3)dx= in metres (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)
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 nativeSmoothMeshLinesdz_profile or auto_configure()

# 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(until_decay=1e-3)
freqs = result.find_resonances()
% 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 port family)
from rfx import Simulation, Box
sim = Simulation(freq_max=15e9, domain=(0.04, 0.02, 0.01), boundary="cpml")
sim.add(Box((0, 0, 0), (0.04, 0.02, 0.01)), material="pec")
sim.add_waveguide_port(0.005, direction="+x", f0=10e9, name="in")
sim.add_waveguide_port(0.035, direction="-x", f0=10e9, name="out")
result = sim.compute_waveguide_s_matrix(num_periods=30, normalize=True)
s11 = result.s_params[0, 0, :]

The waveguide path is recommended only within the documented rectangular-guide gates. 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 (native JAX autodiff)
import jax
def loss(eps_r):
sim = Simulation(freq_max=8e9, domain=(0.04, 0.01, 0.01), boundary="cpml")
sim.add_port(position=(0.008, 0.005, 0.005), component="ez")
result = sim.run(n_steps=150, compute_s_params=True, eps_r_override=eps_r)
return -jnp.abs(result.s_params[0, 0, 50])
grad_fn = jax.grad(loss)
gradient = grad_fn(initial_eps_r)

rfx runs on GPU out of the box via JAX. No separate engine binary, no file-based I/O between the Python front-end and a C++ solver. The full time-stepping loop is JIT-compiled and executes on GPU (~3,000 Mcells/s on RTX 4090).

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, while final RF claims still need the relevant validation path.

rfx uses a single Simulation object that accumulates geometry, materials, sources, and probes. Call run() once — S-parameters, resonances, and field snapshots are computed automatically based on what was requested.

Eleven common RF materials (fr4, rogers4003c, alumina, silicon, copper, pec, etc.) are available by name. No need to look up permittivity tables for standard substrates and conductors.

Simulation(freq_max=N) automatically sets cell size, time step, CPML thickness, and source waveform. For most antenna and waveguide problems you only need to specify freq_max and domain.

For PCB and patch-style problems, the most practical recent rfx workflow is graded z meshing via dz_profile / auto_configure(), rather than assuming one globally fine uniform mesh.


IssueSolution
Coordinates are in metres, not mm or cell unitsAll positions use SI metres
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; use sim.grid to inspect the mesh