Simulating Modulation Schemes for Wireless Communication - 180D-FW-2023/Knowledge-Base-Wiki GitHub Wiki

Introduction

Wireless communication theory governs many aspects of long-distance and short-distance information transfer. A user may wish to transmit voice, video, or countless other types of data. Data transferred over the air is most often carried by electromagnetic waves, which are subject to noise and distortion. To make such communication more robust, signal data is converted to a digital format and encoded, before being modulated onto an EM wave.

There are many encoding and modulation methods that have been developed to protect the signal as it travels from transmitter to receiver. Different methods come with benefits and trade-offs, so simulations are useful in determining what may be best for a certain scenario. This tutorial will cover the methodology to create a computer simulation of a simple communications channel.

Basic Principles

System Overview

The full communication system can be represented by a combination of multiple smaller systems. An input signal is initially sampled and discretized into a stream of bits. This step is required for analog signals, such as voice, to convert them into a digital data format.

Next, in the encoding phase, some redundant information is added to the data. The purpose of this is to trade-off data throughput to gain a higher chance of error correction and detection at the receiver.

After encoding, the data is modulated onto a carrier wave to be sent over the air. This is necessary to send information at specific frequencies, as is the case for RF communication. The signal then travels through the channel, where it may encounter various forms of distortions. One example is noise, which can be modeled as a random variable that adds to the signal energy and causes information loss. The signal is received at the demodulator, which makes a decision about which bits the wave represents. A decoder then attempts to detect or correct possible decision errors. The majority of this article will focus on the modulation portion.

Modulation

Modulation is a method used in wireless communication to convert digital information into a signal that can be sent over the air. Bits are grouped into symbols, which are unique signals that represent a specific combination of bits. The symbols are agreed upon by the sender and receiver, therefore allowing coherent interpretation of information.

The symbols are represented using a set of basis functions. These basis functions are chosen in such a way that they can be easily distinguished by the receiver; namely, they are orthogonal.

Following is an example of breaking up a string of 8 bits into 4 different symbols. From this, it is known that each symbol will be 8/4 = 2 bits, so there are 2^2 = 4 possible symbols that need to be accounted for.

00110001

00 11 00 01

0 3 0 1

Symbol Bit Representation
0 00
1 01
2 10
3 11

Next, the symbols should be represented as a combination of basis functions. For this example, the basis function psi1 = Acos(wt + phi) can be used with 4 different values of A, the signal energy. This is known as Amplitude Shift Keying (ASK).

Visually, the basis function and symbols can be represented in a constellation graph. For ASK, since there is a single basis function, it lies on a single line on the psi axis.

image

4-ASK

There are multiple other possible selections of basis functions.

  • Phase Shift Keying (PSK): Each symbol uses a summation of two basis functions, 90 degrees out of phase (ex: sine and cosine). The amplitudes of the basis functions are confined to have a constant magnitude, so they lie on a single ring.

image

8-PSK

  • Amplitude Phase Shift Keying (APSK): A combination of PSK and ASK. The basis functions from PSK are used, but no longer with confined amplitudes. The result is that any location on the constellation graph may be used. A special case of this is Quadrature Amplitude Modulation (QAM), which confines the symbols to a rectangular pattern.

  • Frequency Shift Keying (FSK): Multiple basis functions with varying frequency. Frequency spacing can be chosen with spacing to ensure orthogonality, leading to an M-dimensional constellation.

Parameters

The performance of the communication system is quantified using a few figures of merit.

  • M: Modulation order. The number of symbols supported by the modulation scheme.
  • m: Bits per symbol. Related to M and B by m = log2(M)
  • Rs: Symbol rate, in symbols per second.
  • Rb: Bit rate, measured in bits per second.
  • Es: Average energy per symbol.
  • SNR: Signal to noise ratio. Ratio of average energy in a signal to the energy of noise.
  • N0: Noise power.
  • e: Symbol error rate. Probability of classifying a signal incorrectly.

