Define materials

Create lossless, lossy, magnetic and perfectly conducting materials, add dispersion, layers, roughness and phase screens, and make parameters trainable.

This guide shows the common ways to build a PhysicalMaterial. For the formulas and the sign convention, see Material model.

Dielectrics and conductors#

The first three arguments are positional: eps_r, mu_r, sigma_e. The fourth is name. Everything else is keyword-only.

from witwin.core import PhysicalMaterial
 
air = PhysicalMaterial()                                        # eps_r=1, mu_r=1, sigma_e=0
glass = PhysicalMaterial(eps_r=6.27, sigma_e=0.0043, name="glass")
ferrite = PhysicalMaterial(eps_r=12.0, mu_r=50.0, name="ferrite")
 
for m in (air, glass, ferrite):
    caps = m.capabilities()
    print(m.name, caps.conductive, caps.magnetic, m.evaluate_at_frequency(5.0e9).eps_r)
None False False (1-0j)
glass True False (6.27-0.015458589082689215j)
ferrite False True (12-0j)

eps_r and mu_r must be greater than 0 and sigma_e at least 0. Invalid Python values raise ValueError at construction.

Perfect conductors#

metal = PhysicalMaterial.perfect_conductor()               # name "perfect_conductor"
copper_pec = PhysicalMaterial.perfect_conductor(name="copper", material_id=7)
print(metal.conductor_model, metal.capabilities().perfect_conductor, int(copper_pec.material_id))
perfect True 7

perfect_conductor(name="perfect_conductor", **kwargs) passes other keyword arguments to the constructor; it refuses conductor_model.

Frequency-dependent materials#

PowerLawDispersion scales eps_r and sigma_e with frequency relative to a reference frequency:

from witwin.core import PowerLawDispersion
 
concrete = PhysicalMaterial(
    eps_r=5.24,
    sigma_e=0.0462,
    name="concrete",
    dispersion=PowerLawDispersion(reference_frequency_hz=1.0e9, eps_r_exponent=0.0, sigma_e_exponent=0.7822),
)
for f in (1.0e9, 10.0e9):
    s = concrete.evaluate_at_frequency(f)
    print(f, round(s.sigma_e, 4), s.eps_r)
1000000000.0 0.0462 (5.24-0.8304497856049322j)
10000000000.0 0.2798 (5.24-0.5029367572710886j)

These exponents follow the ITU-R P.2040 form for concrete: sigma_e grows from 0.0462 S/m at 1 GHz to 0.28 S/m at 10 GHz.

For any other model, write an object with a complex_eps(frequency_hz) method. It satisfies the DispersionSpec protocol, and Core returns its value as the complex permittivity:

class Debye:
    """Single-pole Debye model: eps_inf + delta / (1 + j * f / f_relax)."""
 
    def __init__(self, eps_inf, delta, f_relax):
        self.eps_inf, self.delta, self.f_relax = eps_inf, delta, f_relax
 
    def complex_eps(self, frequency_hz):
        return self.eps_inf + self.delta / (1 + 1j * frequency_hz / self.f_relax)
 
 
water_like = PhysicalMaterial(eps_r=80.0, dispersion=Debye(5.0, 75.0, 20.0e9), name="water-like")
print(water_like.evaluate_at_frequency(20.0e9).eps_r)
(42.5-37.5j)

Use the same sign convention as Core (negative imaginary part for loss).

Layered slabs#

A thin wall or a window is often several layers. layers takes MaterialLayer values. Each layer has a thickness and its own eps_r, sigma_e, mu_r and optional dispersion:

from witwin.core import MaterialLayer
 
double_glazing = PhysicalMaterial(
    eps_r=6.27,
    sigma_e=0.0043,
    thickness_m=0.024,
    geometry_mode="thin_sheet",
    layers=(
        MaterialLayer(thickness_m=0.006, eps_r=6.27, sigma_e=0.0043),
        MaterialLayer(thickness_m=0.012, eps_r=1.0),
        MaterialLayer(thickness_m=0.006, eps_r=6.27, sigma_e=0.0043),
    ),
    name="double glazing",
)
print(double_glazing.capabilities().layered, len(double_glazing.layers))
print(double_glazing.layers[1].evaluate_at_frequency(5.0e9).eps_r)
True 3
(1-0j)

thickness_m (default 0.1 m) and geometry_mode ("thin_sheet" by default, or "volumetric") tell a solver whether to treat the structure as a sheet of that thickness or as a solid volume. Core stores layers and does not combine them; how a solver uses them is up to the solver.

Surface properties#

from witwin.core import SurfaceRoughness
 
brick = PhysicalMaterial(
    eps_r=3.75,
    sigma_e=0.038,
    roughness_front=SurfaceRoughness(rms_height_m=0.002, correlation_length_x_m=0.05, correlation_length_y_m=0.05),
    scattering_coefficient=0.3,
    xpd_coefficient=0.1,
    gain=1.0,
    name="brick",
)
print(brick.capabilities().rough)
True

SurfaceRoughness describes Gaussian-correlated roughness: RMS height, two correlation lengths and the angle of the first correlation axis (principal_axis_rad). scattering_coefficient and xpd_coefficient must be in [0, 1], and gain at least 0. Core stores them for solvers.

Phase screens#

A phase screen is a height map over a surface. It belongs to a structure, not to a material:

import torch
 
from witwin.core import PhaseScreen, Sphere, Structure
 
screen = PhaseScreen(height=torch.randn(64, 64), height_scale_m=0.001, mode="realization_coherent")
rough_ball = Structure(Sphere(radius=0.5), brick, phase_screen=screen)
print(screen.mode, screen.realization_id)
realization_coherent 0

height is an (H, W) floating-point tensor. mode is "realization_coherent" or "ensemble_bsdf". Core checks and stores height_scale_m (greater than 0, in metres), height_offset_m, realization_id, correlation and quadrature_tolerance; the solver interprets them. Changing a phase screen changes the scene's assignment_version.

Trainable parameters#

Pass 0-d floating-point tensors for any continuous field. Core keeps them, and evaluate_at_frequency keeps the graph:

eps_r = torch.tensor(4.0, requires_grad=True)
sigma_e = torch.tensor(0.02, requires_grad=True)
wall = PhysicalMaterial(eps_r=eps_r, sigma_e=sigma_e)
 
sample = wall.evaluate_at_frequency(torch.tensor(3.5e9))
loss = sample.eps_r.abs()
loss.backward()
print(round(eps_r.grad.item(), 4), round(sigma_e.grad.item(), 4))
0.9997 0.1318

Tensor values are not range-checked, so keep them physical yourself (for example with a softplus reparameterisation).

Replacing a material later#

Materials are immutable. To change one in a scene, build a new PhysicalMaterial with the same material_id and a higher version, then call Scene.with_material. See Update a scene.