Building a structure in rfx is two steps: pick a material — a built-in library name or one you register with add_material() — and attach it to a CSG shape with sim.add().
Material library
MATERIAL_LIBRARY is a dict of pre-defined RF/microwave materials that ships with rfx.
from rfx import MATERIAL_LIBRARYprint(list(MATERIAL_LIBRARY.keys()))# ['vacuum', 'air', 'fr4', 'rogers4003c', 'rogers4350b', 'rt_duroid_5880',# 'alumina', 'silicon', 'ptfe', 'copper', 'aluminum', 'pec', 'water_20c']| Name | εᵣ | σ (S/m) | Notes |
|---|---|---|---|
vacuum | 1.0 | 0 | Lossless free space |
air | 1.0006 | 0 | Standard atmosphere |
fr4 | 4.4 | 0.025 | Lossy PCB laminate (constant-σ loss model) |
rogers4003c | 3.55 | 2.67×10⁻³ | Low-loss substrate; σ from tan δ 0.0027 @ 5 GHz (process Dk 3.55) |
rogers4350b | 3.48 | 7.16×10⁻³ | Low-loss substrate; σ from tan δ 0.0037 @ 10 GHz (design Dk 3.48) |
rt_duroid_5880 | 2.20 | 1.10×10⁻³ | Very-low-loss PTFE composite; σ from tan δ 0.0009 @ 10 GHz |
alumina | 9.8 | 0 | Ceramic substrate |
silicon | 11.9 | 0.01 | Lightly doped semiconductor |
ptfe | 2.1 | 0 | PTFE / Teflon |
copper | 1.0 | 5.8×10⁷ | Good conductor — mask-enforced as PEC (see below) |
aluminum | 1.0 | 3.5×10⁷ | Good conductor — mask-enforced as PEC (see below) |
pec | 1.0 | 1×10¹⁰ | Perfect electric conductor (mask-enforced) |
water_20c | 4.9 + Debye | 0 | Water at 20 °C with one Debye pole |
The tabulated σ for the substrate laminates is a fixed effective conductivity derived from the datasheet loss tangent at the stated frequency, not a frequency-dependent model.
Library materials are available directly by name; no add_material() call is needed:
from rfx import Simulation, Box
sim = Simulation(freq_max=10e9, domain=(0.05, 0.05, 0.02), boundary="cpml")sim.add(Box((0.005, 0.005, 0.005), (0.045, 0.045, 0.0066)), material="fr4")sim.add(Box((0.005, 0.005, 0.004), (0.045, 0.045, 0.005)), material="pec")Custom material registration
Register a named material with add_material() before using it in add().
sim.add_material( "my_ceramic", eps_r=9.0, sigma=0.001, mu_r=1.0, # optional, default 1.0)sim.add(Box((0.01, 0.01, 0.0), (0.04, 0.04, 0.002)), material="my_ceramic")add_material() returns self, so calls can be chained:
(sim .add_material("sub", eps_r=3.55, sigma=0.0) .add_material("metal", eps_r=1.0, sigma=5.8e7) .add(Box((0, 0, 0), (0.05, 0.03, 0.001)), material="sub") .add(Box((0.005, 0.005, 0.001), (0.045, 0.025, 0.0015)), material="metal"))Box is volumetric: every upper bound must exceed its lower bound. Use
add_thin_conductor(...) for a sheet whose physical thickness is below one
cell; a zero-thickness Box added with sim.add() rasterizes no volume.
Dispersive materials
Debye poles
Debye dispersion models frequency-dependent permittivity caused by dipolar relaxation (e.g., water, biological tissue):
ε(ω) = ε_∞ + Δε / (1 + jωτ)from rfx import DebyePole
sim.add_material( "water_body", eps_r=4.9, # ε_∞ (high-frequency limit) debye_poles=[ DebyePole(delta_eps=74.1, tau=8.3e-12), # primary relaxation ],)DebyePole fields:
| Field | Description |
|---|---|
delta_eps | Relaxation strength Δε = εₛ − ε_∞ |
tau | Relaxation time (seconds) |
Lorentz poles
Lorentz poles model phonon resonances and plasma-like media. Build them from
physical parameters with the lorentz_pole() and drude_pole() helpers, which
return a LorentzPole and pass straight into lorentz_poles=[...]:
import numpy as npfrom rfx import lorentz_pole, drude_pole
# Lorentz oscillator from (Δε, resonance, damping)sim.add_material( "resonant_material", eps_r=1.0, lorentz_poles=[ lorentz_pole(delta_eps=2.0, omega_0=2 * np.pi * 10e9, delta=1e8), ],)
# Drude (free-electron) model for metals — gold, illustrative constantsgold = drude_pole(omega_p=1.37e16, gamma=4.05e13)sim.add_material("gold", eps_r=1.0, lorentz_poles=[gold])lorentz_pole() parameters:
| Parameter | Description |
|---|---|
delta_eps | Oscillator strength Δε (dimensionless) |
omega_0 | Resonance angular frequency (rad/s) |
delta | Damping coefficient (rad/s) |
drude_pole() parameters:
| Parameter | Description |
|---|---|
omega_p | Plasma frequency (rad/s) |
gamma | Collision rate (rad/s) |
Both helpers return the low-level LorentzPole(omega_0, delta, kappa) tuple used
internally, where kappa = delta_eps · omega_0² (Lorentz) or omega_p² (Drude).
Prefer the helpers over constructing LorentzPole directly.
Geometry: CSG shapes
rfx uses constructive solid geometry (CSG). Shapes are rasterised onto the Yee grid at simulation time.
Box
from rfx import Box
# Box(corner_lo, corner_hi) — axis-aligned rectangular prismslab = Box((0.0, 0.0, 0.0), (0.05, 0.05, 0.002))metal_volume = Box((0.01, 0.01, 0.002), (0.04, 0.04, 0.0025))Coordinates are in meters. corner_lo and corner_hi are (x, y, z) tuples.
Sphere
from rfx import Sphere
# Sphere(center, radius)ball = Sphere((0.025, 0.025, 0.010), radius=0.005)sim.add(ball, material="alumina")Cylinder
from rfx import Cylinder
# Cylinder(center, radius, height, axis)# axis: "x", "y", or "z"via = Cylinder((0.025, 0.015, 0.000), radius=0.0005, height=0.002, axis="z")rod = Cylinder((0.010, 0.025, 0.000), radius=0.001, height=0.050, axis="x")sim.add(via, material="copper")MeshShape (CAD import)
import tempfilefrom pathlib import Path
import trimesh # ships with the same extra as MeshShape
from rfx import MeshShape # needs: pip install "rfx-fdtd[cad]"
# Stand-in for a real CAD export: a 30 x 20 x 2 mm part drawn in MILLIMETRES.with tempfile.TemporaryDirectory() as tmp_dir: bracket = Path(tmp_dir) / "bracket.stl" trimesh.creation.box(extents=(30.0, 20.0, 2.0)).export(bracket)
# STL / OBJ / PLY natively; STEP / STP via the cad extra's OpenCASCADE backend. # scale converts the file's length unit to metres (STL in mm -> scale=1e-3; # STEP is converted to metres already -> scale=1.0). part = MeshShape.from_file( str(bracket), scale=1e-3, translate=(0.025, 0.025, 0.010), )
sim.add(part, material="pec")!!! warning “Not differentiable”
MeshShape rasterizes host-side via trimesh point-in-mesh containment
and raises on traced coordinates — CAD-imported geometry cannot carry
gradients through forward()/optimize(). Use the CSG primitives for
any shape that must be a design variable. The design-interop layer
(rfx.interop) likewise refuses MeshShape rather than approximating it.
Box, Sphere, and Cylinder cover most structures. rfx also ships
PolylineWire, Via, and CurvedPatch; see the
Geometry and Materials API reference for their
signatures.
Composing shapes with draw order
rfx has no Shape-level Boolean CSG that you can pass to sim.add(). Instead,
shapes are rasterised in the order they are added, and on overlapping cells a
later shape overwrites the (εᵣ, σ, μᵣ) fill of an earlier one. Carve or layer a
structure by adding shapes back-to-front:
# Dielectric block with a lower-εr insert carved into itsim.add(Box((0.010, 0.010, 0.0), (0.040, 0.040, 0.004)), material="alumina")sim.add(Box((0.020, 0.020, 0.0), (0.030, 0.030, 0.004)), material="air") # wins here!!! warning
Draw-order overwrite applies only to dielectric / conductivity fills. PEC
cells (σ ≥ 10⁶ S/m, including copper, aluminum, and pec) accumulate in
a Boolean mask that is never cleared, so a later shape cannot carve a hole
in PEC. Model hollow or patterned metal by placing conductor only where metal
should exist.
PEC mask enforcement
Any material with σ ≥ 10⁶ S/m is enforced via a Boolean PEC mask rather than
through the conductivity update: the E-field is set to zero on every PEC cell at
each time step, which avoids the numerical instability that very large σ would
cause in the standard update equations. This threshold captures library pec
(σ = 10¹⁰) and the good-metal entries copper (5.8×10⁷) and aluminum
(3.5×10⁷) — all three are modelled as perfect conductors, so finite-conductivity
loss is not represented on this path.
!!! warning
To model a genuinely lossy conductor, keep σ below 10⁶ S/m; any material at or
above that threshold is silently promoted to PEC. The library copper and
aluminum entries are already above it, so they behave as perfect conductors,
not lossy metals, unless surface_impedance_f0 is explicitly selected on
add_thin_conductor(...).
For a PEC body that rasterizes to one cell along an axis, sim.add(..., two_plane=True) is an opt-in face-registered realization: it zeros the
tangential E components on both node planes instead of the default lower plane.
It is limited to hard PEC on the uniform run()/forward() scan and the
single-device non-uniform runner. Other runners reject it explicitly.
Thin conductor correction
For printed traces and patches thinner than one cell, use the subcell thin-conductor model instead of refining the mesh to resolve the sheet:
from rfx import Box
trace = Box((0.010, 0.014, 0.0016), (0.040, 0.016, 0.0016))sim.add_thin_conductor( trace, sigma_bulk=5.8e7, # copper thickness=35e-6, # 1 oz copper = 35 µm)add_thin_conductor picks a regime from sigma_bulk and the optional
surface_impedance_f0:
- σ_bulk ≥ 10⁶ S/m (copper, as above): the sheet is enforced as a thin PEC sheet — its cells are added to the PEC mask, no volumetric meshing needed. rfx emits a warning noting this routing. This is the common case for printed copper.
- σ_bulk < 10⁶ S/m (resistive films, inks): rfx sets an effective conductivity
σ_eff = σ_bulk · (thickness / Δx)on the intersected Yee cells rather than fully filling them, preserving the sheet resistanceR_s = 1 / (σ_bulk · thickness). surface_impedance_f0set to a positive frequency: rfx uses the band-centre Leontovich resistanceR_s0 = sqrt(π f0 μ0 / σ_bulk)for metals as well as lower-conductivity sheets. Any rasterizable shape may carry this operator, including patternedCylinder,Sphere, andMeshShapegeometries. The rasterized result must occupy exactly one cell layer along its normal and at least one cell; thicker or vanished sheets fail rather than degrade silently.
On a graded mesh (dz_profile), Δx above is the spacing between the midpoints
of the two cells either side of the sheet — (d[k-1] + d[k]) / 2 — not either
cell on its own. That is the length the sheet’s conductivity actually acts over
in the update, so the sheet resistance you asked for is what you get wherever
the sheet lands. The two are the same number whenever the neighbouring cells
match, which is every uniform mesh. If a sheet does land on a grading step,
preflight says so.
The surface-impedance operator is available on the uniform and single-device graded-mesh run/S-parameter lanes documented by the support matrix. Distributed, subgridded, ADI, UPML, dispersive-overlap, multimode waveguide, and MSL-junction combinations remain outside that path and fail explicitly.