For the coming tutorials, we will focus on the effects of signal to noise ratio and modulation order on the symbol error rate.

Software

NumPy and Matplotlib

Python NumPy will be used for this tutorial, but other software such as MATLAB may be used. NumPy is a Python library mainly used for numerical computing and array operations. This makes it a good choice for signal processing applications. Matplotlib will be used in conjunction with Numpy for visualization.

To begin each tutorial, import Numpy and Matplotlib with

import numpy as np
import matplotlib.pyplot as plt

Basic Tutorial: Modulation with Binary ASK

image

For the first tutorial, Binary Amplitude Shift Keying (B-ASK) will be used to demonstrate the basics of simulation. As a binary modulation, B-ASK uses two symbols, with each symbol representing one bit (“0” or “1”). The constellation is shown above. A red decision boundary is drawn leaving equal area on either side between each symbol.

Creating the Input Signal

To begin, create a variable to store signal energy and set it to 1. Next, create a variable for the signal-to-noise ratio. Initially, set this to 1. A SNR of 1 means there is equal noise energy to signal energy, and is thus expected to result in a large error rate. SNR will be varied later in the tutorial. Noise energy is then calculated from these values.

signal_energy = 1
SNR = 1
noise_energy = signal_energy/SNR

Next, create a test signal using numpy’s random.randint method. This will create an array of zeros and ones of a given sample size. A higher sample size may be used to obtain more accurate results, but will take more time to simulate.

sample_size = 10**5
signal = np.random.randint(0, 2, sample_size)

Modulation

The signal we created will be directly modulated (encoding will be skipped for this tutorial). Create a function that can take the signal array and signal energy as input. We must now map the signal bits (0 or 1) to two symbol amplitudes of equal magnitude and opposite sign (-sqrt(Es) or sqrt(Es)). This simulates the constellation of the B-ASK modulation scheme. For this tutorial, 0 is represented by the negative coefficient, and 1 by the positive. The function should return the modulated signal.

def modulate(signal, signal_energy):
 modulated_signal = 2*(signal - 0.5)*np.sqrt(signal_energy)
 return modulated_signal

Channel

Our next function will simulate the channel. This function should take the modulated signal and noise energy as input. For this tutorial, an Additive White Noise Gaussian (AWGN) channel will be assumed. This means that noise is a Gaussian random variable that adds to the signal in all of its dimensions. Since B-ASK is one-dimensional, a random value can simply be added to the input signal using Numpy random.normal. The variance of this variable is related to the noise energy by VAR = ½*N0. The result is that our signal will be shifted off from its intended value by a random amount, which is the source of error.

def channel(modulated_signal, noise_energy):
 mean = 0
 variance = noise_energy/2
 std = np.sqrt(variance)
 noise = np.random.normal(mean, std, len(signal))

 signal_plus_noise = modulated_signal + noise

 return signal_plus_noise

Demodulation

The demodulator function classifies each signal as 0 or 1 based on the decision boundary. Since there are only two symbols, this is as simple as checking whether the signal is negative or positive. Return an array of bits based on this decision.

def demodulate(signal_plus_noise):
 demodulated_signal = signal_plus_noise.copy()
 demodulated_signal[signal_plus_noise < 0] = 0
 demodulated_signal[signal_plus_noise >= 0] = 1
 return demodulated_signal

Error

Finally, to check the error rate, the above function will compare the output and input signals by subtracting them and averaging the number of ones, which represent the number of mismatches. For non-binary modulation methods, any other counting method may be employed.

def calculate_error(signal, demodulated_signal):
 errors = np.abs(demodulated_signal - signal)
 average_error = np.mean(errors)
 return average_error

Running the Simulation

To calculate the error for our simulation, employ the functions we have created according to the wireless system diagram.

signal -> modulate -> channel -> demodulate -> demodulated_signal -> calculate_error

demodulated_signal = demodulate(channel(modulate(signal, signal_energy), noise_energy))
error = calculate_error(signal, demodulated_signal)

