Wall reflection link

Check Channel against the image method with a transmitter, a receiver and a metal wall, where every delay, reflection point and coefficient has a closed form.

This tutorial builds the simplest scene with a reflection and checks Channel's answer against a hand calculation. It shows how to read a PathTable and what convention the complex coefficients use. It takes about one minute, including the first-call compile.

The scene#

A 6 m by 3 m metal wall stands in the plane x = 2.5. The transmitter is at (0, -1, 0.5) and the receiver at (0, 1, 0.5), both 2.5 m in front of the wall. Two paths connect them: line of sight, and one reflection off the wall.

The image method gives the reflected path exactly: mirror the transmitter in the wall plane, to (5, -1, 0.5), and draw a straight line from the image to the receiver. That line crosses the wall at the reflection point (2.5, 0, 0.5), and its length, sqrt(5**2 + 2**2) = 5.385 m, is the reflected path length.

Solve and compare#

import cmath
import math
 
import torch
 
from witwin.core import AntennaState, Mesh, PhysicalMaterial, Scene, Structure
from witwin.channel import deterministic
 
FREQUENCY_HZ = 3.0e9
WAVELENGTH_M = 299_792_458.0 / FREQUENCY_HZ
 
# A 6 m x 3 m metal wall in the plane x = 2.5.
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.perfect_conductor(), 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])
 
config = deterministic.Config(max_depth=1, components={"los", "reflection"}, export_paths=True)
result = deterministic.solve(scene, config, reference_frequency_hz=FREQUENCY_HZ)
 
paths = result.paths
for k in range(paths.delay_s.shape[0]):
    print(f"component {paths.component_id[k].item()}  length {paths.path_length_m[k].item():.4f} m  "
          f"delay {paths.delay_s[k].item() * 1e9:.3f} ns  gain {paths.path_gain[k].item():.4e}")
print("reflection point", [round(v, 3) for v in paths.interaction_position[1].tolist()])
 
# Image method: mirror the transmitter in the wall, then use free-space spreading.
image = torch.tensor([5.0, -1.0, 0.5])
lengths = {"los": 2.0, "reflection": (image - rx.position.cpu()).norm().item()}
wavenumber = 2 * math.pi / WAVELENGTH_M
expected = {
    "los": WAVELENGTH_M / (4 * math.pi * lengths["los"]) * cmath.exp(-1j * wavenumber * lengths["los"]),
    # A perfect conductor reflects the tangential (here vertical) field with coefficient -1.
    "reflection": -WAVELENGTH_M / (4 * math.pi * lengths["reflection"]) * cmath.exp(-1j * wavenumber * lengths["reflection"]),
}
for k, name in enumerate(expected):
    print(f"{name:10s} Channel {complex(paths.coefficient[k].item()):.6f}  image method {expected[name]:.6f}")
print(f"total gain  Channel {result.path_gain.item():.4e}  image method {abs(sum(expected.values())) ** 2:.4e}")

Output (RTX 5080):

component 0  length 2.0000 m  delay 6.671 ns  gain 1.5810e-05
component 1  length 5.3852 m  delay 17.963 ns  gain 2.1806e-06
reflection point [2.5, 0.0, 0.5]
los        Channel 0.003961-0.000345j  image method 0.003961-0.000345j
reflection Channel -0.001131-0.000949j  image method -0.001131-0.000949j
total gain  Channel 9.6821e-06  image method 9.6823e-06

Every number matches the hand calculation.

What the output shows#

  • Rows and components. The PathTable has one row per path. component_id 0 is line of sight and 1 is reflection; see component codes. Rows are sorted by receiver, transmitter, depth and component.
  • Delays. delay_s is the path length divided by the speed of light: 2 m gives 6.671 ns, 5.385 m gives 17.963 ns.
  • Per-path gain. path_gain of a row is the squared magnitude of its coefficient. For line of sight it is the Friis value (wavelength / (4 * pi * d))**2. The metal wall reflects all power, so the reflected row has the Friis value for 5.385 m.
  • Coefficient convention. A path's complex coefficient is its amplitude times exp(-1j * k * d), where k is the wavenumber and d the path length, times the coefficient of each interaction. Here the reflection adds a factor -1: both antennas default to vertical polarization (see Scenes and geometry), and a perfect conductor reverses the field tangential to its surface.
  • Coherent total. result.path_gain is abs(sum of coefficients)**2, so the two paths interfere. Here the total, 9.68e-6, is below the line-of-sight gain alone.

Try next#

  • Replace PhysicalMaterial.perfect_conductor() with PhysicalMaterial(eps_r=5.24, sigma_e=0.12). The reflected gain drops to the Fresnel reflection loss of the material; the delay stays the same.
  • Move the receiver behind the wall (x > 2.5) and add "transmission" with a thin-sheet material. See Model walls and materials.
  • Solve the same scene with path.solve to get the coefficients as a and the delays as tau, then compute the CIR and CFR. See Extract paths, CIR and CFR.