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

Execute and modify the notebook online here.

Implementing Channels

Wireless propagation channels are a core concept in the physical layer modeling of communication and sensing systems. In essence, they describe the behaviour of electromagnetic waves during their propagation between devices capable of transmitting electromagnetic radiation, receiving electromagnetic radation, or both. Within Hermes’ API, channels are addressed by the Channel Module, with each implemented channel model inheriting from a common Channel base class.

Adding a new channel model to the set of provided implementations is rather straightfoward: On the most fundamental level, each channel model class is expected to provide an implementation of the abstract _realize method generating a realization as well as a recall_realization implementation loading a realization object from a HDF serialization. Channel realizations represent a channels large-scale random state and are considered immutable during the simulation of a single parameter combination within a parameter sweep.

However, within simulating a single parameter combination, the channel may evolve over time and space, for example because the terminals move. For this reason, the actual channel is generated by “sampleing” a realization given the locations of a transmitting and receiving device, as well as a global time of signal propagation. The result is represented by a ChannelSample over which electromagnetic signals may be propagated in between the transmitting and receiving device.

To demonstrate the API workflow, we will implement a basic channel only introducing a random phase shift to the propagated waveform, no time of flight delays or multiple antenna scenarios are considered:

from __future__ import annotations
from typing import Set

import numpy as np
from h5py import Group

from hermespy.core import ChannelStateInformation, ChannelStateFormat, HDFSerializable, Signal
from import Channel, ChannelRealization, ChannelSample, ChannelSampleHook, InterpolationMode, LinkState
from import ConsistentUniform, ConsistentGenerator, ConsistentRealization

class PhaseShiftChannelSample(ChannelSample):

    def __init__(
        phase_shift: float,
        gain: float,
        state: LinkState,
    ) -> None:
        ChannelSample.__init__(self, state)
        self.__phase_shift = phase_shift
        self.__gain = gain

    def phase_shift(self) -> float:
        return self.__phase_shift

    def gain(self) -> float:
        return self.__gain

    def _propagate(
        signal: Signal,
        interpolation: InterpolationMode,
     ) -> Signal:

        shifted_samples = signal[:, :] * np.exp(1j * self.__phase_shift)
        return signal.from_ndarray(shifted_samples)

    def state(
        num_samples: int,
        max_num_taps: int,
        interpolation_mode: InterpolationMode = InterpolationMode.NEAREST,
    ) -> ChannelStateInformation:
        state = self.__gain**.5 * np.exp(1j * self.__phase_shift) * np.ones((self.receiver_state.antennas.num_receive_antennas, self.transmitter_state.antennas.num_transmit_antennas, num_samples, 1), dtype=complex)
        return ChannelStateInformation(ChannelStateFormat.IMPULSE_RESPONSE, state)

class PhaseShiftChannelRealization(ChannelRealization[PhaseShiftChannelSample]):

    def __init__(
        rng: ConsistentRealization,
        phase_shift_variable: ConsistentUniform,
        sample_hooks: Set[ChannelSampleHook[PhaseShiftChannelSample]] | None = None,
        gain: float = 1.0,
    ) -> None:
        ChannelRealization.__init__(self, sample_hooks, gain)
        self.__rng = rng
        self.__phase_shift_variable = phase_shift_variable

    def _sample(self, state: LinkState) -> PhaseShiftChannelSample:
        consistent_sample = self.__rng.sample(state.transmitter.position, state.receiver.position)
        phase_shift = 2 * np.pi * self.__phase_shift_variable.sample(consistent_sample)
        return PhaseShiftChannelSample(phase_shift, self.gain, state)

    def _reciprocal_sample(self, sample: PhaseShiftChannelSample, state: LinkState) -> PhaseShiftChannelSample:
        return PhaseShiftChannelSample(sample.phase_shift, sample.gain, state)

    def to_HDF(self, group: Group) -> None:
        group.attrs['gain'] = self.gain
        self.__rng.to_HDF(HDFSerializable._create_group(group, 'consistent_realization'))

    def From_HDF(
        group: Group,
        phase_shift_variable: ConsistentUniform,
        sample_hooks: Set[ChannelSampleHook[PhaseShiftChannelSample]],
    ) -> PhaseShiftChannelRealization:
        return PhaseShiftChannelRealization(

class PhaseShiftChannel(Channel[PhaseShiftChannelRealization, PhaseShiftChannelSample]):

    def __init__(self, decorrelation_distance: float) -> None:
        self.__decorrelation_distance = decorrelation_distance
        self.__rng = ConsistentGenerator(self)
        self.__phase_shift_variable = self.__rng.uniform()

    def _realize(self) -> PhaseShiftChannelRealization:
        return PhaseShiftChannelRealization(

    def recall_realization(self, group: Group) -> PhaseShiftChannelRealization:
        return PhaseShiftChannelRealization.From_HDF(

We can now plug the newly generated channel model into a simulation scenario evaluating an OFDM waveform with access to ideal channel state information, equalizing the channel by zero forcing:

import matplotlib.pyplot as plt

from hermespy.core import dB, ConsoleMode
from hermespy.simulation import Simulation, OFDMIdealChannelEstimation
from hermespy.modem import BitErrorEvaluator, DuplexModem, ElementType, GridElement, GridResource, SymbolSection, OFDMWaveform, ZeroForcingChannelEqualization

# Create a new Monte Carlo simulation
simulation = Simulation(console_mode=ConsoleMode.SILENT)

# Add a single device, operated by a communication modem
operator = DuplexModem()
operator.device = simulation.new_device()
operator.reference = operator.device

# Configure an OFDM waveform with a frame consisting of a single symbol section
operator.waveform = OFDMWaveform(grid_resources=[GridResource(elements=[GridElement(ElementType.DATA, 1024)])],

# Add channel equalization routine
operator.waveform.channel_equalization = ZeroForcingChannelEqualization()

# Configure our newly implemented channel model
channel = PhaseShiftChannel(decorrelation_distance=10)
simulation.scenario.set_channel(operator.device, operator.device, channel)
operator.waveform.channel_estimation = OFDMIdealChannelEstimation(channel, operator.device, operator.device)

# Configure a parameter sweep over the receiver SNR, effectively simulating an AWGN channel
simulation.new_dimension('noise_level', dB(0, 2, 4, 8, 16, 24), operator.device)

# Evaluate the BER
simulation.add_evaluator(BitErrorEvaluator(operator, operator))

# Configure the number of Monte Carlo samples per SNR point
simulation.num_samples = 1000

# Run the simulation and plot the results
result =

The channel’s effect on the communication performance can be higlighted by disabling the zero-forcing channel equalization routine for the configured OFDM waveform.

In this case, the communication bit error rate should roughly approximate \(\tfrac{1}{2}\), indicating that no information is exchanged and the bits are essentially random at the receiver.

from hermespy.modem import ChannelEqualization

# Disable channel equalization by replacing the ZF routine with the default stub
operator.waveform.channel_equalization = ChannelEqualization()

# Run the simulation and plot the results
result =