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 TrueThe 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.complex64weights 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.