Getting Started#

Assuming HermesPy is properly installed within the currently selected Python environment, users may define custom wireless communication scenarios to be investigated within the context of Simulations or Hardware Loops. The whole HermesPy suite can either be directly integrated into custom software projects and operated as a plug-in library via a detailed object-oriented programming interface or configured by YAML-style configuration files and launched from any system command line.

This section provides a rough description of the HermesPy software architecture and gives an introduction into both the library and command line interface in order to get new users quickly accustomed.

HermesPy Architecture#

In its core, the HermesPy API aims to abstract the process of wireless signal processing within a strictly object-oriented class structure. Each processing step is represented by a dedicated class and can be adapted and customized by the software user.

Consider a typically heterogeneous wireless scenario featuring multiple entities transmitting and receiving electromagnetic waveforms. The physical description of said entities is referred to as a Device in Hermes. Devices provide general information required for the modeling of electromagnetic propagation, such as carrier frequency, spatial position, orientation, number of available antennas and the respective antenna array topology. Now, while Devices describe the physical properties, the digital signal processing required for generating waveform transmissions and receptions is modeled by Transmitters and Receivers, respectively. They form HermesPy’s general abstraction for digital signal processing applied before digital-to-analog conversion and after analog-to-digital conversion, respectively:

%%{init: {"flowchart":{"useMaxWidth": false}}}%% graph LR subgraph dspi [DSP Transmit Layer] oai[Transmitter A] obi[Transmitter B] end subgraph phys [Physical Layer] dai[Device A] dbi[Device B] dao[Device A] dbo[Device B] end subgraph dspo [DSP Receive Layer] oao[Receiver A] obo[Receiver B] end oai --> dai obi --> dbi dai --> dao dai --> dbo dbi --> dbo dbi --> dao dao --> oao dbo --> obo click oai "api/core.device.Transmitter.html" "Transmitter" click obi "api/core.device.Transmitter.html" "Transmitter" click dai "api/core.device.Device.html" "Device" click dbi "api/core.device.Device.html" "Device" click dao "api/core.device.Device.html" "Device" click dbo "api/core.device.Device.html" "Device" click oao "api/core.device.Receiver.html" "Receiver" click obo "api/core.device.Receiver.html" "Receiver"

A typical information flow consists of a Transmitter generating a base-band waveform, submitting it to its assigned Device, followed by the Device emitting the submitted transmission in RF-band, while simultaneously recording impinging broadcasts. The recorded broadcasts are submitted to the assigend Receivers to be processed.

There are two types of devices, namely Simulated and Physical, which both inherit from the abstract Device base:

classDiagram class Device { <<Abstract>> +transmit() : DeviceTransmisison* +receive() : DeviceReception* } class PhysicalDevice { <<Abstract>> +transmit() : PhyiscalDeviceTransmisison +receive() : PhysicalDeviceReception } class SimulatedDevice { +transmit() : SimulatedDeviceTransmisison +receive() : SimulatedDeviceReception } class Transmitter { +transmit() : Transmission } class Receiver { +receive() : Reception } PhysicalDevice ..|> Device SimulatedDevice ..|> Device Device *--* Transmitter Device *--* Receiver link Device "api/core.device.Device.html" "Device" link PhysicalDevice "api/hardware_loop.physical_device.PhysicalDevice.html" "Physical Device" link SimulatedDevice "api/simulation.simulated_device.SimulatedDevice.html" "Simulated Device" link Transmitter "api/core.device.Transmitter.html" link Receiver "api/core.device.Receiver.html"

Depending on which Device realization is selected, Hermes acts as either a physical layer simulation platform or a hardware testbed, with the advantage that implemented signal processing algorithms, which are simply classes inheriting from either Transmitter, Receiver, or both, integrate seamlessly into both simulation and hardware testbed setups over a unifying API without the need for any code modifications. Three types of signal processing pipelines are currently provided by Hermes out of the box and shipped in individual namespace packages:

  • Modems provide the typical physical layer signal processing pipeline for digital communication systems, including mapping, modulation, forward error corretion, precoding / beamforming, synchronization, channel estimation and channel equalization.

  • Radars provide the typical physical layer signal processing pipeline for sensing systems, including beamforming and target detection.

  • JCAS is a combination of both, providing a physical layer signal processing pipeline for joint communication and sensing systems.

