Non-Uniform Mesh
rfx provides a graded z-profile (non-uniform Yee grid) with uniform dx and dy. In the current support contract, this is a limited-support path: retained for thin-substrate workflows, but not the default validated reference lane.
Current support status
Section titled “Current support status”- retained for thin-substrate / layered RF qualification work
- no silent fallback / no silent feature dropping
- unsupported combinations now fail clearly
Current unsupported combinations:
- periodic-port workflows + non-uniform z mesh
- NTFF + non-uniform z mesh
- DFT planes + non-uniform z mesh
- TFSF + non-uniform z mesh
- waveguide ports + non-uniform z mesh except the documented restricted
compute_waveguide_s_matrix(normalize=True)single-mode limited-support path - single-cell lumped-port S-parameters + non-uniform z mesh
compute_msl_s_matrix()+ non-uniform z mesh- coaxial ports + non-uniform z mesh
- lumped RLC + non-uniform z mesh
When to use a non-uniform mesh
Section titled “When to use a non-uniform mesh”| Situation | Recommendation |
|---|---|
| Substrate much thinner than wavelength (e.g., 1.6 mm FR4 at 2.4 GHz, lambda = 125 mm) | Non-uniform z is the limited-support tool to evaluate |
| Layer stack with multiple thin dielectric sheets | Non-uniform z for each layer, with limited-support caveats |
| Bulk 3-D structure with no thin z-features | Uniform grid |
| RCS / far-field only | Uniform grid is usually sufficient |
Without a non-uniform z-profile, resolving a 1.6 mm substrate with a 0.5 mm cell gives only 3 cells — too coarse. Shrinking the uniform cell to 0.2 mm resolves the substrate but inflates cell count by 15x in all three dimensions.
A non-uniform profile uses fine cells inside the substrate (e.g., 0.27 mm) and coarse cells in the air region (e.g., 1.5 mm), saving roughly 5–10x in total cell count.
Constructing dz_profile
Section titled “Constructing dz_profile”dz_profile is a 1-D NumPy array of physical z-cell sizes in metres, from z = 0 upward through the physical domain (excluding CPML padding, which rfx adds automatically).
Manual construction
Section titled “Manual construction”import numpy as np
h = 1.6e-3 # substrate thicknessn_sub = 6 # cells through substratedz_sub = h / n_sub # 0.267 mm per substrate cell
margin = 30e-3 # air region above substratedz_air = 1.5e-3 # coarse air cellsn_air = int(round(margin / dz_air))
dz_profile = np.concatenate([ np.full(n_sub, dz_sub), # fine: substrate np.full(n_air, dz_air), # coarse: air])!!! tip
Choose n_sub so that dz_sub ≤ dx / 2. At least 4 substrate cells are
recommended for accurate dispersion. 6–8 cells typically give under 0.5 % resonance error.
Graded transition (optional)
Section titled “Graded transition (optional)”For structures where abrupt fine-to-coarse transitions cause numerical reflections, use make_z_profile() to insert a smooth grading:
from rfx.nonuniform import make_z_profile
dz_profile = make_z_profile( features=[0.0, h], # z-positions that must align to cell boundaries domain_z=h + margin, dx_fine=dz_sub, dx_coarse=dz_air, grading=1.4, # max ratio between adjacent cells)Adjacent cells differing by more than ~1.5x introduce grid dispersion at the transition; keep grading ≤ 1.4.
Passing dz_profile to Simulation
Section titled “Passing dz_profile to Simulation”from rfx import Simulation, Box, GaussianPulseimport numpy as np
h = 1.6e-3margin = 30e-3n_sub = 6dz_sub = h / n_subn_air = 20dz_air = margin / n_air
dz_profile = np.concatenate([np.full(n_sub, dz_sub), np.full(n_air, dz_air)])
sim = Simulation( freq_max=4e9, domain=(0.10, 0.08, 0.0), # Lz=0 is OK — replaced by sum(dz_profile) boundary="cpml", cpml_layers=12, dx=5e-4, dz_profile=dz_profile,)When dz_profile is provided, domain[2] is replaced by sum(dz_profile) automatically. Passing domain[2]=0 is a convenient way to signal this.
auto_configure detection
Section titled “auto_configure detection”auto_configure() inspects the geometry for thin z-features and automatically builds a dz_profile when warranted:
from rfx import auto_configure, Simulation, Box
geometry = [ (Box((0, 0, 0), (0.10, 0.08, 1.6e-3)), "fr4"), (Box((0, 0, 0), (0.10, 0.08, 0)), "pec"), # ground plane]
cfg = auto_configure( geometry=geometry, freq_range=(1e9, 4e9), materials={"fr4": {"eps_r": 4.4, "sigma": 0.025}}, accuracy="standard",)
print(cfg.summary())# SimConfig (accuracy='standard'):# dx = 0.500 mm (20 cells/lambda_min)# dz = 0.267 – 1.500 mm (26 cells, non-uniform)
if cfg.uses_nonuniform: print("Non-uniform z activated")
sim = Simulation(**cfg.to_sim_kwargs())SimConfig.uses_nonuniform is True when dz_profile is not None.
CFL timestep from minimum cell
Section titled “CFL timestep from minimum cell”The timestep is set by the minimum cell size in the entire grid (including CPML padding cells), following the 3-D Courant-Friedrichs-Lewy condition:
dt = 0.99 / (c * sqrt(1/dx^2 + 1/dy^2 + 1/dz_min^2))A fine substrate cell dz_sub = 0.267 mm with dx = dy = 0.5 mm gives:
dt ~= 0.99 / (c * sqrt(4 + 4 + 14)) ~= 0.64 pscompared to dt ~= 0.96 ps for a uniform 0.5 mm grid. The non-uniform grid pays ~1.5x more timesteps, but saves ~10x in cells — a net win of ~7x for typical patch antenna problems.
Mesh and AD memory planning artifact
Section titled “Mesh and AD memory planning artifact”Use Simulation.mesh_intelligence_report(...) before running a memory-constrained
non-uniform case. It compares the configured grid against a uniform-fine grid at
the smallest configured cell size, carries preflight issues, and can include the
segmented-AD estimate used by the non-uniform scan path.
plan = sim.plan_ad_memory(n_steps=10_000, available_memory_gb=24.0)report = sim.mesh_intelligence_report( n_steps=10_000, checkpoint_every=plan.checkpoint_every, available_memory_gb=24.0,)
print(plan.recommendation)print(report.cell_savings_factor)print(report.recommendation)
# Store this alongside validation or experiment artifacts.plan_json = plan.to_json()report_json = report.to_json()Use plan_mesh(...) when deriving a mesh from geometry, or
Simulation.plan_mesh(...) before running an already configured
memory-constrained non-uniform case. The returned MeshPlan carries the
non-uniform profile audit (nominal_dx, dx_min/dx_max,
dy_min/dy_max, dz_min/dz_max, and profiles_present), the configured
memory summary, optional S-parameter support checks, and declaration-only
artifact paths. Supplying artifact_root names intended mesh-plan/report
outputs but does not create directories or write files; scene and replay remain
unclaimed until dedicated exporters exist.
MeshPlan is still a planning artifact rather than physics validation. Resolve
any support checks, then verify the observable against the appropriate uniform
or external reference lane.
The same planning artifact can be generated from the command line with:
python scripts/memory_reduction_planning_artifact.py --available-memory-gb 24.0For validated work, treat the report as a planning artifact rather than a
physics validation by itself: resolve any preflight_issues, then verify the
observable against the appropriate uniform or external reference lane.
make_current_source normalisation
Section titled “make_current_source normalisation”When building sources for the non-uniform grid at the low level, use make_current_source to correctly normalise the current injection to the local cell volume:
from rfx.nonuniform import make_current_source, make_nonuniform_gridimport numpy as np
grid = make_nonuniform_grid( domain_xy=(0.10, 0.08), dz_profile=dz_profile, dx=5e-4, cpml_layers=12,)
# Convert physical position to grid indexfrom rfx.nonuniform import z_position_to_indexiz = z_position_to_index(grid, z_phys=h / 2)
src = make_current_source( grid, ix=grid.nx // 2, iy=grid.ny // 2, iz=iz, component="ez",)!!! note
The high-level Simulation.add_source() handles this normalisation automatically.
make_current_source is only needed when using the low-level run_nonuniform() API
directly.
NonUniformGrid internals
Section titled “NonUniformGrid internals”make_nonuniform_grid() returns a NonUniformGrid NamedTuple:
| Field | Shape | Description |
|---|---|---|
nx, ny, nz | int | Grid dimensions including CPML |
dx, dy | float | Uniform x/y cell size (m) |
dz | (nz,) | Per-cell z sizes including CPML padding |
dt | float | Timestep from min-cell CFL (s) |
cpml_layers | int | CPML cells per face |
inv_dx | (nx,) | 1/dx, pre-computed for E-curl update |
inv_dz | (nz,) | 1/dz[k], pre-computed |
inv_dz_h | (nz,) | 2/(dz[k]+dz[k+1]), for H-curl update |
All inverse-spacing arrays are stored as jnp.float32 for GPU efficiency.