Scenes and geometry
How Channel reads a witwin.core Scene - structures, meshes, endpoints, stable IDs, versions - and compiles it into GPU ray-tracing resources.
Channel does not define its own scene type. You describe the world with witwin.core, the WiTwin world model shared by Channel, Maxwell, Radar and WiTwin Studio, and Channel compiles it for the GPU. This page explains what Channel reads from that world and how compilation works.
Coordinate system and units#
Channel works in a right-handed world Cartesian frame. Positions and lengths are in metres, frequency in hertz, delay in seconds, angles in radians and power in watts. There is no implicit "up" axis for the solvers; the examples in these docs use z as height.
The phase convention is exp(-j*k*d) for a path of length d and wavenumber k, with time dependence exp(+j*2*pi*f*t).
The scene#
witwin.core.Scene(*, structures=(), endpoints=(), ...) is an immutable world: a tuple of Structure objects and a tuple of endpoints. Methods such as with_structures, with_structure_geometry, with_structure_material, with_material and with_endpoints return a new scene and bump the matching version counter.
from witwin.core import Scene
scene = Scene(structures=[wall], endpoints=[tx, rx])
moved = scene.with_endpoints([tx_moved, rx])A Scene carries four version counters: topology_version, geometry_version, material_version and assignment_version. Channel uses them to cache compiled scenes and to detect a stale fixed topology. scene.snapshot(time_s) returns a SceneSnapshot, a time-labelled copy that every solver also accepts.
Structures and geometry#
A Structure(geometry, material, name=None, ...) pairs one geometry with one material. Channel converts every enabled structure to a triangle mesh with the geometry's to_mesh() method, so you can use any witwin.core geometry:
| Geometry | Use |
|---|---|
Mesh(vertices, faces, ...) | Walls, floors, buildings, imported city models. Mesh.from_obj(path, ...) loads a Wavefront OBJ file. |
Box, Cylinder, Sphere, Cone, Ellipsoid, Prism, Pyramid, Torus, HollowBox | Simple solids, tessellated by to_mesh(). |
For a Mesh, two arguments matter for ray tracing:
recenter(defaultTrue) moves the vertex bounding-box center to the origin beforepositionis applied. Passrecenter=Falsewhen your vertices are already in world coordinates.fill_modeis"auto"(default),"solid"or"surface"."surface"declares an open surface, for example a single wall."auto"treats a watertight mesh as a solid and any other mesh as a surface.
import torch
from witwin.core import Box, Mesh, PhysicalMaterial, Structure
wall = Structure(
Mesh(
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]]),
torch.tensor([[0, 1, 2], [1, 3, 2]]),
recenter=False,
fill_mode="surface",
),
PhysicalMaterial(eps_r=4.0, sigma_e=0.02),
name="wall",
)
pillar = Structure(Box(position=(2.0, 0.0, 0.0), size=(1.0, 1.0, 4.0)), PhysicalMaterial.perfect_conductor())Every triangle receives a stable primitive ID and every structure a material ID. Results report these IDs (primitive_id, material_id) so you can map a path back to the surface it hit. Diffraction runs on the mesh edges, so the edges of your triangles are the wedges Channel can diffract around.
Endpoints#
Endpoints are the antennas. AntennaState(antenna_id, role, position, *, ...) is one transmitter (role="tx") or receiver (role="rx"):
| Argument | Meaning |
|---|---|
antenna_id | Stable integer ID. It must be unique in the scene. |
position | World position in metres, a 3-element tensor. A tensor with requires_grad=True makes the result differentiable with respect to it. |
power_w | Transmit power in watts. Leave it out for receivers. A transmitter without it is solved with 1 W. |
orientation | Optional local-to-world rotation of the antenna. |
polarization | Optional polarization vector in the antenna frame. The default is vertical, (0, 0, 1), or (1, 0, 0) with pattern="horizontal". |
element_positions | Optional (N, 3) element offsets, for an antenna array. See Use antenna arrays. |
weights | Optional default complex element weights, used by PathResult.beamform(). |
synthetic_array | True (default) models the array with one ray trace and per-element phase steering; False traces every element. |
pattern | "isotropic" (default), "vertical", "horizontal", "custom", or an AntennaPattern. |
ReceiverGrid is a receiver that stands for a planar grid of sample points. See Radio maps.
Results index transmitters and receivers in the order they appear in scene.endpoints, filtered by role.
Compilation#
Before it traces rays, Channel compiles the scene at one reference frequency into a CompiledScene. Compilation:
- converts every structure to triangles and builds a RayD scene with a BVH (bounding volume hierarchy) on the GPU;
- evaluates every material at the reference frequency and uploads the material tables;
- records the scene's four version counters.
The four solvers compile for you. witwin.channel.scene.compile(scene, reference_frequency_hz=...) does the same thing explicitly; you need it for the propagation consumer. Compiled scenes are cached per process, so solving the same scene again at the same frequency reuses the GPU resources. witwin.channel.scene.clear_compile_cache() drops the cache.
Because materials are evaluated at compile time, the compiled scene is tied to one frequency. To simulate another carrier, call the solver with another reference_frequency_hz; Channel compiles again. A propagation-consumer request whose frequency does not match its CompiledScene fails; Channel never recompiles or reuses stale material values silently.