Note

This static document was automatically created from the output of a jupyter notebook.

Execute and modify the notebook online here.

Using Beamformers

This Jupyter notebook provides an in-depth introduction to HermesPy’s antenna array definition and beamforming interfaces. It showcases how to configure MIMO antenna arrays in classic and hybrid configurations, how to specify the antenna element topologies and how to configure custom antenna models within the array. Furthermore, operating conventional delay and sum beamformers on the ….

Consider a classical fully digitally beamformed transmitting antenna array consisting of a planar grid of \(4 \times 4 = 16\) atennna elements located in the x-y plane of a cartesian coordinate system. From a hardware point of view, this means a dedicated digital-analog converter will feed, over its respective radio-frequency chain, each antenna element, respectively. Within Hermes, this concept is realised by AntennaPorts, which can be connected to a single antenna element or to multiple antenna elements.

A digital antenna array as assumed above can described in Hermes’ API by

[14]:
from itertools import product

import numpy as np
from scipy.constants import speed_of_light

from hermespy.core import AntennaMode, Transformation
from hermespy.simulation import SimulatedAntennaPort, SimulatedIdealAntenna, SimulatedCustomArray

sampling_rate = 1e9
carrier_frequency = 70e9
wavelength = speed_of_light / carrier_frequency

transmit_ports = [
    SimulatedAntennaPort(
        [SimulatedIdealAntenna(AntennaMode.TX)],
        Transformation.From_Translation(np.array([.5 * wavelength * x, .5 * wavelength * y, 0]))
    ) for x, y in product(range(4), range(4))
]

uniform_array = SimulatedCustomArray(transmit_ports)

which generates an antenna array consisting of ideal istropic antenna elements spaced at half-wavelength intervals, each antenna connected to a single feeding antenna port. This topology can be visualized by calling

[15]:
# Visualize the antenna array element positions in the array's local coordinate system
_ = uniform_array.plot_topology()

Of course, we are not limited to uniform linear arrays, but can also define arbitrary antenna element topologies, such as a spiral configurations:

[16]:
v = .1 * wavelength
w =  200 * wavelength
c = .4 * wavelength

spiral_ports = [
    SimulatedAntennaPort(
        [SimulatedIdealAntenna(AntennaMode.TX)],
        Transformation.From_Translation(
            np.array([(v*t + c) * np.cos(w * t), (v*t + c) * np.sin(w * t), 0]
        ))
    ) for t in range(16)
]

spiral_array = SimulatedCustomArray(spiral_ports)
_ = spiral_array.plot_topology()

Let’s investigate how the antenna array’s radiation pattern changes with respect to the assumed antenna element topology. For this, we will initialize a new simulation environment and assign our custom antenna array to a device representing a transmitting base station / access point:

[17]:
from scipy.constants import pi

from hermespy.beamforming import BeamformingTransmitter, ConventionalBeamformer, SphericalFocus
from hermespy.core import ConsoleMode, Signal
from hermespy.simulation import Simulation

# Initialize a new simulation
simulation = Simulation(console_mode=ConsoleMode.SILENT, seed=42)

# Create a new device and assign it the antenna array
base_station_device = simulation.new_device(
    antennas=uniform_array,
    carrier_frequency=carrier_frequency,
    pose=Transformation.From_Translation(np.array([0, 0, 0])),
)

To probe the charcteristics, we assume a conventional delay and sum beamformer transmitting a sinusoidal probing signal over the antenna array.

[18]:
# Configure the device to transmit a beamformed sinusoidal probing signal
beamformer = ConventionalBeamformer()
sinusoid = Signal.Create(np.outer(np.ones(1), np.exp(2j * pi * .25 * np.arange(100))), sampling_rate, carrier_frequency)
base_station_transmitter = BeamformingTransmitter(sinusoid, beamformer)
base_station_transmitter.device = base_station_device

We may now visualize the array’s radiation pattern by specifiying the beamformer’s transmitting focus point and calling the plot_pattern method.

[19]:
# Focus spherical coordinates -.75pi, .3pi
beamformer.transmit_focus = SphericalFocus(-.75 * pi, .3 * pi)
uniform_array.plot_pattern(carrier_frequency, beamformer)
[19]:
../_images/notebooks_beamforming_usage_12_0.png
[20]:
# Focus spherical coordinates 0, 0, i.e. along the z-axis
beamformer.transmit_focus = SphericalFocus(0, 0)
uniform_array.plot_pattern(carrier_frequency, beamformer)
[20]:
../_images/notebooks_beamforming_usage_13_0.png

