An FDTD answer depends on the cell size. A convergence study shows how much: you solve the same problem on several meshes and check that the number you care about stops moving. Do one before you quote a number from a new geometry.
This tutorial runs a three-mesh study of a resonance in a loaded cavity. It takes about ten seconds on a laptop CPU.
1. Choose one number
A study tracks one scalar. Pick the one your claim is about:
| Claim | Metric |
|---|---|
| resonance | a Harminv frequency in a fixed band |
| S-parameter | abs(S[receiver, driven, k]) at one stated frequency |
| field amplitude | a peak or RMS probe value |
| far field | directivity, or the pattern at one angle |
Different quantities converge at different rates. If you need two, run two studies.
2. Write a factory that builds the same problem at any cell size
convergence_study calls your factory with each cell size. The factory must
build the same physical problem every time, so choose cell sizes that put
every face of your geometry on a grid plane. Otherwise the geometry itself
changes from mesh to mesh as faces snap to different planes.
Here the cavity is 80 × 64 × 40 mm with a 16 × 16 × 8 mm dielectric insert. Every dimension and position is a multiple of 8 mm, so cells of 2.0, 1.6 and 1.0 mm all land on them exactly.
from rfx import Box, GaussianPulse, Simulation
def make_case(dx: float) -> Simulation: sim = Simulation(freq_max=4e9, domain=(0.080, 0.064, 0.040), boundary="pec", dx=dx) sim.add_material("insert", eps_r=4.0, sigma=0.01) sim.add(Box((0.032, 0.024, 0.016), (0.048, 0.040, 0.024)), material="insert") sim.add_source((0.024, 0.032, 0.008), "ez", waveform=GaussianPulse(f0=2.4e9, bandwidth=0.8), amplitude_kind="current") sim.add_probe((0.056, 0.032, 0.008), "ez") return simFor geometry that can’t be aligned at every size, run
make_case(dx).fidelity_report() at each size to see where the faces landed,
and include that snapping in what you report.
3. Run the study
from rfx import convergence_study
def first_resonance(result) -> float: modes = result.find_resonances(freq_range=(2e9, 4e9)) if not modes: raise RuntimeError("no mode in the band: lengthen the run or widen freq_range") return float(min(m.freq for m in modes)) # the lowest mode, at every mesh
study = convergence_study( sim_factory=make_case, dx_values=[2.0e-3, 1.6e-3, 1.0e-3], # coarse to fine metric_fn=first_resonance, run_kwargs={"num_periods": 40}, # the same record at every mesh)study.summary()One CPU run printed the following. The last digits can differ slightly between versions and hardware.
Mesh Convergence Summary================================================== dx (mm) Metric Rel. Error-------------------------------------------------- 2.0000 2.92606e+09 0.0998% 1.6000 2.9269e+09 0.0708% 1.0000 2.92797e+09 0.0344%--------------------------------------------------Extrapolated value : 2.92898e+09Convergence order : 1.54study.metrics, study.extrapolated, study.order and study.errors hold
the same numbers; study.results keeps each run’s Result; study.plot()
draws them.
The metric function picks the lowest mode in a fixed band, so the study follows the same mode at every mesh. It raises when no mode is found instead of indexing an empty list.
4. Check the record length separately
A study that changes dx says nothing about whether the runs were long
enough. Rerun the finest mesh with a longer record and compare:
longer = make_case(1.0e-3).run(num_periods=80)f_long = first_resonance(longer)f_study = float(study.metrics[-1])print(f"40 periods: {f_study / 1e9:.5f} GHz, 80 periods: {f_long / 1e9:.5f} GHz")Here the two agree to far better than the mesh-to-mesh change, so the record length is not what limits the answer. If they differed, you would need a longer record for every mesh in the study.
5. Read the result
- The change between the two finest meshes is the most direct measure of the remaining mesh error. Here it is about 0.04 %.
- The extrapolated value estimates the zero-cell-size answer, assuming
the error follows a power law in
dx.errorsare relative to it. - The observed order should be plausible. Standard Yee is second order; staircased dielectric edges and conductors often pull it lower, as here (1.54). An order near zero, a negative one, or metrics that don’t move monotonically mean the study is not in the converging range. Refine further or look for a setup problem before trusting the extrapolation.
When you report the result, give the cell sizes, the metric at each, the extrapolated value and order, and the script that produced them.
A study covers the metric it tracked, on the geometry and settings it used. It is not evidence for other outputs of the same model. Accuracy against the real device also needs a comparison with a reference: another solver, an analytic result or a measurement. See Cross-Validation and Accuracy and the Benchmark Table.
Other kinds of refinement
- Graded meshes.
convergence_studypasses one number to your factory, so it can be a scale factor instead of a cell size: builddx,dz_profileand so on from it. Refine the fine and coarse regions together, and keep neighbouring cells within a ratio of about 1.3. See Non-Uniform Mesh. - Ports. Lumped and wire port S11 needs about 60 cells per wavelength at the top frequency. Microstrip is usually set by the substrate thickness. Coax needs at least 4 cells across the annulus. See Probes and S-Parameters.
- Probe ladders are placed in cells. A coaxial port’s probes are spaced in cells, so refining the mesh shortens the physical span they cover. Give the probe array enough span at the finest mesh.
- Domain and absorber. A fine mesh does not fix an absorber that is too
close. Vary the air margin and
cpml_layersas a separate check.
Limits
- Metrics jump around between meshes → the geometry probably snaps
differently at each size. Align faces to all cell sizes, or check
fidelity_report()at each one. - A different mode is picked at each mesh → select by a fixed band and
rule (as
first_resonancedoes), or by field pattern. - Finest mesh is too expensive → use a graded mesh to refine only where fields vary quickly, and refine it as a whole.
- The answer moves with record length → lengthen every run in the study, not only the finest.
