REMI Lab
OPEN KNOWLEDGE / TOOLS

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

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

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:

FieldDescription
delta_epsRelaxation strength Δε = εₛ − ε_∞
tauRelaxation 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 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.


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

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 tempfile
from 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 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.


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 resistance R_s = 1 / (σ_bulk · thickness).
  • surface_impedance_f0 set to a positive frequency: rfx uses the band-centre Leontovich resistance R_s0 = sqrt(π f0 μ0 / σ_bulk) for metals as well as lower-conductivity sheets. Any rasterizable shape may carry this operator, including patterned Cylinder, Sphere, and MeshShape geometries. 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.

Back to top ↑