REMI Lab
OPEN KNOWLEDGE / TOOLS

Migration Guide

이 페이지의 내용

This page helps you move a model from Meep or openEMS to rfx. All three are Yee-cell FDTD solvers, so the physics is familiar; the API is different. It maps the concepts, translates three typical scripts and lists the differences that most often catch newcomers.

Before you port a production model, check that its mesh, boundaries, ports and observables are inside the Recommended Configuration and Support Boundaries.

Concept mapping

ConceptMeepopenEMSrfx
Grid setupSimulation(resolution=N)InitCSX() + InitFDTD()Simulation(freq_max=..., domain=...) or Simulation.auto(...)
Cell sizeresolution (cells per unit length)SetDeltaUnit(1e-3) and mesh linesdx= in metres; derived from freq_max if omitted
SourceSource, EigenModeSourceAddExcitationadd_source() (no impedance), add_port() and the other port types
S-parametersadd_flux() + post-processingCalcPortone calculator per port family: lumped/wire run(compute_s_params=True), microstrip compute_msl_s_matrix(), waveguide compute_waveguide_s_matrix(), coax compute_coaxial_line_reflection() / compute_coaxial_two_port()
Resonancesharminv(...)manual FFTresult.find_resonances()
Auto-stopstop_when_fields_decayedEndCriteriarun(until_decay=1e-3) for open problems; a fixed n_steps for a closed PEC cavity
MaterialsMedium(epsilon=...)AddMaterialsim.add_material(...), or a library name such as "fr4"
Metalperfect_electric_conductorAddMetalsim.add(shape, material="pec") for a volume; sim.add_thin_conductor(...) or a zero-thickness Box for a sheet
AbsorberPML(thickness)AddPMLboundary="cpml", cpml_layers=
Dispersive mediaLorentzianSusceptibilityAddLorentzMaterialDebyePole, LorentzPole, drude_pole()
Gradientsadjoint solver for design-region objectivesnot nativejax.grad through the solver
Inverse designmeep.adjoint.OptimizationProblemnot nativerfx.optimize(sim, region, objective)
Non-uniform meshnot nativeSmoothMeshLinesdz_profile= (and dx_profile/dy_profile) with the limits in Non-Uniform Mesh

An openEMS AddMetal box with one zero-length side is a sheet, and a solid box is a volume. rfx follows the same split: the declaration decides it. See Core Concepts.

Translating scripts

Meep: cavity resonance

# Meep (for comparison only)
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(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",
amplitude_kind="current")
sim.add_probe(position=(0.06, 0.06, 0.02), component="ez")
result = sim.run(n_steps=5000)
modes = result.find_resonances() # list of modes with .freq, .Q, ...
print([f"{m.freq / 1e9:.3f} GHz" for m in modes[:3]])

Coordinates are in metres and freq_max in hertz; there are no normalized units. With no dx, rfx picks a cell size from freq_max, and with no waveform the source is a GaussianPulse centred at freq_max / 2.

A closed PEC box has no loss, so its field never decays and until_decay is the wrong stop. Choose a fixed record long enough for the frequency resolution you need, repeat with a longer one, and keep only the modes whose frequency does not move. The Q of a lossless cavity is infinite; add material or conductor loss before you read a Q.

openEMS: waveguide S-parameters

% openEMS (MATLAB, for comparison only)
CSX = InitCSX();
FDTD = InitFDTD('EndCriteria', 1e-5);
[CSX, port{1}] = AddRectWaveGuidePort(CSX, 0, 1, ...);
RunOpenEMS(Sim_Path, Sim_CSX);
port = calcPort(port, Sim_Path, freq);
s11 = port{1}.uf.ref ./ port{1}.uf.inc;
# rfx equivalent
import jax.numpy as jnp
from rfx import Simulation
# WR-90: the transverse domain is the guide cross-section. Its faces are the
# guide walls, declared PEC; the absorber sits on the port axis only.
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={"x": "cpml", "y": "pec", "z": "pec"},
cpml_layers=16)
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")
result = sim.compute_waveguide_s_matrix(num_periods=45)
s11 = result.s_params[0, 0, :] # shape (n_ports, n_ports, n_freqs)

The mesh here is coarse so the example runs quickly. The calculator’s warnings name the absorber depth and record length it needs; 16 layers and 45 periods satisfy them here. normalize=False (the default) is the modal V/I decomposition; normalize="flux" gives a power-normalized transmission magnitude. There is no single CalcPort equivalent: run(compute_s_params=True) is only for lumped and wire add_port(...) ports. See Probes and S-Parameters.

Meep adjoint: inverse design

# Meep (for comparison only)
opt = mpa.OptimizationProblem(...)
opt.update_design([design_params])
f, g = opt() # forward + adjoint
# rfx: optimize() runs jax.grad through the solver; no adjoint code to 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=4e9, 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.0025), "ez", # in the air below the slab
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)
# This objective needs the port DFT at the target frequency.
opt = optimize(sim, region, objective, n_iters=3, lr=0.01, n_steps=400,
port_s11_freqs=jnp.asarray([3e9]), verbose=False)
print(opt.loss_history) # opt.eps_design holds the optimized permittivity

Three iterations on a coarse mesh keep the example short, and preflight warns that 2.5 mm cells are at the edge of phase accuracy in the slab. A real design needs a finer mesh, many more iterations and a converged forward run to check the final result. See Inverse Design.

What is different in rfx

  • Everything runs in Python through JAX. There is no solver binary and no file exchange. The same script runs on CPU, or on an NVIDIA GPU with a CUDA-enabled JAX build.
  • Gradients come from the solver itself. Supported workflows let jax.grad differentiate through the time stepping, so an ordinary Python loss function can drive an optimizer. Check the final design’s RF result with a converged run; the loss is only a proxy.
  • One Simulation object holds the model. Add materials, shapes, sources, ports and observables to it, then call run() or a port calculator. run() computes lumped and wire port S-parameters automatically; other port families use their own calculators.
  • Preflight checks the setup before you spend time on a solve. Read sim.preflight(). run() repeats it and prints the warnings.
  • Snapshots. run(snapshot=SnapshotSpec(...)) records field frames on the uniform grid, with a fixed n_steps or with until_decay. The frames are interval steps apart (default 10); read their times from result.snapshot_axes. A graded mesh, multi-device runs and the ADI solver do not record snapshots and raise if you ask for them.
  • A built-in material library. fr4, rogers4003c, rogers4350b, rt_duroid_5880, alumina, ptfe, silicon, copper, aluminum, pec, air, vacuum and water_20c work by name. Their values are nominal: use your laminate’s data for a real design.
  • Automatic defaults. Without dx, the cell size comes from freq_max. The CPML is 16 layers unless you set cpml_layers. Simulation.auto(freq_range=(f_min, f_max)) proposes a domain and mesh from a band.

Common gotchas

GotchaWhat to do
Positions are in metres, not millimetres or cellsWrite 0.012, not 12
freq_max is in hertz, not normalized frequencyfreq_max=5e9 for 5 GHz
The absorber cells are added outside domain=domain= is the region you model. Geometry drawn to a face continues into the absorber, as a board running off the edge should
run() returns a Result, not filesRead result.time_series, result.s_params, result.find_resonances()
There is no mesh fileThe solved grid is result.grid; rfx.plan_simulation_mesh(sim) inspects the mesh before a run
An argument a path does not support raisesrun() refuses, for example, until_decay on a closed graded mesh before the first step; the message says what to change

Next

Quick Start runs a first simulation, and Core Concepts explains the model behind it.

Back to top ↑