Note
This static document was automatically created from the output of a jupyter notebook.
Execute and modify the notebook online here.
Implementing Precodings¶
Symbol Precoders are Hermes’ abstraction for operations on time-domain series of complex numbers representing communication symbols. Within the signal processing chain for communication, modeled by the Modem module, Symbols are considered in between the mapping of bit streams to symbols and their modulation to base-band signals during transmission, and, inversely, in between the demodulation of base-band signals to symbols and their following unmapping to bit streams.
At this stage, users may configure custom operations on the symbol series for any coherent modulation scheme. This tutorial will demonstrate the programming interface by implementing a symbol precoding which only reverses the modulated symbols in time-domain. We don’t expect any performance gain or loss from this operation, the only purpose of this exercise is to get new users and developers accustomed to the specific interface.
Let’s jump right into it and implement the prosed precoding as a class titled SymbolFlipper, which inherits from the base Symbol Precoders, common to all symbol precoding routines.
[2]:
import numpy as np
from hermespy.modem import StatedSymbols, TransmitSymbolEncoder, ReceiveSymbolDecoder
class SymbolFlipper(TransmitSymbolEncoder, ReceiveSymbolDecoder):
def encode_symbols(self, symbols: StatedSymbols, num_output_streams: int) -> StatedSymbols:
encoded_symbols = symbols.copy()
encoded_symbols.raw = np.flip(encoded_symbols.raw, axis=1)
return encoded_symbols
def decode_symbols(self, symbols: StatedSymbols, num_output_streams: int) -> StatedSymbols:
decoded_symbols = symbols.copy()
decoded_symbols.raw = np.flip(decoded_symbols.raw, axis=1)
decoded_symbols.states = np.flip(decoded_symbols.states, axis=2)
return decoded_symbols
@property
def num_transmit_input_symbols(self) -> int:
return 1
@property
def num_transmit_output_symbols(self) -> int:
return 1
@property
def num_receive_input_symbols(self) -> int:
return 1
@property
def num_receive_output_symbols(self) -> int:
return 1
def _num_transmit_input_streams(self, num_output_streams: int) -> int:
return num_output_streams
def num_receive_output_streams(self, num_input_streams: int) -> int:
return num_input_streams
Symbol Precoders expect their two abstract methods encode and decode to be defined. As their names already hint, the encode routine will be called during data transmission and perform an operation on an incoming stream of StatedSymbols, the decoding routine will be called during data reception and is expected to reverse the effects of its encoding counterpart. Additionally, some precodings might alter the number of antenna streams during their coding operations, so two additional properties num_input_streams and num_output_streams must be specified.
StatedSymbols are an extension of Symbols and carry ChannelStateInformation as additional information next to the raw symbol stream. This is required since some symbol precodings might rely on a channel estimate for effective precoding. Both the raw symbol stream and channel state information are essentially numpy arrays of specific dimensions. The raw symbol array has three dimensions, representing antenna streams, words and symbols, respectively, while the channel state information has four dimensions, representing output antennas, input antennas, words and symbols, respectively.
[3]:
import matplotlib.pyplot as plt
from hermespy.core import ConsoleMode, dB
from hermespy.simulation import Simulation
from hermespy.modem import BitErrorEvaluator, DuplexModem, ElementType, GridElement, GridResource, SymbolSection, OFDMWaveform
# Create a new Monte Carlo simulation
simulation = Simulation(console_mode=ConsoleMode.SILENT)
# Add a single device, operated by a communication modem
device = simulation.new_device()
operator = DuplexModem()
device.add_dsp(operator)
# Configure an OFDM waveform with a frame consisting of a single symbol section
operator.waveform = OFDMWaveform(grid_resources=[GridResource(elements=[GridElement(ElementType.DATA, 1024)])],
grid_structure=[SymbolSection(pattern=[0])])
# Configure our newly implemented symbol precoding
operator.transmit_symbol_coding[0] = SymbolFlipper()
operator.receive_symbol_coding[0] = SymbolFlipper()
# Configure a parameter sweep over the receiver SNR, effectively simulating an AWGN channel
simulation.new_dimension('noise_level', dB(0, 2, 4, 8, 12, 16, 20), device)
# Evaluate the BER
simulation.add_evaluator(BitErrorEvaluator(operator, operator))
# Run the simulation and plot the results
result = simulation.run()
_ = result.plot()
plt.show()
To highlight that the implemented precoder has no effect on the communication performance, we can deactivate it and re-run the simulation:
[4]:
_ = operator.transmit_symbol_coding.pop_precoder(0)
result = simulation.run()
_ = result.plot()
plt.show()