The following subsections will introduce how to set up simulations and run hardware testbed setups.

Simulations#

Simulation campaigns are defined by a set of SimulatedDevices interconnected by Channel Models, with the combination of both forming a SimulationScenario. Therefore, considering channel reciprocity, a SimulationScenario featuring \(D\) devices requires the specification of \(\tfrac{D(D+1)}{2}\) Channel Models. Considering a model featuring \(D=2\) dedicated devices, the following simulated physical layer model is formed:

%%{init: {"flowchart":{"useMaxWidth": false, "curve": "linear"}}}%% graph LR subgraph phys [Simulated Physical Layer] direction LR dai[SimulatedDevice A] dbi[SimulatedDevice B] caa{{Channel A,A}} cab{{Channel A,B}} cba{{Channel B,A}} cbb{{Channel B,B}} dao[SimulatedDevice A] dbo[SimulatedDevice B] end dai -.-> caa -.-> dao dai -.-> cab -.-> dbo dbi -.-> cbb -.-> dbo dbi -.-> cba -.-> dao click dai "api/simulation.simulated_device.SimulatedDevice.html" "Simulated Device" click dbi "api/simulation.simulated_device.SimulatedDevice.html" "Simulated Device" click dao "api/simulation.simulated_device.SimulatedDevice.html" "Simulated Device" click dbo "api/simulation.simulated_device.SimulatedDevice.html" "Simulated Device" click caa "api/channel.channel.Channel.html" "Channel Model" click cab "api/channel.channel.Channel.html" "Channel Model" click cba "api/channel.channel.Channel.html" "Channel Model" click cbb "api/channel.channel.Channel.html" "Channel Model"

Each device is linked to two channel instances. Note that, even though four channels are depicted, channel B, A links to the same channel instance as A, B due to the reciprocity assumption, leading to a total of \(\tfrac{2(2+1)}{2}=3\) unique channel instances for the depicted scenario. Initializing said scenario is as simple as creating a new simulation instance and adding two devices:

1# Create a new simulation
2simulation = Simulation()
3
4# Add two dedicated devices to the simulation
5tx_device = simulation.new_device()
6rx_device = simulation.new_device()

When adding new devices to a simulation, the simulation will automatically intialize the required channel instances as IdealChannels. However, the user may freely select from a multitude of different channel models, which are provided by the Channel package. For example, the following snippet configures the depicted scenario with a 5G Multipath Fading Channel:

1# Specifiy the channel instance linking the two devices
2simulation.set_channel(tx_device, rx_device, MultipathFading5GTDL())

While SimulatedDevices and Channel Models form the physical layer description of a simulation, the signal processing, i.e. the transmit and receive layer, generates the waveforms that will actually be generated by devices and transmitted over the channels. For communication cases in which we want to declare one device as the sole transmitter and one device as the sole receiver, HermesPy offers the SimplexLink class, which automatically configures the transmit and receive layer of the devices:

1# Define a simplex communication link between the two devices
2link = SimplexLink(tx_device, rx_device)

The Modem package provides a range of communication waveform implementations, for this minimal introduction we will choose a Root-Raised-Cosine single carrier waveform:

1# Configure the waveform to be transmitted over the link
2link.waveform = RootRaisedCosineWaveform(symbol_rate=1e6, oversampling_factor=8,
3                                         num_preamble_symbols=10, num_data_symbols=100,
4                                         roll_off=.9)

We may now already directly call the SimplexLink’s transmit and receive rountines to directly generate, process and visualize the generated information such as base-band waveforms and symbol constellations:

1# Generate and visualize a communication waveform transmitted over the link
2transmission = link.transmit()
3transmission.signal.plot()
4
5# Receive the transmission at rx_device
6reception = link.receive(transmission.signal)
7reception.symbols.plot_constellation()

