Place antennas and receiver grids

Create transmitters and receivers, set orientation, polarization, array elements, weights, patterns and power, and lay out a planar receiver grid.

Endpoints are the transmitters and receivers in a scene. Core describes them; solvers decide how to radiate and receive. Two classes exist: AntennaState for one endpoint, and ReceiverGrid for a plane of receiver samples that counts as one endpoint.

A transmitter and a receiver#

from witwin.core import AntennaState, Scene
 
tx = AntennaState(1, "tx", (0.0, 0.0, 10.0), power_w=1.0)
rx = AntennaState(2, "rx", (50.0, 20.0, 1.5))
scene = Scene(endpoints=[tx, rx])
print(tx.role, tx.position, tx.pattern.kind, tx.synthetic_array)
tx tensor([ 0.,  0., 10.]) isotropic True

The first three arguments are positional: antenna_id, role ("tx" or "rx") and position. The ID is always yours to choose; Core reserves it so automatic IDs do not collide with it.

Orientation and polarization#

orientation rotates the endpoint's local frame into the world. It takes Euler angles (yaw, pitch, roll) in radians or a quaternion (w, x, y, z). polarization is a 3-vector in the local frame.

import math
 
tilted = AntennaState(3, "tx", (0.0, 0.0, 2.0), orientation=(math.pi / 2, 0.0, 0.0), polarization=(1.0, 0.0, 0.0))
print(tilted.orientation_matrix().round(decimals=3))
print(tilted.polarization_world().round(decimals=3))
tensor([[-0., -1.,  0.],
        [ 1., -0.,  0.],
        [-0.,  0.,  1.]])
tensor([-0., 1., 0.])

A yaw of 90 degrees turns local x into world y. Both methods keep autograd, so you can optimise an orientation.

Warning

Antenna Euler angles are (yaw, pitch, roll); geometry Euler angles are (roll, pitch, yaw). See Coordinates and units.

Arrays#

Give element_positions (A, 3) in the local frame, and optionally complex or real weights (A,):

import torch
 
spacing = 0.5 * 3.0e8 / 28.0e9                                   # half a wavelength at 28 GHz
elements = torch.tensor([[0.0, (i - 1.5) * spacing, 0.0] for i in range(4)])
steer = torch.exp(-1j * math.pi * torch.arange(4) * math.sin(math.radians(20.0)))
 
array = AntennaState(4, "tx", (0.0, 0.0, 10.0), element_positions=elements, weights=steer, synthetic_array=True)
print(array.element_positions.shape, array.weights.dtype)
torch.Size([4, 3]) torch.complex64

weights must have one value per element. synthetic_array (default True) is a flag for the solver. In Channel, True traces rays once from the array position and gives each element a phase offset, and False traces every element; see Use antenna arrays.

Patterns#

pattern is a string or an AntennaPattern. The kinds are "isotropic" (default), "vertical", "horizontal" and "custom". A custom pattern carries a callable that maps a tensor to a tensor:

from witwin.core import AntennaPattern
 
def cosine_pattern(direction):
    return direction[..., 2].clamp_min(0.0)
 
patch = AntennaState(5, "tx", (0.0, 0.0, 3.0), pattern=AntennaPattern("custom", cosine_pattern))
print(patch.pattern.kind)
custom

"custom" requires a callable, and the other kinds forbid one. Core does not call the function; check the solver's documentation for the argument it passes.

Receiver grids#

A ReceiverGrid is a row-major plane of receiver samples. It takes an ID, the origin, two world-frame axis directions, a (rows, columns) shape and a (row, column) spacing:

from witwin.core import ReceiverGrid
 
coverage = ReceiverGrid(
    10,
    origin=(-10.0, -10.0, 1.5),
    x_axis=(1.0, 0.0, 0.0),
    y_axis=(0.0, 1.0, 0.0),
    shape=(41, 41),
    spacing=(0.5, 0.5),
)
points = coverage.points()
print(coverage.role, points.shape, points[0].tolist(), points[-1].tolist())
rx torch.Size([1681, 3]) [-10.0, -10.0, 1.5] [10.0, 10.0, 1.5]

Sample (row, column) sits at origin + row * spacing[0] * x_axis + column * spacing[1] * y_axis. The axes are not normalised for you; pass unit vectors unless you want a sheared or scaled grid. A grid's role is always "rx", and its origin is the same tensor as position. ReceiverGrid takes the same optional orientation, polarization, element_positions, weights, synthetic_array, pattern and metadata as AntennaState, but no power_w.

Moving endpoints#

Endpoints move through a DynamicScene with endpoint_trajectories. See Animate a scene.