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 7perfect_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)TrueSurfaceRoughness 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 0height 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.1318Tensor 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.