Quickstart

Build a scene with one wall, then compute the path gain, the propagation paths, the channel impulse response and a radio map.

This page takes about five minutes. You build a scene with one wall, a transmitter and a receiver, and run two solvers on it. You need a working installation.

The full script#

Save this as quickstart.py and run python quickstart.py.

import torch
 
from witwin.core import AntennaState, Mesh, PhysicalMaterial, ReceiverGrid, Scene, Structure
from witwin.channel import deterministic, path
 
FREQUENCY_HZ = 3.0e9
 
# A 6 m x 3 m wall in the plane x = 2.5 m, as two triangles.
vertices = torch.tensor([
    [2.5, -3.0, -1.0],
    [2.5, 3.0, -1.0],
    [2.5, -3.0, 2.0],
    [2.5, 3.0, 2.0],
])
faces = torch.tensor([[0, 1, 2], [1, 3, 2]])
wall = Structure(
    Mesh(vertices, faces, recenter=False, fill_mode="surface"),
    PhysicalMaterial(eps_r=4.0, sigma_e=0.02),
    name="wall",
)
 
tx = AntennaState(1, "tx", torch.tensor([0.0, -1.0, 0.5]), power_w=1.0)
rx = AntennaState(2, "rx", torch.tensor([0.0, 1.0, 0.5]))
scene = Scene(structures=[wall], endpoints=[tx, rx])
 
# 1. Coherent field and path gain, with the path table.
result = deterministic.solve(
    scene,
    deterministic.Config(max_depth=1, components={"los", "reflection"}, export_paths=True),
    reference_frequency_hz=FREQUENCY_HZ,
)
print("path gain:", result.path_gain.item())
for name, power in result.component_power.items():
    print(f"  {name:12s} {power.item():.4e}")
paths = result.paths
print("delays (ns):", [round(d * 1e9, 3) for d in paths.delay_s.tolist()])
print("reflection point:", paths.interaction_positions[1, 0].tolist())
 
# 2. Explicit paths and the channel impulse response.
links = path.solve(
    scene,
    path.Config(max_depth=1, components={"los", "reflection"}),
    reference_frequency_hz=FREQUENCY_HZ,
)
a, tau = links.cir()
print("paths per link:", links.num_paths.flatten().tolist())
print("CIR delays (ns):", [round(t * 1e9, 3) for t in tau.flatten().tolist()])
 
# 3. A radio map on a 4 x 8 grid of receivers at z = 0.5 m.
grid = ReceiverGrid(
    3,
    torch.tensor([-2.0, 2.0, 0.5]),     # origin: sample (0, 0)
    torch.tensor([1.0, 0.0, 0.0]),      # rows step along +x
    torch.tensor([0.0, -1.0, 0.0]),     # columns step along -y
    (4, 8),                             # 4 rows, 8 columns
    (0.5, 0.5),                         # spacing in metres
)
radio_map = deterministic.solve(
    Scene(structures=[wall], endpoints=[tx, grid]),
    deterministic.Config(max_depth=1, components={"los", "reflection"}),
    reference_frequency_hz=FREQUENCY_HZ,
)
gain_db = 10 * torch.log10(radio_map.path_gain)
print("map shape:", tuple(gain_db.shape))
print(f"map range: {gain_db.min().item():.1f} dB to {gain_db.max().item():.1f} dB")

Expected output:

path gain: 1.3773109458270483e-05
  los          1.5810e-05
  reflection   7.2370e-08
  diffraction  0.0000e+00
delays (ns): [6.671, 17.963]
reflection point: [2.5, 0.0, 0.5]
paths per link: [2]
CIR delays (ns): [0.0, 11.292]
map shape: (1, 8, 4)
map range: -53.2 dB to -36.1 dB

What the script does#

The scene. witwin.core.Scene holds structures and endpoints. A Structure pairs a geometry with a material. Here the geometry is an open Mesh of two triangles (fill_mode="surface" marks it as a surface, not a closed solid) and the material is a dielectric with relative permittivity 4 and conductivity 0.02 S/m. Endpoints are AntennaState objects with a stable integer ID, a role ("tx" or "rx") and a position in metres. Transmitters declare power_w. See Scenes and geometry.

The deterministic solve. deterministic.solve finds every line-of-sight and single-reflection path between the transmitter and the receiver and adds their complex fields coherently. path_gain is |field|**2, and the field includes the source amplitude sqrt(power_w). With power_w=1.0 it equals the path gain of the link (received power over transmitted power for isotropic antennas); with 2 W it doubles. It is smaller than the line-of-sight power alone because the two paths interfere. With export_paths=True, result.paths is a PathTable with one row per path: the direct path (2 m, 6.671 ns) and the wall reflection (5.385 m, 17.963 ns), which hits the wall at (2.5, 0, 0.5).

The path solve. path.solve returns the same paths in a padded PathResult with explicit antenna dimensions, plus delay, departure and arrival angles and the interactions of every path. cir() returns the channel impulse response; by default it shifts delays so the first path of each link arrives at 0. See Extract paths, CIR and CFR.

The radio map. A ReceiverGrid replaces the point receiver with a planar grid of sample points. Sample (row, column) sits at origin + row * spacing[0] * x_axis + column * spacing[1] * y_axis. The solver returns one value per transmitter and sample, shaped (T, columns, rows), here (1, 8, 4). See Radio maps.

All results are CUDA tensors. Units are SI: metres, seconds, hertz, watts.

Next steps#