When you are designing a modem, you’ll typically make heavy use of *simulations*. Much of the design work is concentrated on the algorithmic part of the device, especially in these days of *Software-Defined Radio* (*SDR*), where the actual electronics might be mostly off-the-shelf. After deciding on theoretical grounds on a design that should work for a particular application, simulations are *the* way to check things in practice. *If* the simulations are close enough to reality to be relevant, of course.

One of the essential things that you need for your simulations, is *noise*. Noise is inevitable in all practical communications channels, and it often not subtle, so you really need it. This article shows how to add noise to a signal to set the \(E_s/N_0\) to a given value.

\(E_s/N_0\) is a way to specify the *signal-to-noise ratio* (*SNR*) of a digital communications signal. See Why is Eb/N0 the Natural Figure of Merit in Digital Communications? for some context you are not familiar with this. The central equation of that article is

\[\frac{E_b}{N_0}=\frac{S}{N}\frac{B}{R_b}.\]

In the current article, I’m going to use energy per *symbol* \(E_s\) and *symbol* rate \(R_s\) instead of energy per *bit* \(E_b\) and *bit* rate \(R_b\), because that is typically more natural in a simulation setting. The connection between both is easy. If the number of bits per symbol is \(b\), then \(E_s=bE_b\) and \(R_s=R_b/b\). A symbol contains \(b\) bits, so the energy per symbol is \(b\) times higher than the energy per bit, and the bit rate is \(b\) times higher than the symbol rate. Substitution in the formula above results in a completely analogous expression for \(E_s/N_0\) as for \(E_b/N_0\),

\[\frac{E_s}{N_0}=\frac{S}{N}\frac{B}{R_s}.\]

The “most natural” figure of merit might still be \(E_b/N_0\), in the sense that it depends on how much energy needs to be spent to get a *bit* from point A to point B. However, the things that are actually sent over the air or over the wire are *symbols*, so, in practice, you will often want to work with \(E_s/N_0\).

## The Question

Concretely, this article answers the following question. How to set the \(E_s/N_0\) of a complex signal \(x[k]\) of length \(K\) to a given value by adding *Additive White Gaussian Noise* (*AWGN*). \(E_s\) is the energy per symbol, and \(N_0\) is the noise spectral density. If you are unsure why the signal would be complex, read What is a Constellation Diagram? first.

The input signal is digital, so \(x[k]\) is simply a series of \(K\) complex numbers. The task is now to create a noise signal \(n[k]\), also of length \(K\), so that the summed signal \(x[k]+n[k]\) has the required \(E_s/N_0\). The first step is to convert the \(E_s/N_0\) to a simple SNR, so that we can determine the power that the noise samples need to have.

As stated above, we know that

\[\frac{E_s}{N_0}=\frac{S}{N}\frac{B}{R_s},\]

with \(B\) the bandwidth of the channel and \(R_s\) the symbol rate. The rest of this article assumes that \(E_s/N_0\) is linear scale, so you might need to convert it from decibels.

The above formula can be rewritten as

\[N=\frac{SB}{(E_s/N_0)R_s}.\]

The meaning of this expression is simple: if the average power per sample of a signal is \(S\), then adding noise with an average power per sample of \(N\) results in a signal with the required \(E_s/N_0\).

The first thing that we now need to figure out is which values to use for each of the unknowns in the expression for \(N\).

## The Unknowns

The first one is the average power per sample, \(S\). Since we are talking about a simulation here, \(S\) is probably simply known by design. However, if you have to start from an actual sequence \(x[k]\) of length \(K\), then you can compute the average power as

\[S=\frac{1}{K}\sum_{k=0}^{K-1}{|x[k]|^2}.\]

The second unknown is \(B\). This bandwidth is defined as the channel bandwidth, but what concrete number do we use in the simulation? In the original formula for \(E_s/N_0\), the channel bandwidth is used to go from noise \(N\) to *noise spectral density* \(N_0=N/B\). I.e., the bandwidth is used to compute how the total noise power can be translated to noise power per Hz of bandwidth. This means that we can simply use the bandwidth of the simulation for \(B\), since AWGN has *constant spectral density*. Also remember that, since the signal \(x[k]\) is complex, the bandwidth is *equal* to the sampling rate, not half of it as for real signals.

The last unknown is \(R_s\). This is simply the symbol rate.

With the unknowns pinned down, we now know \(N\), the average noise power per sample to add to \(x[k]\).

## The Noise

Given \(N\), how exactly do you create the signal \(n[k]\) to be added to \(x[k]\)?

An important point is that \(n[k]\) is a complex signal, as is \(x[k]\), so we need complex noise samples. Assuming you have access to Gaussian distributed samples with mean \(0\) and variance \(1\), written as \(\mathcal{N}(\mu=0,\,\sigma^2=1)\), you first create two real signals for the real and imaginary part, with \(n_\Re[k]\sim\mathcal{N}(0,1)\) and \(n_\Im[k]\sim\mathcal{N}(0,1)\) for each \(k\), and then combine them as

\[n[k]=\sqrt\frac{N}{2}(n_\Re[k]+i\,n_\Im[k]).\]

The power of a signal of which the samples are distributed as \(\mathcal{N}(0,1)\) is 1, and the factor in front of it changes the power to \(N\). The factor \(\sqrt{N/2}\) is due to two things. First, the power has to be divided by \(2\), because half of it has to be applied to the real part of the signal, and half of it to the imaginary part. Second, the square root has to be added, because the *power* of the samples has to be multiplied by \(N/2\), while it is the *amplitude* that is multiplied in practice.

## The Code

The following Python function takes a complex input signal \(s\) and adds AWGN with the specified \(E_b/N_0\) (in dB), for a simulation bandwidth of \(B\), and a bit rate of \(R_s\).

from __future__ import division import numpy as np def add_noise(s, EsN0_dB, B, Rs): EsN0 = 10 ** (EsN0_dB / 10) K = len(s) S = np.sum(np.abs(s) ** 2) / K n_real = np.random.normal(0, 1, K) n_imag = np.random.normal(0, 1, K) n = np.sqrt(S * B / (2 * EsN0 * Rs)) * (n_real + 1j * n_imag) return s + n

## Add new comment