Radio maps

How a ReceiverGrid defines a radio map, how grid samples map to result tensor indices, and what the map values mean.

A radio map (coverage map) is the received power over an area. In Channel you get one by replacing a point receiver with a witwin.core.ReceiverGrid and running the deterministic or a Monte Carlo solver.

Defining the grid#

import torch
from witwin.core import ReceiverGrid
 
grid = ReceiverGrid(
    3,                                   # stable antenna ID
    torch.tensor([-2.0, 2.0, 0.5]),      # origin, the position of sample (0, 0)
    torch.tensor([1.0, 0.0, 0.0]),       # x_axis: direction of increasing row
    torch.tensor([0.0, -1.0, 0.0]),      # y_axis: direction of increasing column
    (4, 8),                              # shape: (rows, columns)
    (0.5, 0.5),                          # spacing: (row step, column step) in metres
)

Sample (row, column) is at origin + row * spacing[0] * x_axis + column * spacing[1] * y_axis. x_axis and y_axis are world-frame unit vectors, independent of the antenna orientation. Both spacings must be positive; flip an axis vector to step in the negative direction. grid.points() returns the (rows * columns, 3) sample positions in row-major order.

A grid is one receiver endpoint with one stable ID. It accepts the same antenna arguments as AntennaState (orientation, polarization, pattern, and so on), applied at every sample.

Result layout#

The deterministic solver returns maps shaped (T, columns, rows): one map per transmitter, with the column index first. For the grid above that is (T, 8, 4), and result.path_gain[t, j, i] is the value at sample (row=i, column=j).

Solverpath_gain shapeSpatial maps
deterministic(T, columns, rows)component_power and component_fields entries have the same shape as path_gain
montecarlo.basic(T, rows * columns)component_maps entries are (T, columns, rows); path_gain.reshape(T, columns, rows) matches their sum
montecarlo.bdpt(T, columns, rows)component_maps entries are (T, columns, rows)
pathnot a mapeach grid sample becomes one receiver in PathResult

To get a map indexed [t, row, column], transpose the last two axes:

gain_rc = result.path_gain.transpose(-1, -2)  # (T, rows, columns)

BDPT accepts either one ReceiverGrid or point receivers in a scene, not a mix.

What the values mean#

Map values are received power: the field projected on the receiver's polarization, squared, with the transmitter's power_w included. Antennas default to vertical polarization (0, 0, 1), so even with isotropic patterns the value drops sharply directly above or below a transmitter, where a vertically polarized wave has no vertical field component. With power_w=1.0 the value is the path gain, and 10 * log10(path_gain) is the gain in dB (a negative number; the path loss is its negative). A sample that no path reaches is exactly 0, which is -inf in dB. Clamp before taking the logarithm when you plot:

gain_db = 10 * torch.log10(result.path_gain.clamp_min(1e-20))

The deterministic solver adds complex fields, so its map shows interference between paths. Monte Carlo maps add powers and average out the fringes; they are estimates whose noise falls as samples grows. The deterministic map is therefore not expected to match a Monte Carlo map sample by sample, but area averages agree. The solver comparison shows both.

Warning

Monte Carlo Basic computes line of sight per sample, but estimates reflections by tracing rays from the transmitter and counting the rays that cross each grid cell. Reflected rays therefore have to cross the grid plane. With a horizontal grid at exactly the transmitter height and only vertical walls, no reflected ray crosses it and the reflection map is zero. Place the grid above or below the transmitter, as in Compute a radio map (transmitter at 2.0 m, grid at 1.5 m).

Resolution and cost#

The deterministic solver's cost grows with the number of samples times the number of candidate paths per sample, so a fine map in a large scene with max_depth=3 is expensive. Start with a coarse grid and max_depth=1, then refine. Monte Carlo cost grows with samples (the number of sampled ray paths, default 4096) and only weakly with grid size. Both Monte Carlo solvers cap their GPU workspace with workspace_limit_bytes (default 1 GiB) and raise rather than truncate when the budget is too small.