Skip to content

Non-Uniform Mesh

rfx supports a non-uniform (graded) Yee grid. This guide covers the common case — a graded z-profile (dz_profile) with uniform dx and dy — for thin layered structures. dx_profile and dy_profile can grade the transverse axes, but their boundary-cell and domain-extent contracts differ from dz_profile. For concrete arrays, both dx_profile endpoints must equal the scalar dx, while the two concrete dy_profile endpoints must equal each other. For traced profiles, the caller must keep both dx_profile and dy_profile endpoints equal to the scalar dx. Domain-extent behavior is described with the setup example below. Use the uniform Cartesian Yee grid first. Choose a graded grid only when a thin feature would make a uniform grid impractical, and validate the final RF observable against a uniform or external reference.

  • suitable for thin-substrate or layered-z RF setup studies when the observable remains inside its documented limits;
  • memory and mesh reports are planning artifacts, not physics validation;
  • most unsupported combinations raise explicit errors rather than silently dropping features; a few (NTFF, DFT-plane probes, full-plane flux monitors) instead run on the graded-z path but are not part of the validated default — see the table below.

Unsupported or restricted combinations:

CombinationPublic status
periodic (Floquet) ports + non-uniform z meshunsupported (raises)
single-cell lumped-port S-parameters + non-uniform z meshunsupported (raises); nonuniform add_port(..., extent=...) WirePort extraction is experimental, so compare it with the uniform calculation or use a uniform grid
coaxial-port S-parameters + non-uniform z meshunsupported (raises)
lumped-RLC + non-uniform z meshruns as a field ADE element (time-domain fields only); no S-parameter output — not a S-param workflow and does not raise
compute_msl_s_matrix() + non-uniform meshmode='laplace'/'uniform' only (the add_msl_port default); mode='eigenmode' raises
compute_waveguide_s_matrix() + non-uniform transverse mesh (dx_profile/dy_profile)single-mode ports with normalize=True or normalize='flux'; one stored Palace comparison covers normalize='flux', a graded-dy ratio of 2, WR-90 empty/PEC-short/dielectric-slab cases, and magnitude over 8.2—12.4 GHz (max_mag_abs_diff=0.008529, previously 0.07009 on an under-provisioned absorber). Treat other profiles, bands, modes, phase, and junctions as unvalidated. Differentiable eps_override/sigma_override requires flux normalization. Only eps_override has a nonuniform AD-vs-FD regression test; neither gradient claim is RF validation.
TFSF plane-wave source + non-uniform z meshnormal incidence along x only (direction='+x'/'-x', angle_deg=0); ±z or oblique incidence raises
finite-region flux monitor (add_flux_monitor(size=...)) + non-uniform z meshunsupported (raises); use a full-plane monitor (omit size=)

NTFF boxes, DFT-plane probes, and full-plane flux monitors run on the graded-z path, but their observables must be validated against the uniform or external reference before any public claim.


SituationRecommendation
Substrate much thinner than wavelength (e.g., 1.6 mm FR4 at 2.4 GHz, lambda = 125 mm)Evaluate a non-uniform z mesh, then validate the final observable
Layer stack with multiple thin dielectric sheetsNon-uniform z can reduce cell count; keep support checks visible
Bulk 3-D structure with no thin z-featuresUniform grid
RCS / far-field onlyUniform 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 refines all three axes at once (2.5x per axis), inflating the total cell count by roughly 15x.

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). The resulting cell-count reduction is setup-dependent; record the mesh report and verify the RF observable, rather than treating the mesh saving itself as validation evidence.


dz_profile is a 1-D NumPy array of physical z-cell sizes in meters, from z = 0 upward through the physical domain (excluding CPML padding, which rfx adds automatically).

import numpy as np
h = 1.6e-3 # substrate thickness
n_sub = 6 # cells through substrate
dz_sub = h / n_sub # 0.267 mm per substrate cell
margin = 30e-3 # air region above substrate
dz_air = 1.5e-3 # coarse air cells
n_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 Resolve the substrate with at least 4 cells as a practical starting point; use 6–8 when the in-substrate field variation matters. Then verify the resonance or S-parameter observable for the specific geometry.

Avoid abrupt fine-to-coarse jumps; adjacent-cell ratios above 1.3x can introduce numerical reflections or dispersion. To generate a graded profile from scratch, use make_z_profile from rfx.nonuniform:

from rfx import smooth_grading
from rfx.nonuniform import make_z_profile
candidate = 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.3,
)
dz_profile = smooth_grading(candidate, max_ratio=1.3)

The final fill cell from make_z_profile() can still exceed the requested adjacent-cell ratio. smooth_grading() repairs that ratio, but it may change the profile length, total domain height, or a requested feature edge. Inspect the final cumulative edges after either operation. Keep grading conservative, record the generated profile, and verify the RF observable against the relevant reference.

