# Materials and Geometry

Channel: release; package: 1.8.0; source: 7d1e97331656dd74ad0f5471653ac0c5effdff70

Page: https://remilab.ai/rfx/versions/v1.8.0/guide/materials-geometry/
Source: https://github.com/bk-squared/rfx/blob/7d1e97331656dd74ad0f5471653ac0c5effdff70/docs/public/guide/materials-geometry.mdx

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.

```python
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 |
|---|---|---|---|
| `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:

```python
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()`.

```python
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:

```python
(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ωτ)
```

```python
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=[...]`:

```python
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:

| 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

```python
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

```python
from rfx import Sphere

# Sphere(center, radius)
ball = Sphere((0.025, 0.025, 0.010), radius=0.005)
sim.add(ball, material="alumina")
```

### Cylinder

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

```python
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](https://remilab.ai/rfx/versions/v1.8.0/api/geometry-materials/) 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:

```python
# 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:

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