This will bypass physical layer simulations including device and channel models and directly receive the transmitted waveform, resulting in perfect information recovery. Refering back to the intial architecture graph, we patched the transmit layer directly into the receive layer.

During simulations, however, the full physical layer is considered. HermesPy is drop-based, meaning with each call of the Simulation’s drop method, new realizations of the configured channel models are generated, the transmit routines of all Transmitters are called, the generated waveforms are propagated over the configured channels and the receive routines of all Receivers. The generated information is collected in SimulatedDrops to be accessed by the user:

1# Generate and plot a single simulation drop
2drop = simulation.scenario.drop()
3drop.device_transmissions[0].operator_transmissions[0].signal.plot()
4drop.device_receptions[1].operator_receptions[0].equalized_symbols.plot_constellation()

After the generation of a new SimulatedDrop, Evaluators may be used to conveniently extract performance information. For instance, the bit error rate of the generated drop may be extracted by a BitErrorEvaluator:

1# Add a bit error rate evaluation to the simulation
2ber = BitErrorEvaluator(link, link)
3ber.evaluate().plot()

This is the core routine of a typical Monte Carlo simulation, which is usually conducted over a grid of parameter values. For each parameter combination, a new SimulatedDrop is generated and evaluated. This process is executed multiple times in parallel, depending on the number of available CPU cores and the user’s configuration. Finally, the generated evaluations are concatenated towards a single result.

%%{init: {"flowchart":{"useMaxWidth": false}}}%% flowchart TD pla[Simulated Physical Layer] plb[Simulated Physical Layer] c{Simulation} plc[Simulated Physical Layer] pla --> |Evaluations| c c --> |Parameters| pla plb --> |Evaluations| c c --> |Parameters| plb plc --> |Evaluations| c c --> |Parameters| plc

A simulation iterating over the receiving device’s signal to noise ratio as parameters and estimating the respective bit error RootRaisedCosine can be launched by executing

1# Iterate over the receiving device's SNR and estimate the respective bit error rates
2simulation.new_dimension('snr', dB(20, 16, 12, 8, 4, 0), rx_device)
3simulation.add_evaluator(ber)
4
5result = simulation.run()
6result.plot()

which will result in a rendered plot being generated. The full code snippet implementing the above introduction can be downloaded from GitHub - Getting Started Simulation. For more complex simulation examples and instructions on how to integrate and evaluate your own signal processing algorithms in HermesPy, please refer to the Tutorials section.

Hardware Loop#

Hardware Loops allow for the execution and measurement collection of signal processing algorithms in the transmit and receive processing layer on real hardware. They are defined by a set of PhysicalDevices forming a PhysicalScenario, which represents the physical layer description of the hardware loop.

%%{init: {"flowchart":{"useMaxWidth": false}}}%% graph LR subgraph phys [Hardware Loop Physical Layer] direction LR dai[PhysicalDevice A] dbi[PhysicalDevice B] world((Real World)) dao[PhysicalDevice A] dbo[PhysicalDevice B] end dai -.-> world -.-> dao dai -.-> world -.-> dbo dbi -.-> world -.-> dbo dbi -.-> world -.-> dao click dai "api/hardware_loop.physical_device.PhysicalDevice.html" "Physical Device" click dbi "api/hardware_loop.physical_device.PhysicalDevice.html" "Physical Device" click dao "api/hardware_loop.physical_device.PhysicalDevice.html" "Physical Device" click dbo "api/hardware_loop.physical_device.PhysicalDevice.html" "Physical Device"

When compared to simulations, Hardware Loops obvisouly lack channel and hardware modeling capabilities. Instead, each trigger of a PhysicalDevice will generate a transmission and transmit the respective base-band samples over the air. In other words, the simulated device and channel models have been replaced by the real world.

Setting up a Hardware Loop is as simple as creating a new PhysicalScenario and passing it to a new HardwareLoop instance:

