Introduction¶
Before running these notebooks, we recommend running:
pip install torchradio[notebooks]
to ensure you have all of the necessary dependencies.
from pprint import pformat, pprint
from torchradio import Receiver, Transmitter
from torchradio.algorithm import Modem
from torchradio.env import PlanarEnvironment
from torchradio.position import get_null_distribution, get_uniform_distribution
Define an environment to simulate with dimensions 100 x 100. We will assume a planar environment. At initialization, there are no devices in the environment.
env = PlanarEnvironment(x_max=100, y_max=100)
print(f"Bounds: {env.bounds}")
print(f"Devices: {pformat(env.devices)}")
Bounds: Bounds3D(x_max=100, y_max=100, z_max=0)
Devices: {'receivers': {}, 'transmitters': {}}
Let's define some devices for our environment. We will use the pre-built Modem class to make it easier to define devices. Each device needs to be specified with a so-called "Spatial Distribution". For each simulation, we will simulate the device's position according to this distribution.
As an example, the next block defines a simple QPSK transmitter. The transmitter's position is sampled uniformly from a 10 x 20 metre rectangle located at (30, 40).
algorithm = Modem("psk", 4).tx # combine modem with Modem class and isolate the
spatial_distribution = get_uniform_distribution([25, 35], [30, 50], [0, 0])
qpsk_transmitter = Transmitter(algorithm, spatial_distribution)
print(qpsk_transmitter)
<torchradio.device.Transmitter object at 0x7f6df4102c90>
Note that a Transmitter can be defined with a max_gain parameter. Any transmissions that violate this gain will saturate the transmission.
Calling device.place() will randomly sample a new position for the device according to its spatial distribution
for i in range(5):
print(f"Placement {i}: {qpsk_transmitter.place()}")
Placement 0: Position(x=25.208818, y=38.12274, z=0.0) Placement 1: Position(x=28.343914, y=41.070465, z=0.0) Placement 2: Position(x=29.90762, y=47.56172, z=0.0) Placement 3: Position(x=32.97873, y=42.237152, z=0.0) Placement 4: Position(x=33.996162, y=39.393585, z=0.0)
We can use get_null_distribution if we want a device to be pinned to a single location.
spatial_distribution = get_null_distribution(30, 40)
qpsk_transmitter = Transmitter(algorithm, spatial_distribution)
for i in range(5):
print(f"Placement {i}: {qpsk_transmitter.place()}")
Placement 0: Position(x=30, y=40, z=0) Placement 1: Position(x=30, y=40, z=0) Placement 2: Position(x=30, y=40, z=0) Placement 3: Position(x=30, y=40, z=0) Placement 4: Position(x=30, y=40, z=0)
To create an interesting simulation environment with multiple transmitters and receivers, we first create dictionaries to house the device definitions. Note that the current use of Modem with overlapping centre frequencies and constellations will lead to incoherent receiver outputs. We will look at using more sensible algorithms later on.
transmitters = {
"tx_1": Transmitter(Modem("psk", 4).tx, get_null_distribution(10, 10), 2),
"tx_2": Transmitter(Modem("psk", 8).tx, get_null_distribution(20, 10), 3),
"tx_3": Transmitter(Modem("qam", 4).tx, get_null_distribution(50, 80), 8),
"tx_4": Transmitter(Modem("psk", 4).tx, get_null_distribution(20, 70)),
}
receivers = {
"rx_1": Receiver(Modem("psk", 4).rx, get_null_distribution(10, 10)),
"rx_2": Receiver(Modem("psk", 8).rx, get_null_distribution(20, 10)),
"rx_3": Receiver(Modem("qam", 4).rx, get_null_distribution(50, 80)),
}
print(f"Transmitters: {list(transmitters.keys())}")
print(f"Receivers: {list(receivers.keys())}")
Transmitters: ['tx_1', 'tx_2', 'tx_3', 'tx_4'] Receivers: ['rx_1', 'rx_2', 'rx_3']
We can now run place the devices in the environment. Notice the updated output from env.devices.
env.place(transmitters, receivers)
pprint(env.devices)
{'receivers': {'rx_1': Position(x=10, y=10, z=0),
'rx_2': Position(x=20, y=10, z=0),
'rx_3': Position(x=50, y=80, z=0)},
'transmitters': {'tx_1': Position(x=10, y=10, z=0),
'tx_2': Position(x=20, y=10, z=0),
'tx_3': Position(x=50, y=80, z=0),
'tx_4': Position(x=20, y=70, z=0)}}
We can remove devices from the environment by called env.reset()
env.reset()
pprint(env.devices)
{'receivers': {}, 'transmitters': {}}
Let's re-add the devices to the environment and run a simulation! env.simulate takes a single argument n_timesteps that determines how long the simulation will run for.
env.place(transmitters, receivers)
device_logs = env.simulate(100)
The device logs can be used to compute losses and update trainable algorithms. These logs may also be used for analytical purposes to determine the performance characteristics of different algorithms.