This page walks through one complete simulation: a Gaussian pulse passing through a dielectric slab in open space. Every rfx model follows the same six steps:
- Domain and mesh. Create a
Simulationwith its size, cell size and boundaries. - Materials and geometry. Register materials and place shapes.
- Excitation. Add a source, or a port if you want S-parameters.
- Observables. Add probes, monitors or far-field boxes.
- Preflight. Let rfx check the setup before you spend time on a solve.
- Run and read. Call
run()and read the returnedResult.
The example runs in a few seconds on a laptop CPU.
1. Domain and mesh
import numpy as npfrom rfx import Simulation, Box, GaussianPulse
sim = Simulation( freq_max=5e9, # highest frequency you care about (Hz) domain=(0.06, 0.04, 0.04), # x, y, z size in metres dx=2e-3, # cubic cell size in metres boundary="cpml", # absorbing (open) boundary on every face cpml_layers=8, # absorber thickness in cells)- All lengths are in metres, frequencies in hertz, and conductivity in S/m.
freq_maxdoes two things. It sets the default time step, and it is the frequency preflight uses to judge whether the mesh is fine enough.- The CPML absorber is added outside the
domainyou give, sodomainis the region you model. Structures that reach the domain edge continue into the absorber, which is how an infinite slab or a line leaving the box is modelled. Keep sources and probes a few cells in from the edge.
2. Materials and geometry
sim.add_material("slab", eps_r=3.0, sigma=0.005)sim.add(Box((0.028, 0.0, 0.0), (0.036, 0.04, 0.04)), material="slab")Box(corner_lo, corner_hi) is an axis-aligned block. Here the slab is 8 mm
thick along x and spans the whole cross-section. Library materials such as
"fr4", "rogers4350b" and "pec" need no add_material call. See
Materials and Geometry.
3. Excitation
sim.add_source( (0.014, 0.02, 0.02), "ez", waveform=GaussianPulse(f0=3e9, bandwidth=0.8), amplitude_kind="current",)This is a soft point source that drives Ez in one cell. amplitude_kind="current"
means the waveform amplitude is a current in amperes. Always pass it
explicitly. A point source has no impedance, so it gives fields, resonances and
far fields but not S-parameters. Use a port for those; see
Sources and Ports.
4. Observables
sim.add_probe((0.020, 0.02, 0.02), "ez") # probe 0: between source and slabsim.add_probe((0.046, 0.02, 0.02), "ez") # probe 1: behind the slabA probe records one field component at one point for every time step.
5. Preflight
report = sim.preflight()print(report.format())report.raise_for_failure()Preflight looks for mistakes that give a wrong answer without crashing:
- a mesh too coarse for the material and
freq_max; - structures or probes inside the absorber;
- geometry that does not land where you drew it;
- a lossless resonator whose Q can never converge.
Errors stop the run. Warnings are advisory, but read every one of them.
run() repeats the check and prints any warnings.
Try eps_r=4.0 on the slab. At 2 mm cells that leaves exactly 15 cells per
wavelength inside the dielectric, and preflight will warn about phase accuracy.
6. Run and read the result
result = sim.run(n_steps=1000)
ts = np.asarray(result.time_series) # shape (n_steps, n_probes)print(ts.shape, f"dt = {result.dt:.3e} s")
spectrum = np.abs(np.fft.rfft(ts, axis=0))freqs = np.fft.rfftfreq(ts.shape[0], d=result.dt)in_band = (freqs > 1e9) & (freqs < 5e9)ratio = spectrum[in_band, 1] / spectrum[in_band, 0]print("settling:", result.settling_witness["status"]) # "absent" here: nothing to judgeresult.time_serieshas one column per probe, in registration order.result.settling_dbjudges whether the record was long enough for the spectral results the run computed (S-parameters, DFT planes, flux monitors, far fields). After the source ends, rfx fits the poles still ringing in the record and estimates, at every frequency those results read, how much of the value comes from what rings on past the record’s end.settling_dbis the worst of those shares in dB; at or below −40 dB the record is long enough.result.settling_witness["status"]sayspass,fail,undetermined(the source was still on, or the fit could not be judged) orabsent(nothing the run reads was recorded to judge, as here). Onlypassis a pass.- This run reads only probe time series, so the check is
absentand the FFT above is yours to judge: if the probe signals have not decayed to a small fraction of their peak by the last step, run longer.
The ratio above only compares the spectra at two points. It is not a transmission coefficient, because probe 0 sees the incident and reflected waves together. For reflection and transmission, use two flux monitors and a reference run without the slab. The Probes and S-Parameters guide shows how.
Choosing the run length
run() offers three ways to decide how long to step:
| Argument | Meaning |
|---|---|
n_steps=N | exactly N time steps |
num_periods=P | P periods at freq_max (default 20) |
until_decay=1e-3 | step until the field energy has decayed to that fraction of its peak, up to decay_max_steps |
With absorbing boundaries, until_decay watches the total energy inside the
domain, which is a reliable stop for spectra, flux and S-parameters. A closed
PEC box never loses energy, so there it falls back to a single field sample
(on a graded mesh, a closed-boundary until_decay is refused instead).
That fallback is only reliable for lossy or radiating ring-down, so use a fixed
n_steps in a closed structure. Also use a fixed n_steps when you compare
runs, for example in a convergence study.
Where to go next
- Core Concepts: the mental model behind the six steps, including cells, boundaries, conductors, sources and ports.
- Your First Patch Antenna: a real device on a graded mesh.
- Examples: the ordered learning path of runnable scripts.