Let’s extend our simulation to evaluate our custom base station’s performance in a urban communication scenario. For this purpose, we will add a device representing user equipment to be illuminated by the base station, located somewhere in the positive x-y-z quadrant of the base station’s local cartesian coordinate system.

[21]:
from hermespy.core import SignalReceiver

# Create a new simulated device representing user equipment
user_equipment_device = simulation.new_device(
    carrier_frequency=carrier_frequency,
    pose=Transformation.From_Translation(np.array([50., 50., 100.])),
)

# Configure the user equipment to simply record 120 samples of any signal impinging on it
user_equipment_receiver = SignalReceiver(120, sampling_rate, sinusoid.power[0])
user_equipment_receiver.device = user_equipment_device

We can instruct the base station to always steer its main lobe towards the user equipment by changing the beamformer’s transmit focus point to a device focus object referencing the user equipment:

[22]:
from hermespy.beamforming import DeviceFocus

# Focus the base station's main lobe on the user equipment
beamformer.transmit_focus = DeviceFocus(user_equipment_device)

# Visualize the beam pattern
uniform_array.plot_pattern(carrier_frequency, beamformer)
[22]:
../_images/notebooks_beamforming_usage_17_0.png

The propagation characeristics between base station and terminal are to be modeled by 3GPP’s 3D urban microcell model (street canyon):

[23]:
from hermespy.channel import UrbanMicrocells, O2IState

# Configure a line of sight street canyon channel between base station and terminal
simulation.set_channel(
    base_station_device,
    user_equipment_device,
    UrbanMicrocells(expected_state=O2IState.LOS),
)

All of the individual parts of a physical layer simulation within Hermes are now in place. Before launching a full-scale Monte-Carlo campaign which might run for several hours we can generate single drops in order to investigate whether the signals received by the user equipment are in line with our expectations.

[24]:
off_target_focus = SphericalFocus(-.75 * pi, .3 * pi)
on_target_focus = DeviceFocus(user_equipment_device)

beamformer.transmit_focus = off_target_focus
_ = simulation.scenario.drop()
_ = user_equipment_receiver.reception.signal.plot(title='Off Target Reception')

beamformer.transmit_focus = on_target_focus
_ = simulation.scenario.drop()
_ = user_equipment_receiver.reception.signal.plot(title='On Target Reception')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[24], line 5
      2 on_target_focus = DeviceFocus(user_equipment_device)
      4 beamformer.transmit_focus = off_target_focus
----> 5 _ = simulation.scenario.drop()
      6 _ = user_equipment_receiver.reception.signal.plot(title='Off Target Reception')
      8 beamformer.transmit_focus = on_target_focus

File D:\repositories\hermespy\hermespy\core\scenario.py:906, in Scenario.drop(self)
    902     _ = self.receive_operators(drop.operator_inputs, cache=True)
    904 else:
    905     # Generate a new drop
--> 906     drop = self._drop()
    908     # Serialize the drop to HDF if in record mode
    909     if self.mode == ScenarioMode.RECORD:

File D:\repositories\hermespy\hermespy\simulation\scenario.py:704, in SimulationScenario._drop(self)
    701 device_transmissions = self.transmit_devices()
    703 # Simulate channel propagation
--> 704 channel_propagations, channel_realizations = self.propagate(device_transmissions)
    706 # Process receptions
    707 trigger_realizations = [t.trigger_realization for t in device_transmissions]

File D:\repositories\hermespy\hermespy\simulation\scenario.py:517, in SimulationScenario.propagate(self, transmissions)
    512 alpha_propagation = alpha_beta_sample.propagate(
    513     transmissions[device_alpha_idx], InterpolationMode.NEAREST,
    514 )
    516 # Propagate signal emitted from device beta to device alpha over the linking channel
--> 517 beta_propagation = beta_alpha_sample.propagate(
    518     transmissions[device_beta_idx], InterpolationMode.NEAREST,
    519 )
    521 # Store propagtions in their respective coordinates within the propagation matrix
    522 propagation_matrix[device_alpha_idx, device_beta_idx] = beta_propagation

File D:\repositories\hermespy\hermespy\channel\channel.py:383, in ChannelSample.propagate(self, signal, interpolation_mode)
    381 # Assert that the signal's number of streams matches the number of antennas of the transmitter
    382 if _signal.num_streams != self.num_transmit_antennas:
--> 383     raise ValueError(
    384         f"Number of signal streams to be propagated does not match the number of transmitter antennas ({_signal.num_streams} != {self.num_transmit_antennas}))"
    385     )
    387 # Propagate signal
    388 propagated_signal = self._propagate(_signal, interpolation_mode)

ValueError: Number of signal streams to be propagated does not match the number of transmitter antennas (16 != 1))

The signal plots visualizing the user equipment’s reception confirm that the base station beamforming works as intended. When focusing the beamformer away from the base station, the received signal power drops significantly when compared to the case where the beamformer is focused towards the user equipment.

These plots only represent a single data point of an evaluation. We can instruct HermesPy to conduct a simulation campaign evaluating the expected received signal power at the user equipment by adding the respective evaluator to the simulation.

[ ]:
from hermespy.core import ReceivedPowerEvaluator

# Add a signal power evaluation to the simulation
simulation.add_evaluator(ReceivedPowerEvaluator(user_equipment_receiver))

The simulation can be instructed to dynamically switch the beamforming focus from our on-target angles to our off-target angles and collect received power estimates for each configuration by adding a simulation grid dimension configuring the beamformer’s respective attribute:

[ ]:
from hermespy.core import dB, SamplePoint

# Configure a simulation dimension varying the receive SNR
snr_dimension = simulation.new_dimension(
    'noise_level',
    dB([0, 2, 4, 8, 16, 32]),
    user_equipment_device,
)

# Configure a simulation dimension switching the beam focus
focus_dimension = simulation.new_dimension(
    'transmit_focus',
    [
        SamplePoint(off_target_focus, 'Off-Target'),
        SamplePoint(on_target_focus, 'On-Target'),
    ],
    beamformer,
    title='Beam Focus',
)

# Collect 100 drops per parameter combination
simulation.num_drops = 100

# Run the simulation campaign and plot the results
result = simulation.run()
_ = result.plot()
../_images/notebooks_beamforming_usage_25_0.png

As expected, we confirmed that our beamformer will increase the power received at the user equipment by three orders of magnitude when focusing the equipment when compared to the selected off-focus angle.

As a reminder, up to now we assumed a completely digitally steered antenna array. How about hybrid arrays? In this case multiple antenna elements will be connected to a single antenna port:

[ ]:
# Generate a port topology that is digitally steered in the
# x domain and has multiple antennas fed by the same port in y domain
hybrid_ports = [
    SimulatedAntennaPort(
        [SimulatedIdealAntenna(AntennaMode.TX, Transformation.From_Translation(np.array([0, .5 * wavelength * y, 0]))) for y in range(4)],
        Transformation.From_Translation(np.array([.5 * wavelength * x, 0, 0]))
    ) for x in range(4)
]

# Assign our new toplogy to the base station
hybrid_array = SimulatedCustomArray(hybrid_ports)
base_station_device.antennas = hybrid_array

Visually, the hybrid array’s beamforming pattern looks almost identical to the fully digitally steered array’s:

[ ]:
# Visualize the array's beamforming pattern when focusing the user equipment
beamformer.transmit_focus = on_target_focus
_ = hybrid_array.plot_pattern(carrier_frequency, beamformer)
../_images/notebooks_beamforming_usage_29_0.png

Let’s alter our simulation to further investigate the pros and cons of connecting multiple antennas to a single port. For this purpose, we’ll drop the SNR dimension, and instead change the antenna array model during simulation runtime to estimate the difference in the received signal power at the user equipment for each array configuration:

[ ]:
# Remove the SNR dimension from the simulation parameter sweep grid
simulation.remove_dimension(snr_dimension)

# Add a dimension switching between our the two antenna array candidates
simulation.new_dimension(
    'antennas',
    [
        SamplePoint(uniform_array, 'Uniform'),
        SamplePoint(hybrid_array, 'Hybrid'),
    ],
    base_station_device,
    title='Array',
)

simulation.num_drops = 200
array_comparison = simulation.run()
array_comparison.print()
┏━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━┓
┃ Beam Focus  Array    RxPwr  ┃
┡━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━┩
│ Off-Target  Uniform │ 2.8 dB │
│ Off-Target  Hybrid  │ 7.4 dB │
│ On-Target   Uniform │ 50 dB  │
│ On-Target   Hybrid  │ 39 dB  │
└────────────┴─────────┴────────┘

In summary, the result shows that a uniform digitally steered array will have higher beam gain when illuminating the user equipment and cause less interference when illuminating a different target than the hybrid array.