1# Create a new hardware loop
2hardware_scenario = PhysicalScenarioDummy(seed=42)
3hardware_loop = HardwareLoop[PhysicalScenarioDummy, PhysicalDeviceDummy](hardware_scenario)
4
5# Add two dedicated devices to the hardware loop, this could be, for example, two USRPs
6tx_device = hardware_loop.new_device(carrier_frequency=1e9)
7rx_device = hardware_loop.new_device(carrier_frequency=1e9)

The PhysicalScenarioDummy is a physical scenario implementation intended for testing and demonstration purposes and does not require real hardware. Instead, the PhysicalDeviceDummies instances behave identical to SimulatedDevices. For this reason, we can also assign channel models to the managing PhysicalScenarioDummy instances:

1# Specifiy the channel instance linking the two devices
2# Only available for PhysicalScenarioDummy, which is a simulation of hardware behaviour
3hardware_scenario.set_channel(tx_device, rx_device, MultipathFading5GTDL())

This is, of course, not possible in real hardware scenarios such as USRP Systems featuring USRP Devices or Audio Scenarios featuring Audio Devices.

For communication cases in which we want to declare one device as the sole transmitter and one device as the sole receiver, HermesPy offers the SimplexLink class, which automatically configures the transmit and receive layer of the devices:

1# Define a simplex communication link between the two devices
2link = SimplexLink(tx_device, rx_device)

The Modem package provides a range of communication waveform implementations, for this minimal introduction we will choose a Root-Raised-Cosine single carrier waveform:

1# Configure the waveform to be transmitted over the link
2link.waveform = RootRaisedCosineWaveform(symbol_rate=1e6, oversampling_factor=8,
3                                         num_preamble_symbols=10, num_data_symbols=100,
4                                         roll_off=.9)
5link.waveform.channel_estimation = SingleCarrierLeastSquaresChannelEstimation()
6link.waveform.channel_equalization = SingleCarrierZeroForcingChannelEqualization()

Just like the simulation pipeline, the hardware loop runtime will generate drops to be evaluated. However, instead of multiple drops being generated in parallel, the hardware loop’s drop generation is performed sequentially by triggering the configured PhysicalDevices.

After the generation of a new Drop, Evaluators may be used to conveniently extract performance information. For instance, the bit error rate of the generated drop may be extracted by a BitErrorEvaluator:

1# Add a bit error rate evaluation to the hardware loop
2ber = BitErrorEvaluator(link, link)
3hardware_loop.add_evaluator(ber)

Working with real hardware usually requires a lot of oversight and debugging, so the HardwareLoop features a visualization interface which will render plots of required information in real-time:

1# Add some runtime visualizations to the hardware loop
2hardware_loop.add_plot(DeviceTransmissionPlot(tx_device, 'Tx Signal'))
3hardware_loop.add_plot(DeviceReceptionPlot(rx_device, 'Rx Signal'))
4hardware_loop.add_plot(ReceivedConstellationPlot(link, 'Rx Constellation'))

The plots will be updated with each new Drop. An overview of existing visualization routines can be found in Visualizers.

Identically to the simulation pipeline, the HardwareLoop can be configured to iterate over a grid of parameter values and generate a fixed number of drops per parameter combination:

1# Iterate over the receiving device's SNR and estimate the respective bit error rates
2hardware_loop.new_dimension('carrier_frequency', [1e9, 10e9, 100e9], tx_device, rx_device)
3hardware_loop.num_drops = 10
4
5hardware_loop.results_dir = hardware_loop.default_results_dir()
6hardware_loop.run()

Setting the results_dir parameter will result in a consolidation of all drop data and evaluations into a single drops.h5 file within the respective directory. The data can be accessed by the user for further processing, or even directly replayed by the HardwareLoop:

1# Replay the recorded dataset
2hardware_loop.replay(join(hardware_loop.results_dir, 'drops.h5'))

The full code snippet implementing the above introduction can be downloaded from GitHub - Getting Started Loop. For more complex simulation examples and instructions on how to integrate and evaluate your own signal processing algorithms in HermesPy, please refer to the Tutorials section.