REMI Lab
OPEN KNOWLEDGE / TOOLS

Materials and Geometry

이 페이지의 내용

A structure in rfx is a shape plus a material: pick a library material or register your own with add_material(), then attach it to a shape with sim.add(). This page covers materials, shapes, and exactly how metal is placed on the grid. For the short version of the three conductor kinds, see Core Concepts.

Library materials

MATERIAL_LIBRARY holds common RF materials. Use them by name; no registration is needed.

from rfx import MATERIAL_LIBRARY, Box, Simulation
print(list(MATERIAL_LIBRARY))
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")
# A 1 mm PEC plate. It has real thickness, so it is a volume (see below).
sim.add(Box((0.005, 0.005, 0.004), (0.045, 0.045, 0.005)), material="pec")
Nameε_rσ (S/m)Notes
vacuum1.00
air1.00060
fr44.40.025lossy PCB laminate
rogers4003c3.552.67×10⁻³σ from tan δ 0.0027 at 5 GHz
rogers4350b3.487.16×10⁻³σ from tan δ 0.0037 at 10 GHz
rt_duroid_58802.201.10×10⁻³σ from tan δ 0.0009 at 10 GHz
alumina9.80
silicon11.90.01lightly doped
ptfe2.10
copper1.05.8×10⁷treated as PEC
aluminum1.03.5×10⁷treated as PEC
pec1.01×10¹⁰perfect conductor
water_20c4.9 + one Debye pole0water at 20 °C

The laminate σ values are a constant conductivity that matches the datasheet loss tangent at one frequency. Loss away from that frequency scales as 1/f, so it is not a frequency-dependent loss model.

When you omit dx, rfx picks the mesh from your geometry and materials, and preflight(), fidelity_report() and run() all use that same mesh. Adding geometry can change it. To differentiate with respect to geometry or mesh inputs, pass an explicit dx (and any profiles) up front.

Custom materials

sim.add_material("my_ceramic", eps_r=9.0, sigma=0.001, mu_r=1.0)
sim.add(Box((0.01, 0.01, 0.0), (0.04, 0.04, 0.002)), material="my_ceramic")

add_material() and add() return the simulation, so you can chain them:

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

Any material with σ ≥ 10⁶ S/m, such as "metal" above, becomes a perfect conductor. See How conductors land on the lattice.

Dispersive materials

Debye poles

A Debye pole models dipolar relaxation (water, tissue): ε(ω) = ε_∞ + Δε / (1 + jωτ). eps_r is ε_∞.

from rfx import DebyePole
sim.add_material("water_body", eps_r=4.9,
debye_poles=[DebyePole(delta_eps=74.1, tau=8.3e-12)])

delta_eps is εₛ − ε_∞ and tau is the relaxation time in seconds.

Lorentz and Drude poles

Build Lorentz and Drude poles with the helpers and pass them as lorentz_poles=[...]:

import numpy as np
from rfx import drude_pole, lorentz_pole
# Lorentz oscillator: strength, resonance (rad/s), damping (rad/s).
sim.add_material("resonant", eps_r=1.0, lorentz_poles=[
lorentz_pole(delta_eps=2.0, omega_0=2 * np.pi * 10e9, delta=1e8)])
# Drude plasma: plasma frequency and collision rate, both in rad/s.
sim.add_material("plasma", eps_r=1.0,
lorentz_poles=[drude_pole(omega_p=2 * np.pi * 10e9, gamma=2 * np.pi * 1e9)])

Both helpers return a LorentzPole(omega_0, delta, kappa). Use the helpers rather than building that tuple yourself.

A Drude or Lorentz medium has its own time-step limit, and a medium above it is refused when the model is assembled, with the largest stable step in the message. A metal at its optical plasma frequency (gold: ω_p ≈ 1.37×10¹⁶ rad/s) is far above that limit on any RF cell, and no stable step is found. Model RF metal as a conductor (material="pec", a thin conductor, or a finite sigma) instead.

Shapes

Coordinates are in metres. Shapes are rasterized onto the grid when the simulation is built.

