A graded mesh uses small cells only where the geometry needs them, such as a
thin substrate, and large cells in the air around it. This page shows how to
build a cell-size profile, pass it to Simulation, and check that your
geometry lands where you drew it.
Start with a uniform grid. Switch to a graded one when a thin feature would make a uniform grid too large, and then compare the final result against a uniform run or another reference.
When a graded mesh pays off
A 1.6 mm substrate at 0.5 mm cells gets only 3 cells, which is too coarse. Shrinking a uniform cell to 0.2 mm resolves it, but it refines all three axes: about 15× more cells. A graded z profile puts 0.27 mm cells in the substrate and 1.5 mm cells in the air, and leaves x and y alone.
| Situation | Mesh |
|---|---|
| substrate much thinner than a wavelength | graded z |
| a stack of several thin layers | graded z, one fine band per layer |
| patch or trace edges whose size matters | in-plane profiles from edge_aware_profiles, required unless snap="declared" |
| bulk 3-D structure, no thin features | uniform |
| far field or RCS of a bulk object | uniform |
Building a z profile
A dz_profile is a 1-D array of cell sizes in metres, from z = 0 upward. It
covers the physical domain; rfx adds the absorber cells itself.
The easiest way to build one is make_band_profile. Give it every interface,
including both domain ends, and a target cell size per segment:
import numpy as npfrom rfx.nonuniform import make_band_profile
h = 1.6e-3 # substrate thicknessbelow = 20e-3 # air below the substrateabove = 30e-3 # air above it
dz_profile = make_band_profile( edges=[0.0, below, below + h, below + h + above], cell_sizes=[1.5e-3, h / 6, 1.5e-3], # 6 cells through the substrate protected=[False, True, False], # keep the substrate uniform max_ratio=1.3,)print(len(dz_profile), dz_profile.min(), dz_profile.max())The result has every interface on a cell boundary (to 1e-12 m), every
neighbouring-cell ratio at most max_ratio, and a sum equal to the domain
height exactly. The ramps between sizes are placed in the unprotected
segments.
Rules of thumb:
- At least 4 cells through a substrate; 6–8 when the field inside it matters.
- Neighbour ratio ≤ 1.4 in z (the validated limit), and ≤ 1.3 in x and y. Preflight warns above that.
- Interfaces on cell boundaries.
make_band_profiledoes this for you. - Uniform cells next to the absorber. Leave room for
cpml_layersfull-size cells between the last ramp and each absorbing face. Here the 20 mm below and 30 mm above the substrate are enough for 8 layers. Preflight warns (nu_grading_reaches_absorber) when a ramp reaches into that runway.
Two alternatives:
make_z_profile(features, domain_z, dx_fine, dx_coarse, grading)puts fine cells at each listed z plane and at both ends, with ramps between.- A hand-built array with
np.concatenate. If you do this,rfx.smooth_grading()can insert ramp cells, but it shifts every interface above them, so check the cumulative edges afterwards. Neither builder needs it.
Using the profile
from rfx import Box, Simulationfrom rfx.mesh_edges import edge_aware_profiles
sub_lo, sub_hi = below, below + hboard = Box((0.01, 0.01, sub_lo), (0.09, 0.07, sub_hi))ground = Box((0.01, 0.01, sub_lo), (0.09, 0.07, sub_lo))# This horizontal sheet needs x/y registration; keep the layered z profile.profiles = edge_aware_profiles( (0.10, 0.08, float(np.sum(dz_profile))), 1e-3, sheets=[ground], solids=[board], axes="xy",)
sim = Simulation( freq_max=4e9, domain=(0.10, 0.08, 0.0), # z = 0: use sum(dz_profile) dx=1e-3, dz_profile=dz_profile, **profiles, boundary="cpml", cpml_layers=8,)
z_edges = np.concatenate(([0.0], np.cumsum(dz_profile)))assert np.isclose(z_edges, sub_lo).any() and np.isclose(z_edges, sub_hi).any()
sim.add(board, material="fr4")sim.add_thin_conductor(ground)
report = sim.preflight()print(report.format())domain[2] = 0 tells rfx to use sum(dz_profile) as the height. A positive
value is kept as given and is not checked against the profile, so pass
float(np.sum(dz_profile)) if you set it yourself.
Keep the interfaces on node planes, as the assert above checks. A sheet
conductor lands on the nearest node plane, so a substrate face between nodes
leaves a gap between the copper and the dielectric. Preflight’s
graded_box_rasterization warning fires when a thin Box gets far fewer
cells than the fine spacing nearby implies; fix the profile rather than the
warning.
Grading x and y
dx_profile and dy_profile work the same way, with one extra rule: both end
cells must equal the scalar dx, so every absorber face sees the same cell.
make_band_profile(..., boundary_cell=dx) does this.
The same uniform-runway rule applies next to each absorbing face. Keep fine bands away from absorbing faces, or close those faces with PEC.
Solving sheets at their drawn size
A PEC sheet’s in-plane edge is solved about 0.35 of a cell beyond its last
grid node, so a patch or trace comes out slightly larger than drawn.
A PEC sheet whose free-edge size is solved more than 1 % off its drawing
is refused by run(), forward() and compute_mixed_s_matrix().
Preflight reports this as a sheet_effective_size error.
Build profiles with edge_aware_profiles to place sheet edges where they solve
at their drawn size. Use axes="xyz" and pass the returned dz_profile too
for vertical sheets; the horizontal ground above only needs x/y registration
and keeps its layered z profile. Alternatively, Simulation(snap="declared")
accepts the difference as a warning and records the choice on
Simulation.realized_geometry(); it changes neither the mesh nor the solved
geometry.
Ends at seams, overlaps and T-joins with other sheet metal, at or past a
domain face, or joined to a realized PEC volume are not judged. A gap of one
cell or more to a block still leaves a free edge. These rules and the tolerance
are implemented in rfx/mesh_edges.py::solved_sheet_span and
rfx/preflight/pec_geometry.py::_warn_sheet_effective_size.
For a patch and board:
from rfx.mesh_edges import edge_aware_profiles
LX, LY, LZ = 0.04, 0.03, 0.01board = Box((0.0, 0.0, 0.0), (LX, LY, 1.6e-3))patch = Box((0.0105, 0.0083, 1.6e-3), (0.0295, 0.0217, 1.6e-3))
profiles = edge_aware_profiles((LX, LY, LZ), 1e-3, sheets=[patch], solids=[board])patch_sim = Simulation(freq_max=6e9, domain=(LX, LY, LZ), dx=1e-3, boundary="cpml", cpml_layers=8, **profiles)patch_sim.add(board, material="fr4")patch_sim.add_thin_conductor(patch)Draw the geometry at its true size; do not shift it to compensate.
Letting rfx choose the mesh
auto_configure sizes the cell, the domain, the absorber and the run length
from your geometry and band, and builds a dz_profile when it finds thin z
features:
from rfx import auto_configure
cfg = auto_configure( geometry=[(Box((0, 0, 0), (0.10, 0.08, 1.6e-3)), "fr4")], freq_range=(1e9, 4e9), materials={"fr4": {"eps_r": 4.4, "sigma": 0.025}}, accuracy="standard",)print(cfg.summary())auto_sim = Simulation(**cfg.to_sim_kwargs())cfg.uses_nonuniform tells you whether it chose a graded z mesh. Its profile
can step by more than the 1.4 ratio (it does here, and the Simulation
constructor warns), so treat it as a starting point. It plans from the
dielectrics you pass. Add conductors to the simulation afterwards, as
sheets or as volumes at least one cell thick.
If you create a Simulation without dx, rfx picks a mesh from the
registered geometry automatically; see
Materials and Geometry
for placing geometry on an automatic mesh.
Time step and cost
The time step follows the smallest cell anywhere in the grid:
dt = 0.99 / (c * sqrt(1/dx^2 + 1/dy^2 + 1/dz_min^2))With 1.5 mm cells in x and y and a 0.133 mm minimum in z, that is about 0.44 ps, against 2.86 ps for a uniform 1.5 mm grid. A graded mesh has far fewer cells but needs more steps. Whether it is faster overall depends on the model.
To estimate memory before a run, especially for gradients:
plan = sim.plan_ad_memory(n_steps=10_000, available_memory_gb=8.0)mesh_report = sim.mesh_intelligence_report( n_steps=10_000, checkpoint_every=plan.checkpoint_every, available_memory_gb=8.0)print(plan.recommendation)print(f"{mesh_report.cell_savings_factor:.1f}x fewer cells than uniform-fine")These are planning numbers. They say nothing about accuracy.
What runs on a graded mesh
| Feature | Graded mesh |
|---|---|
| point sources, probes | yes |
| lumped RLC elements | in the field update; no S-parameters |
| NTFF far field | limited: one dipole case checked against theory |
| DFT planes, flux monitors | run; no general accuracy statement |
wire-port S-parameters (add_port(..., extent=...)) | experimental; compare with a uniform run |
| one-cell lumped-port S-parameters | refused |
compute_msl_s_matrix | experimental; mode="eigenmode" refused |
compute_waveguide_s_matrix | single-mode, normalize=True or "flux" |
| coaxial calculators | refused |
| TFSF plane wave | normal incidence along ±x only |
| Floquet ports, UPML | refused |
Unsupported combinations raise before the first step instead of running with
a feature silently dropped. On a graded mesh, run() refuses snapshots and
conformal_pec=True, and on a closed (PEC) boundary it refuses until_decay;
use a fixed n_steps there.
Limits
- Accuracy is per observable. A graded run that completes is not evidence that its S-parameters, flux or far field are right. Compare with a uniform run or an external reference.
- Grading ratios above 1.4 in z, or 1.3 in x and y, are outside the validated range. Preflight warns.
- TFSF along a graded axis: preflight warns, because the scattered-field region picks up a spurious field (about 0.7 of the total-field peak with no scatterer). Normal incidence along ±x needs a uniform x axis.
- Microstrip probes on a graded feed: the automatic probe offset and spacing are counted in cells under the port, and recounted at run time. Keep the feed region uniform if you want predictable probe positions.
- Graded-mesh waveguide results beyond the published WR-90 comparisons are experimental. A dz-only profile has no accuracy evidence yet.
- Subgridding (
add_refinement) is a different tool: it works only throughrun()on a uniform mesh, andforward(),optimize()andtopology_optimize()refuse a refined model. - The full per-combination status is in the support matrix.