Varying SNR

Now that average error can be calculated, it would be interesting to see how error changes with varying SNR. The following script graphs the average error for different SNR levels.

signal_energy = 1
sample_size = 10**7

SNR_array = np.linspace(0,5,10) # Generate SNRs to graph
error_array = np.zeros(len(SNR_array)) # Array for recording each SNR's error value

for i, SNR in np.ndenumerate(SNR_array): # Iterate through each SNR, recording error in the output signal
  noise_energy = signal_energy/SNR

  signal = np.random.randint(0, 2, sample_size)
  demodulated_signal = demodulate(channel(modulate(signal, signal_energy), noise_energy))
  error = calculate_error(signal, demodulated_signal)

  error_array[i] = error

plt.yscale("log")
plt.scatter(SNR_array, error_array)
plt.ylabel("Symbol Error Rate")
plt.xlabel("SNR")

As expected, high SNR greatly reduces the error rate.

image

Tutorial: Modulation with M-ASK

M-ASK is the generalized scheme to B-ASK (2-ASK) in that its M symbols lie on a one dimensional axis. While each symbol in BASK only represents one bit, M-ASK’s symbols are log2(M) bits each. We will see that the extra information comes at the cost of error rate. Most code from the previous section can be reused aside from some modifications we will make.

First, change the signal array to include samples assuming values of 0, 1, 2, ..., M. We will use 4 to start. You can consider each integer in the signal array as a short form of the bit representation (0 represents 00, 1 represents 01, 2 represents 10, etc.).

M = 4
signal = np.random.randint(0, M, sample_size)

Next, we will edit our modulate function. We need a baseline to compare different order modulations, and a simple way to do this is to standardize the energy between them. In the function definition, add M as an argument. Then, create an array to represent the M possible symbol choices. You may start with just a linearly spaced array of M values centered at 0. Next, normalize the array to the average of its magnitudes squared. This average represents the average energy per symbol. By normalizing our array to its average symbol energy, we are setting its average symbol energy to 1 regardless of modulation order.

To create our modulated signals, index from this array with our signal array. Essentially, we are mapping 0 to the most negative symbol, 1 to the next one, 2 to the next one, etc.

def modulate(signal, signal_energy, M):
 symbols = np.linspace(-1,1,M)
 symbols = symbols/np.sqrt(np.mean(symbols**2))
 modulated_signal = symbols[signal]
 return modulated_signal

No change is necessary to the channel, but we must update the demodulation function. Add M as an argument and use it to find all M possible symbols. Iterate through the signal+noise array and find the index of the closest symbol using np.argmin() on the difference between the signal+noise and symbol. This index is then assigned to the demodulated signal so that it can be compared to the original signal array.

def demodulate(signal_plus_noise, M):
 symbols = np.linspace(-1,1,M)
 symbols = symbols/np.sqrt(np.mean(symbols**2))
 demodulated_signal = signal_plus_noise.copy()
 for i, sig in np.ndenumerate(signal_plus_noise):
   demodulated_signal[i] = np.abs(symbols - sig).argmin()
 return demodulated_signal

Edit the error comparison function to count the number of ones in an error array. The error array equals zero when there is a match and one when there is a mismatch.

def calculate_error(signal, demodulated_signal):
 errors = demodulated_signal != signal
 average_error = np.mean(errors)
 return average_error

You can run this to compare the different symbol error for different modulation orders. It is important to remember that this is the symbol error rate and not bit error rate. Since we are no longer using a binary modulation, these error values are not the same.

Intuitively, with equal average symbol energy, higher modulation orders are “squeezing” more symbols into the same space. This leads to more symbol errors as the order increases.

Conclusion

While this tutorial provides fundamentals for wireless simulations, there are many more layers of complexity that can be added. Modulation type comparison, bit error rate, channel division, and encoding schemes are a few examples of further topics to explore.

References

  • Fundamentals of Wireless Communication by David Tse and Pramod Viswanath