from rfx import Cylinder, Sphere
slab = Box((0.0, 0.0, 0.0), (0.05, 0.05, 0.002)) # corner_lo, corner_hi
ball = Sphere((0.025, 0.025, 0.010), radius=0.005) # center, radius
via = Cylinder((0.025, 0.015, 0.001), radius=0.0005, # center, radius,
height=0.002, axis="z") # height, axis
sim.add(ball, material="alumina")
sim.add(via, material="copper")

Cylinder takes its centre point, not its base: the via above spans z = 0 to 2 mm. rfx also provides PolylineWire, Via and CurvedPatch; see the Geometry and Materials API.

CAD import

MeshShape imports STL, OBJ and PLY files, and STEP/STP through the CAD extra (pip install "rfx-fdtd[cad]"). scale converts the file’s length unit to metres:

import tempfile
from pathlib import Path
import trimesh
from rfx import MeshShape
# A stand-in CAD part: 30 x 20 x 2 mm, drawn in millimetres.
with tempfile.TemporaryDirectory() as tmp:
stl = Path(tmp) / "bracket.stl"
trimesh.creation.box(extents=(30.0, 20.0, 2.0)).export(stl)
part = MeshShape.from_file(str(stl), scale=1e-3,
translate=(0.025, 0.025, 0.010))
sim.add(part, material="pec")

STL in millimetres needs scale=1e-3. STEP files are converted to metres on import, so use scale=1.0. MeshShape is not differentiable: it raises on traced coordinates. Use Box, Sphere or Cylinder for any shape that must be a design variable.

Draw order

There is no Boolean CSG. Shapes are rasterized in the order you add them, and where they overlap, a later dielectric overwrites an earlier one. To carve a hole in a dielectric, add the hole’s material second:

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

This works for dielectrics only. Conductors accumulate and are never removed, so a later shape cannot cut a hole in metal. Draw metal only where metal should be.

How conductors land on the lattice

This section is the full rule. Other pages link here instead of restating it.

Any material with σ ≥ 10⁶ S/m is a conductor. rfx zeroes the tangential E on it instead of using σ in the update, which would be unstable. This covers pec, copper and aluminum, so all three are lossless. To model a lossy bulk material, keep σ below 10⁶ S/m. For conductor loss on a metal sheet, use add_thin_conductor(..., surface_impedance_f0=f0) (see Thin conductors).

The rule

An E component is PEC if and only if its location lies inside the closed conductor region. Every conductor is exactly one of three kinds, and your declaration decides which. rfx never infers the kind from how a shape happens to rasterize.

KindWhat it isDeclare it withAn E edge is PEC when
volumea set of cellssim.add(shape, material="pec") with Box, Sphere, Cylinderthe edge touches an occupied cell
sheeta footprint on one node plane, zero thicknesssim.add_thin_conductor(shape), or a zero-thickness Box in sim.add()the edge lies in the plane and both its end nodes are in the footprint
wirea path of edgesPolylineWire thinner than half a local cellthe edge lies on the path

Where things sit

Node i is at x_i = i·Δx. Cell i spans [x_i, x_{i+1}]. Ex[i,j,k] sits at (x_{i+½}, y_j, z_k), and Ey, Ez likewise are offset by half a cell along their own axis.

  • Volumes are sampled at cell centres: cell i is occupied when lo ≤ x_{i+½} < hi. A box drawn on node planes occupies exactly the cells between them. An off-lattice face rounds to the nearest node plane.
  • Dielectrics are sampled at nodes, half-open.
  • Sheets land on the node plane nearest the shape’s mid-plane along its normal. An exact half-cell tie goes to the lower plane.

What follows

  • A PEC Box from z_a to z_b on node planes has walls at both faces, and every edge between them is shorted. The realized thickness equals the drawn thickness, including for a one-cell plate.
  • A sheet owns no cell. It writes no ε_r or σ, and the E component normal to it stays live.
  • Sheets on the same plane are merged first, so abutting sheets join without a seam. Sheets on adjacent planes stay two separate films.
  • For a sheet to sit on a dielectric face, that face must be on a node plane. Node planes are integer multiples of dx from the domain origin. Choosing dx = h_sub / n sets the laminate’s thickness in cells but does not put its face on a node; the face’s offset must also be a whole number of cells. Otherwise the sheet snaps to the nearest node, and preflight reports the offset.
  • The rule does not depend on axis order or mirroring.