from rfx import Box, Simulation
import numpy as np
h = 1.6e-3
n_sub = 6
lower_air = np.full(24, 8e-3 / 24) # 0.333 mm cells
substrate = np.full(n_sub, h / n_sub) # 0.267 mm cells
upper_air = np.full(90, 30e-3 / 90) # 0.333 mm cells
dz_profile = np.concatenate((lower_air, substrate, upper_air))
# Geometry boundaries must coincide with the final profile.
z_edges = np.concatenate(([0.0], np.cumsum(dz_profile)))
substrate_lo = z_edges[len(lower_air)]
substrate_hi = z_edges[len(lower_air) + n_sub]
assert np.isclose(substrate_hi - substrate_lo, h)
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,
)
sim.add(
Box((0.01, 0.01, substrate_lo), (0.09, 0.07, substrate_hi)),
material="fr4",
)
# Run preflight after Simulation and geometry are configured.
report = sim.preflight()
for issue in report.by_code("graded_box_rasterization"):
print(issue)
# Add the source and observables, then resolve the full report before running.

In the example above, domain[2]=0 tells the constructor to replace the z extent with sum(dz_profile). If a positive domain[2] is supplied, it is retained; rfx does not replace it or establish that it equals the profile sum. Set it explicitly to float(np.sum(dz_profile)) and verify the equality when the profile is assembled separately. Concrete dx_profile and dy_profile extents are synthesized from their profile sums. Traced transverse profiles cannot be host-summed, so the caller must supply positive concrete domain[0] / domain[1] extents and keep them consistent with the profiles.

Transition insertion changes the final cell sequence. A fine band requested near a thin Box can otherwise shift outside the box span, leaving the material with fewer cells than its requested fine spacing suggests. Inspect cumulative edges as shown above and run preflight after registering geometry.

The graded_box_rasterization advisory reports the actual and implied z-cell counts when a Box occupies at most four cells and less than half of the count implied by nearby fine spacing. Align the material boundary to a final profile edge or rebuild the grading region; do not dismiss the warning based only on the pre-grading request.


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"),
]
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 = 1.500 mm (50 cells/λ_min)
# ... # domain / cpml / n_steps / freq / source lines
# dz = 0.133 – 1.499 mm (67 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. This example plans from the dielectric thickness only. A zero-thickness Box does not rasterize a conductor; register a physical sheet separately with sim.add_thin_conductor(...) after constructing the simulation, or give a volumetric conductor a finite, cell-aligned thickness.


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))

For the auto-configured example above, dz_min = 0.133 mm with dx = dy = 1.5 mm gives approximately:

dt ~= 0.99 / (c * sqrt(0.44 + 0.44 + 56.5)) ~= 0.44 ps

(the terms are 1/dx^2, 1/dy^2, and 1/dz_min^2 in mm^-2), compared with about 2.86 ps for a uniform 1.5 mm grid. The non-uniform grid pays more timesteps because the smallest cell controls CFL; whether it is faster overall depends on the configured domain, material, monitors, and validation run.


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=8.0)
report = sim.mesh_intelligence_report(
n_steps=10_000,
checkpoint_every=plan.checkpoint_every,
available_memory_gb=8.0,
)
print(plan.recommendation)
print(report.cell_savings_factor)
print(report.recommendation)
# Store this alongside validation or local-run artifacts.
plan_json = plan.to_json()
report_json = report.to_json()

Use the module-level rfx.plan_mesh(geometry, freq_range, ...) 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 in its cell_sizes block (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; the scene and replay declarations stay not_claimed until dedicated exporters exist.

From a source checkout, the same planning artifact can be generated without running FDTD. The scripts/ path is not installed with the wheel:

Terminal window
python scripts/memory_reduction_planning_artifact.py \
--n-steps 10000 \
--available-memory-gb 8.0 \
--output /tmp/rfx-memory-plan.json

Both MeshPlan and the mesh-intelligence report describe the planned setup; they do not validate the physics. Resolve every preflight_issues entry and support check, then verify the observable against an appropriate uniform-grid or external reference.


For advanced use, run_nonuniform and make_current_source (both in rfx.__all__) expose the non-uniform field-update loop and cell-volume-normalized source injection directly, bypassing Simulation. See rfx/nonuniform.py for their signatures. Use make_current_source when constructing the low-level source array directly; the high-level Simulation path performs its own source registration and scaling.

Soft-source amplitude semantics: on this path the add_source waveform is natively a current in amperes (E += Cb·I/dV, resolution-independent injected power). Pass add_source(..., amplitude_kind='current') to say so explicitly — the bare per-path default is deprecated (issue #571), and an explicit amplitude_kind means the same amplitude on the uniform and non-uniform builders.