Skip to content

Materials and Geometry

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 is a dict of pre-defined RF/microwave materials that ships with rfx.

from rfx import MATERIAL_LIBRARY
print(list(MATERIAL_LIBRARY.keys()))
# ['vacuum', 'air', 'fr4', 'rogers4003c', 'rogers4350b', 'rt_duroid_5880',
# 'alumina', 'silicon', 'ptfe', 'copper', 'aluminum', 'pec', 'water_20c']
Nameεᵣσ (S/m)Notes
vacuum1.00Lossless free space
air1.00060Standard atmosphere
fr44.40.025Lossy PCB laminate (constant-σ loss model)
rogers4003c3.552.67×10⁻³Low-loss substrate; σ from tan δ 0.0027 @ 5 GHz (process Dk 3.55)
rogers4350b3.487.16×10⁻³Low-loss substrate; σ from tan δ 0.0037 @ 10 GHz (design Dk 3.48)
rt_duroid_58802.201.10×10⁻³Very-low-loss PTFE composite; σ from tan δ 0.0009 @ 10 GHz
alumina9.80Ceramic substrate
silicon11.90.01Lightly doped semiconductor
ptfe2.10PTFE / Teflon
copper1.05.8×10⁷Good conductor — mask-enforced as PEC (see below)
aluminum1.03.5×10⁷Good conductor — mask-enforced as PEC (see below)
pec1.01×10¹⁰Perfect electric conductor (mask-enforced)
water_20c4.9 + Debye0Water 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")

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.


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:

FieldDescription
delta_epsRelaxation strength Δε = εₛ − ε_∞
tauRelaxation time (seconds)

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 np
from 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 constants
gold = drude_pole(omega_p=1.37e16, gamma=4.05e13)
sim.add_material("gold", eps_r=1.0, lorentz_poles=[gold])

lorentz_pole() parameters:

ParameterDescription
delta_epsOscillator strength Δε (dimensionless)
omega_0Resonance angular frequency (rad/s)
deltaDamping coefficient (rad/s)

drude_pole() parameters:

ParameterDescription
omega_pPlasma frequency (rad/s)
gammaCollision 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.


rfx uses constructive solid geometry (CSG). Shapes are rasterised onto the Yee grid at simulation time.

from rfx import Box
# Box(corner_lo, corner_hi) — axis-aligned rectangular prism
slab = 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.

from rfx import Sphere
# Sphere(center, radius)
ball = Sphere((0.025, 0.025, 0.010), radius=0.005)
sim.add(ball, material="alumina")
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")
from rfx import MeshShape # needs: pip install "rfx-fdtd[cad]"
# 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("bracket.stl", scale=1e-3, translate=(0.01, 0.0, 0.0))
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.

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 it
sim.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.


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.


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 one of two regimes from sigma_bulk:

  • σ_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 resistance R_s = 1 / (σ_bulk · thickness).