sim.fidelity_report() prints each conductor’s drawn extent next to the realized wall planes, in your units. Read it whenever placement matters.

What rfx refuses

These declarations are accepted by sim.add() or add_thin_conductor(), but sim.preflight() reports them as errors (“cannot be realized on this lattice”), and run() and forward() raise ValueError:

  • A PEC shape with 0 < thickness < one local cell on any axis. It is neither a sheet nor a resolved volume. Declare a sheet, or resolve the thickness.
  • A PEC shape that rasterizes to zero cells, such as a sphere smaller than a cell. Metal never vanishes silently.
  • A PEC Box with two or three zero-thickness axes. A line or a point is not a conductor; use PolylineWire.
  • An add_thin_conductor() shape thicker than one cell along its normal. That is a volume; use add().

A PEC Box with exactly one zero-thickness axis is fine: it is a sheet, realized exactly as add_thin_conductor() would realize it.

The removed two_plane=True keyword on sim.add() raises TypeError immediately. The volume rule already gives every plate both faces.

Metal that is not a declared conductor

Three things put metal in a domain outside this rule. Know which one a result came from:

  • A PEC boundary (boundary="pec" or a BoundarySpec face) zeroes tangential E on the domain face. It is part of the domain, not a body.
  • A σ fill through the low-level rasterize(), such as rasterize(grid, [(shape, 1.0, 1e7)]), writes a large conductivity into cells and lets the update damp the field. It is a lossy-volume model, not PEC: no edge is zeroed and fidelity_report() does not list it. Some scattering examples use it on purpose because TFSF illumination takes material arrays directly.
  • Subpixel smoothing and conformal PEC (subpixel_smoothing="kottke_pec", conformal_pec=True) use their own weights near curved metal.

Coax conductors built by the coaxial port are realized PEC, not a σ fill; see Sources and Ports.

Freeze the mesh before placing geometry from it

With an automatic mesh, adding geometry can refine the mesh and move node planes. If you place conductors using grid coordinates, first register the geometry that drives the mesh, then call sim.freeze_mesh():

frozen = Simulation(freq_max=10e9, domain=(0.03, 0.03, 0.03), boundary="pec")
grid = frozen.freeze_mesh()
d = float(grid.dx)
frozen.add(Box((5 * d, 5 * d, 5 * d), (9 * d, 9 * d, 6 * d)), material="pec")

Reading the grid or running preflight does not freeze it. After freezing, a feature the mesh cannot resolve is still refused. On a non-uniform grid, use the node coordinates, since grid.dx is only the boundary spacing. Register ports and boundaries before you keep array indices, because they can change the padding. Alternatively, pass explicit dx= and profiles from the start.

Worked example: foil is a sheet

35 µm copper under a 0.787 mm RO4003C laminate, with dx = h_sub / 4. The copper is a fifth of a cell thick, so a Box in add() could not be realized: preflight flags it and run() raises. Declare it as a sheet, and put the laminate floor on a node plane by writing its position as a whole number of cells:

H_SUB = 0.787e-3
DX = H_SUB / 4 # 196.75 um: four cells through the laminate
Z_GND = 20 * DX # node plane 20
XY_LO, XY_HI = 25 * DX, 175 * DX # nodes 25 and 175
board = Simulation(freq_max=12e9, domain=(0.04, 0.04, 0.012), dx=DX,
boundary="cpml")
board.add(Box((XY_LO, XY_LO, Z_GND), (XY_HI, XY_HI, Z_GND + H_SUB)),
material="rogers4003c")
board.add_thin_conductor(Box((XY_LO, XY_LO, Z_GND), (XY_HI, XY_HI, Z_GND)))
board.fidelity_report()

The report has no findings. The z rows read:

geometry[0] 'rogers4003c'
z: declared [3935.0, 4722.0] um -> realized [3935.0, 4722.0] um | face residuals (0.0, 0.0) um | extent 787.0 -> 787.0 um
thin_conductor[0] sigma_bulk=5.8e+07 S/m -> pec — PEC sheet on node plane z = 3935.00 um (...)
z: declared [3935.0, 3935.0] um -> realized [3935.0, 3935.0] um | face residuals (0.0, 0.0) um | extent 0.0 -> 0.0 um

A floor at a round z = 4 mm would not be a node plane (4 mm / 196.75 µm = 20.33). The sheet would land at node 20, 65 µm below the dielectric. Drawing the foil as a one-cell Box is also wrong: it becomes a volume that takes the laminate’s bottom cell, and the report flags that 25 % of the dielectric’s cells are also PEC.

Worked example: a plate is a volume

A 2 mm iris in a WR-90 guide at dx = 1 mm:

guide = Simulation(freq_max=12e9, domain=(0.30, 0.02286, 0.01016), dx=1e-3,
boundary="cpml")
guide.add(Box((0.145, 0.0, 0.0), (0.147, 0.02286, 0.01016)), material="pec")

It is realized with faces at x = 145 mm and 147 mm and everything between them shorted, so a closed-form model can use the drawn 2 mm directly. fidelity_report() lists three wall planes (145, 146 and 147 mm) because the interior plane is shorted too. max − min of those planes is the plate’s outer span. To measure an aperture, take the gap between wall groups.

Thin conductors

Use add_thin_conductor() for printed copper and resistive films that are thinner than a cell:

trace = Box((0.010, 0.014, 0.0016), (0.040, 0.016, 0.0016))
sim.add_thin_conductor(trace, sigma_bulk=5.8e7, thickness=35e-6)

sigma_bulk and surface_impedance_f0 choose the model:

SettingModel
sigma_bulk >= 1e6 (default copper)lossless PEC sheet on one node plane. thickness is ignored. rfx warns.
sigma_bulk < 1e6 (resistive film, ink)lossy sheet with conductivity sigma_bulk * thickness / d, preserving the sheet resistance 1/(σ·t)
surface_impedance_f0=f0resistive sheet with Rs = √(π f0 μ0 / σ_bulk), same footprint and edges as the PEC sheet

Here d is the cell size normal to the sheet. On a graded mesh it is (d[k-1] + d[k]) / 2, the average of the two cells either side of the sheet plane, so you get the sheet resistance you asked for wherever the sheet lands.

The surface-impedance model is a thick-conductor (Leontovich) loss:

  • It adds loss only. It does not model the surface reactance, so resonances read slightly high.
  • Rs is fixed at f0 for the whole run. Its error grows as |√(f/f0) − 1| away from f0.
  • It is valid for conductors at least about three skin depths thick. For thinner films, use the sigma_bulk < 1e6 path instead.
  • The sheet can have any shape (discs, meanders, MeshShape outlines, ground planes with holes), but it must rasterize to exactly one cell layer.

A surface-impedance sheet does not appear in pec_mask or in the σ array. To find all metal in a model, use sim.conductor_mask().

A sheet owns no cell, so it does not take the material of the cells around it. If you leave a vacuum slot for the foil between two dielectric boxes, the E edges on the sheet plane sit in vacuum and the stack-up gains a thin air layer. Extend the dielectric to the sheet plane; preflight flags the slot.

Limits

  • Metal loss. Every metal in the library is lossless PEC. For conductor loss on a sheet, use surface_impedance_f0. There is no volumetric metal loss.
  • Laminate loss is constant σ. It matches tan δ at one frequency only. For broadband loss, fit Debye or Lorentz poles to your material data.
  • Off-grid geometry moves. A face between node planes lands on the nearest one, up to half a cell away. Check fidelity_report(), and refine instead of rounding your design to the grid.
  • Surface-impedance sheets run on the uniform and single-device graded-mesh run() and S-parameter paths. Distributed, subgridded, ADI, UPML, multimode-waveguide, MSL-junction and optimization paths refuse them. See the support matrix.
  • MeshShape is not differentiable.
Back to top ↑