How to Add Noise to Set a Digital Communications Signal to a Given Es/N0

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 is 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_s/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

The content of this field is kept private and will not be shown publicly.
Spam avoidance measure, sorry for this.

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Submitted on 23 August 2020