Power Measurement

This first post goes over measuring power of signals and computing signal to noise ratios. Differences between real-valued and complex-valued signals are discussed. Finally, signals with appreciable bandwidth are measured.

using Gadfly
using ColorSchemes
using Statistics
using Printf
using FFTW
set_default_plot_size(20cm, 15cm)
style(background_color=colorant"white");

Two functions to measure power, one for real valued vectors and a second for complex vectors.

function power(x::Vector{Float64})::Float64
    """
    power(x::Vector{Float64})::Float64

    Returns the average power (in dB) of the given sequence

    # Examples
    ```julia-repl
    julia> power([1.0, 2.0, 3.0, 4.0])
    8.75
    ```
    """
    10 .* log10.(mean(abs.(x) .^ 2))
end

function power(x::Vector{ComplexF64})::Float64
    """
    power(x::Vector{Float64})::Float64

    Returns the average power (in dB) of the given sequence

    # Examples
    ```julia-repl
    julia> power([1.0+1im, 2.0+2im, 3.0+3im, 4.0+4im])
    11.76
    ```
    """
    10 .* log10.(mean(abs.(x) .^ 2))
end
;
# Parameters for our signals
duration = 1             # Seconds
fs = 100e3               # Hz
f = 10e3                 # Hz
t = 1/fs:1/fs:duration   # Seconds
;

Measuring SNR For Real Valued Signals

In this example the intended SNR is set to 0 dB, a signal is constructed with unity gain and then a noise signal that will give the intended SNR is created. Then the power() function is used to ensure that the signal has the correct SNR.

To find the signal to noise ratio simply subtract the signal power (in dB) from the noise power (in dB). Remember that subtraction in log-space is division in linear-space. Note that the power of each signal is -3 dB and the SNR is 0 dB.

snr = 0                           # dB
snr_linear = 10 .^ (snr ./ 10)    # linear

x = cos.(2π * f .* t)
x_power = power(x)
x_power_linear = 10 .^ (x_power ./ 10)

noise_power_linear = x_power_linear ./ snr_linear
noise_power = 10*log10(noise_power_linear)

noise = sqrt(noise_power_linear) .* randn(Float64, size(x))

xₙ = x + noise
n_power = power(noise)
xₙ_power = power(xₙ)
snr_measured = x_power .- n_power
@printf "Avg Power of x = %2.2f dB\n" x_power
@printf "Avg Power of noise = %2.2f dB\n" n_power
@printf "Measured SNR = %2.2f dB\n" snr_measured
@printf "Expected SNR = %2.2f dB\n" snr
Avg Power of x = -3.01 dB
Avg Power of noise = -3.02 dB
Measured SNR = 0.01 dB
Expected SNR = 0.00 dB

Plot 100 samples of the tone, before noise is added and after.

n = 100  # number of sample to plot
p_1 = plot(Guide.title("x (real valued signal)"), Guide.ylabel("Amplitude", orientation=:vertical), Guide.xlabel("Time [s]"))
p_2 = plot(Guide.title("xₙ (real valued signal)"), Guide.ylabel("Amplitude", orientation=:vertical), Guide.xlabel("Time [s]"))

push!(p_1, layer(x=t[1:n], y=x[1:n], Geom.line, color=[colorant"blue"]))
push!(p_2, layer(x=t[1:n], y=xₙ[1:n], Geom.line, color=[colorant"blue"]))

stack = vstack(p_1, p_2)

<?xml version=”1.0” encoding=”UTF-8”?>

Time [s] -0.0015 -0.0010 -0.0005 0.0000 0.0005 0.0010 0.0015 0.0020 0.0025 -0.0010 -0.0009 -0.0008 -0.0007 -0.0006 -0.0005 -0.0004 -0.0003 -0.0002 -0.0001 0.0000 0.0001 0.0002 0.0003 0.0004 0.0005 0.0006 0.0007 0.0008 0.0009 0.0010 0.0011 0.0012 0.0013 0.0014 0.0015 0.0016 0.0017 0.0018 0.0019 0.0020 -0.001 0.000 0.001 0.002 -0.00100 -0.00095 -0.00090 -0.00085 -0.00080 -0.00075 -0.00070 -0.00065 -0.00060 -0.00055 -0.00050 -0.00045 -0.00040 -0.00035 -0.00030 -0.00025 -0.00020 -0.00015 -0.00010 -0.00005 0.00000 0.00005 0.00010 0.00015 0.00020 0.00025 0.00030 0.00035 0.00040 0.00045 0.00050 0.00055 0.00060 0.00065 0.00070 0.00075 0.00080 0.00085 0.00090 0.00095 0.00100 0.00105 0.00110 0.00115 0.00120 0.00125 0.00130 0.00135 0.00140 0.00145 0.00150 0.00155 0.00160 0.00165 0.00170 0.00175 0.00180 0.00185 0.00190 0.00195 0.00200 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -10 -9 -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10 -9.0 -8.5 -8.0 -7.5 -7.0 -6.5 -6.0 -5.5 -5.0 -4.5 -4.0 -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 -10 -5 0 5 10 -9.0 -8.8 -8.6 -8.4 -8.2 -8.0 -7.8 -7.6 -7.4 -7.2 -7.0 -6.8 -6.6 -6.4 -6.2 -6.0 -5.8 -5.6 -5.4 -5.2 -5.0 -4.8 -4.6 -4.4 -4.2 -4.0 -3.8 -3.6 -3.4 -3.2 -3.0 -2.8 -2.6 -2.4 -2.2 -2.0 -1.8 -1.6 -1.4 -1.2 -1.0 -0.8 -0.6 -0.4 -0.2 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 3.2 3.4 3.6 3.8 4.0 4.2 4.4 4.6 4.8 5.0 5.2 5.4 5.6 5.8 6.0 6.2 6.4 6.6 6.8 7.0 7.2 7.4 7.6 7.8 8.0 8.2 8.4 8.6 8.8 9.0 Amplitude xₙ (real valued signal) Time [s] -0.0015 -0.0010 -0.0005 0.0000 0.0005 0.0010 0.0015 0.0020 0.0025 -0.0010 -0.0009 -0.0008 -0.0007 -0.0006 -0.0005 -0.0004 -0.0003 -0.0002 -0.0001 0.0000 0.0001 0.0002 0.0003 0.0004 0.0005 0.0006 0.0007 0.0008 0.0009 0.0010 0.0011 0.0012 0.0013 0.0014 0.0015 0.0016 0.0017 0.0018 0.0019 0.0020 -0.001 0.000 0.001 0.002 -0.00100 -0.00095 -0.00090 -0.00085 -0.00080 -0.00075 -0.00070 -0.00065 -0.00060 -0.00055 -0.00050 -0.00045 -0.00040 -0.00035 -0.00030 -0.00025 -0.00020 -0.00015 -0.00010 -0.00005 0.00000 0.00005 0.00010 0.00015 0.00020 0.00025 0.00030 0.00035 0.00040 0.00045 0.00050 0.00055 0.00060 0.00065 0.00070 0.00075 0.00080 0.00085 0.00090 0.00095 0.00100 0.00105 0.00110 0.00115 0.00120 0.00125 0.00130 0.00135 0.00140 0.00145 0.00150 0.00155 0.00160 0.00165 0.00170 0.00175 0.00180 0.00185 0.00190 0.00195 0.00200 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 -3.0 -2.8 -2.6 -2.4 -2.2 -2.0 -1.8 -1.6 -1.4 -1.2 -1.0 -0.8 -0.6 -0.4 -0.2 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 -4 -2 0 2 4 -3.0 -2.9 -2.8 -2.7 -2.6 -2.5 -2.4 -2.3 -2.2 -2.1 -2.0 -1.9 -1.8 -1.7 -1.6 -1.5 -1.4 -1.3 -1.2 -1.1 -1.0 -0.9 -0.8 -0.7 -0.6 -0.5 -0.4 -0.3 -0.2 -0.1 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 3.0 Amplitude x (real valued signal)

Next, plot the power spectral density. The PSD shows how the power of the signal is concentrated over the frequency band, 50 kHz in this case. Glancing at the PSD it might be tempting to think that the SNR is closer to 35 dB. However, the power of sine wave is concentrated entirely in one bin, where the noise is spread out over all the FFT bins. Since all the noise is spread out in frequency the PSD clearly shows the presence of the tone even at 0 dB SNR.

fft_size = 10000
xf = (abs.(fft(xₙ[1:fft_size]))).^2 / (fs / 2) / fft_size / (fft_size/length(xₙ))
nf = (abs.(fft(noise[1: fft_size]))).^2 / (fs / 2) / fft_size / (fft_size/length(xₙ))
noise_floor = 10 * log10.(mean(nf))

freqs = FFTW.fftfreq(length(xf), fs)
freq_response = plot()
push!(freq_response, Guide.title("PSD"), 
                     Guide.xlabel("Frequency [kHz]"), 
                     Guide.ylabel("Power Density [dB/Hz]", 
                     orientation=:vertical))
push!(freq_response, Coord.cartesian(xmin=-fs/(2 * 1e3), xmax=fs/(2*1e3), 
                                     ymin=-80, ymax=0))
push!(freq_response, layer(x=[-fs/(2 * 1e3), fs/(2*1e3)], 
                           y=[noise_floor, noise_floor], 
                           Geom.line, 
                           color=[colorant"red"]))
push!(freq_response, layer(x=freqs ./ 1e3, 
                           y=10 .* log10.(xf), 
                           Geom.line, 
                           color=[colorant"blue"]))
Frequency [kHz] -200 -150 -100 -50 0 50 100 150 200 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 -200 -100 0 100 200 -150 -145 -140 -135 -130 -125 -120 -115 -110 -105 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -180 -160 -140 -120 -100 -80 -60 -40 -20 0 20 40 60 80 100 -160 -155 -150 -145 -140 -135 -130 -125 -120 -115 -110 -105 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 -200 -100 0 100 -160 -155 -150 -145 -140 -135 -130 -125 -120 -115 -110 -105 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 Power Density [dB/Hz] PSD

Compare the theoretical and measured power of the signal and noise.

Computing the measurements is simple. For the noise, take the mean() of the FFT output and for the tone, take the peak of FFT output.

Calculating the the noise power and signal power is a little more challenging. To calculate the expected noise measurement from the FFT, take the noise power and then calculate how it’s spread out over the usable bandwidth. There are two parts to this calculation. The first is two divide (or subtract in log space) by the usable bandwidth, since this is a real valued signal the usable bandwidth is fs / 2.

The second step is to divide (or subtract in log space) by the portion of the signal that the FFT uses. Longer signals have more power due to their length. Since the power() function uses the entire signal and the FFT has length 10000, that ratio needs to be compensated for.

The approach for calculating the theoretical peak power is similar for noise. The difference is that we must compensate for the bandwidth of the bin that tone occupies as well. The reason for this is that the tone is actually narrower than the bin so the power of the tone gets spread out over the bandwidth of the bin.

peak_power = findmax(10 .* log10.(xf))[1]

@printf "Measured Noise Power %2.1f dB/Hz\n" noise_floor
@printf "Theoretical Noise Power %2.1f dB/Hz\n" noise_power  - 10*log10((fs/2)) - 10*log10(fft_size/length(xₙ))
@printf "Measured Peak power %2.2f dB/Hz\n" peak_power
@printf "Theoretical Peak power %2.2f dB/Hz\n" (x_power + noise_power) - 10*log10((fs/2) / fft_size) - 10*log10(fft_size / length(xₙ)) 
Measured Noise Power -40.0 dB/Hz
Theoretical Noise Power -40.0 dB/Hz
Measured Peak power -3.15 dB/Hz
Theoretical Peak power -3.01 dB/Hz

Measuring SNR for Complex Valued Signals

For a complex signal, calculating the power is the same as a real signal, just count both the real and complex parts. Here, the power of the complex signal is 0 dB where the real signal was -3 dB. This is because the complex signal has real (cos) and imaginary (sin) components. Since the complex signal has twice as many components it has twice the power (ie 3 dB more).

snr = 0  # dB
snr_linear = 10 .^ (snr ./ 10)
x = im.* sin.(2π * f .* t) + cos.(2π * f .* t)
x_power = power(x)
x_power_linear = 10 .^ (x_power ./ 10)

noise_power_linear = x_power_linear ./ snr_linear
noise_power = 10*log10(noise_power_linear)

noise = sqrt(noise_power_linear) .* randn(ComplexF64, size(x))

snr_measured = x_power .- n_power

xₙ = x + noise
n_power = power(noise)
xₙ_power = power(xₙ)
snr_measured = x_power .- n_power
@printf "Avg Power of x = %2.2f dB\n" x_power
@printf "Avg Power of noise = %2.2f dB\n" n_power
@printf "SNR Measured = %2.2f dB\n" snr_measured
@printf "SNR Expected = %2.2f dB\n" snr
Avg Power of x = 0.00 dB
Avg Power of noise = 0.02 dB
SNR Measured = -0.02 dB
SNR Expected = 0.00 dB

Plot the time domain signal. Notice that both the real (inphase) and imaginary (quadrature) parts are present.

p_1 = plot(Guide.title("x (complex valued signal)"), Guide.ylabel("Amplitude", orientation=:vertical), Guide.xlabel("Time [s]"))
p_2 = plot(Guide.title("xₙ (complex valued signal)"), Guide.ylabel("Amplitude",  orientation=:vertical), Guide.xlabel("Time [s]"))

push!(p_1, layer(x=t[1:n], y=real(x[1:n]), Geom.line, color=[colorant"blue"]))
push!(p_1, layer(x=t[1:n], y=imag(x[1:n]), Geom.line, color=[colorant"red"]))
push!(p_2, layer(x=t[1:n], y=real(xₙ[1:n]), Geom.line, color=[colorant"blue"]))
push!(p_2, layer(x=t[1:n], y=imag(xₙ[1:n]), Geom.line, color=[colorant"red"]))

stack = vstack(p_1, p_2)

<?xml version=”1.0” encoding=”UTF-8”?>

Time [s] -0.0015 -0.0010 -0.0005 0.0000 0.0005 0.0010 0.0015 0.0020 0.0025 -0.0010 -0.0009 -0.0008 -0.0007 -0.0006 -0.0005 -0.0004 -0.0003 -0.0002 -0.0001 0.0000 0.0001 0.0002 0.0003 0.0004 0.0005 0.0006 0.0007 0.0008 0.0009 0.0010 0.0011 0.0012 0.0013 0.0014 0.0015 0.0016 0.0017 0.0018 0.0019 0.0020 -0.001 0.000 0.001 0.002 -0.00100 -0.00095 -0.00090 -0.00085 -0.00080 -0.00075 -0.00070 -0.00065 -0.00060 -0.00055 -0.00050 -0.00045 -0.00040 -0.00035 -0.00030 -0.00025 -0.00020 -0.00015 -0.00010 -0.00005 0.00000 0.00005 0.00010 0.00015 0.00020 0.00025 0.00030 0.00035 0.00040 0.00045 0.00050 0.00055 0.00060 0.00065 0.00070 0.00075 0.00080 0.00085 0.00090 0.00095 0.00100 0.00105 0.00110 0.00115 0.00120 0.00125 0.00130 0.00135 0.00140 0.00145 0.00150 0.00155 0.00160 0.00165 0.00170 0.00175 0.00180 0.00185 0.00190 0.00195 0.00200 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -10 -9 -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10 -9.0 -8.5 -8.0 -7.5 -7.0 -6.5 -6.0 -5.5 -5.0 -4.5 -4.0 -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 -10 -5 0 5 10 -9.0 -8.8 -8.6 -8.4 -8.2 -8.0 -7.8 -7.6 -7.4 -7.2 -7.0 -6.8 -6.6 -6.4 -6.2 -6.0 -5.8 -5.6 -5.4 -5.2 -5.0 -4.8 -4.6 -4.4 -4.2 -4.0 -3.8 -3.6 -3.4 -3.2 -3.0 -2.8 -2.6 -2.4 -2.2 -2.0 -1.8 -1.6 -1.4 -1.2 -1.0 -0.8 -0.6 -0.4 -0.2 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 3.2 3.4 3.6 3.8 4.0 4.2 4.4 4.6 4.8 5.0 5.2 5.4 5.6 5.8 6.0 6.2 6.4 6.6 6.8 7.0 7.2 7.4 7.6 7.8 8.0 8.2 8.4 8.6 8.8 9.0 Amplitude xₙ (complex valued signal) Time [s] -0.0015 -0.0010 -0.0005 0.0000 0.0005 0.0010 0.0015 0.0020 0.0025 -0.0010 -0.0009 -0.0008 -0.0007 -0.0006 -0.0005 -0.0004 -0.0003 -0.0002 -0.0001 0.0000 0.0001 0.0002 0.0003 0.0004 0.0005 0.0006 0.0007 0.0008 0.0009 0.0010 0.0011 0.0012 0.0013 0.0014 0.0015 0.0016 0.0017 0.0018 0.0019 0.0020 -0.001 0.000 0.001 0.002 -0.00100 -0.00095 -0.00090 -0.00085 -0.00080 -0.00075 -0.00070 -0.00065 -0.00060 -0.00055 -0.00050 -0.00045 -0.00040 -0.00035 -0.00030 -0.00025 -0.00020 -0.00015 -0.00010 -0.00005 0.00000 0.00005 0.00010 0.00015 0.00020 0.00025 0.00030 0.00035 0.00040 0.00045 0.00050 0.00055 0.00060 0.00065 0.00070 0.00075 0.00080 0.00085 0.00090 0.00095 0.00100 0.00105 0.00110 0.00115 0.00120 0.00125 0.00130 0.00135 0.00140 0.00145 0.00150 0.00155 0.00160 0.00165 0.00170 0.00175 0.00180 0.00185 0.00190 0.00195 0.00200 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -3.5 -3.0 -2.5 -2.0 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 -3.0 -2.8 -2.6 -2.4 -2.2 -2.0 -1.8 -1.6 -1.4 -1.2 -1.0 -0.8 -0.6 -0.4 -0.2 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 -4 -2 0 2 4 -3.0 -2.9 -2.8 -2.7 -2.6 -2.5 -2.4 -2.3 -2.2 -2.1 -2.0 -1.9 -1.8 -1.7 -1.6 -1.5 -1.4 -1.3 -1.2 -1.1 -1.0 -0.9 -0.8 -0.7 -0.6 -0.5 -0.4 -0.3 -0.2 -0.1 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 3.0 Amplitude x (complex valued signal)

Plot the PSD. Now there is only one tone instead of two. This is because a complex signal has twice as many samples as a real signal (I and Q vs just I). Since there are twice as many samples over the same time duration, complex signals are able to meet the Nyquist criterion at fs, not fs/2.

fft_size = 10000
xf = (abs.(fft(xₙ[1:fft_size]))).^2 ./ fs ./ fft_size / (fft_size/length(xₙ))
nf = (abs.(fft(noise[1:fft_size]))).^2 ./ fs ./ fft_size / (fft_size/length(xₙ))
freqs = FFTW.fftfreq(length(xf), fs)
noise_floor = 10 * log10.(mean(nf))

freq_response = plot()
push!(freq_response, Guide.title("Frequency Response"), Guide.xlabel("Frequency [kHz]"), Guide.ylabel("Power Density [dB/Hz]"))
push!(freq_response, Coord.cartesian(xmin=-fs/(2 * 1e3), xmax=fs/(2*1e3), ymin=-80, ymax=10))
push!(freq_response, layer(x=[-fs/(2 * 1e3), fs/(2*1e3)], y=[noise_floor, noise_floor], Geom.line, color=[colorant"red"]))
push!(freq_response, layer(x=freqs ./ 1e3, y=10 .* log10.(xf), Geom.line, color=[colorant"blue"]))
Frequency [kHz] -200 -150 -100 -50 0 50 100 150 200 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 -200 -100 0 100 200 -150 -145 -140 -135 -130 -125 -120 -115 -110 -105 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -180 -170 -160 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 -170 -160 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 -200 -100 0 100 -170 -165 -160 -155 -150 -145 -140 -135 -130 -125 -120 -115 -110 -105 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 Power Density [dB/Hz] Frequency Response
peak_power = findmax(10 .* log10.(xf))[1]

@printf "Measured Noise Power %2.1f dB/Hz\n" noise_floor 
@printf "Theoretical Noise Power %2.1f dB/Hz\n" n_power   - 10*log10(fs) - 10*log10(fft_size/length(xₙ))
@printf "Measured Peak power %2.1f dB/Hz\n" peak_power
@printf "Theoretical Peak power %2.1f dB/Hz" (x_power + noise_power) - 10*log10(fs / fft_size) - 10*log10(fft_size / length(xₙ))
Measured Noise Power -40.0 dB/Hz
Theoretical Noise Power -40.0 dB/Hz
Measured Peak power 0.0 dB/Hz
Theoretical Peak power 0.0 dB/Hz

Note here that the real and complex valued noise signals are off by 3 dB, -43 dB/Hz for the real signal and -40 dB/Hz for the complex. This is because in this simulation the noise power is determined the by the signal power and the SNR (0 dB). The complex signal has 3 dB more power so to maintain the 0 dB SNR the noise is 3 dB higher.

Also note that the complex tone has a peak power of 0 dB/Hz and the power of the real tone is -3 dB/Hz. The complex tone has twice the power from having the real and imaginary components.

Signals With Bandwidth

Measuring SNR

Measuring SNR is again the same for signals with bandwith.

function sinc(x)
   sin.(x) ./ (x); 
end

# Create sinc pulse and normalize to 0 dB
x₁ = sinc.(2π * f .* t .- 1000*π);
x₂ = x₁ ./ sqrt.(10 .^ (power(x₁)./10));
x = x₂
 
snr = 0
snr_linear = 10 .^ (snr ./ 10)
x_pwr = power(x)
x_pwr_linear = 10 .^ (x_pwr ./ 10)
noise = sqrt(x_pwr_linear ./ (snr_linear)) .* randn(Float64, size(x))

xₙ = x + noise
n_pwr = power(noise)
xₙ_pwr = power(xₙ)
snr = x_pwr .- n_pwr;

@printf "Avg Power of x = %2.2f dB\n" x_pwr
@printf "Avg Power of noise = %2.2f dB\n" n_pwr
@printf "SNR = %2.2f dB\n" snr
Avg Power of x = 0.00 dB
Avg Power of noise = -0.02 dB
SNR = 0.02 dB

Below, is the time domain plot of the sinc() function

p_1 = plot(Guide.title("x (complex valued signal)"), Guide.ylabel("Amplitude", orientation=:vertical), Guide.xlabel("Time [s]"))
p_2 = plot(Guide.title("xₙ (complex valued signal)"), Guide.ylabel("Amplitude", orientation=:vertical), Guide.xlabel("Time [s]"))

n = 10000
push!(p_1, layer(x=t[1:n], y=real(x[1:n]), Geom.line, color=[colorant"blue"]))
push!(p_1, layer(x=t[1:n], y=imag(x[1:n]), Geom.line, color=[colorant"red"]))
push!(p_2, layer(x=t[1:n], y=real(xₙ[1:n]), Geom.line, color=[colorant"blue"]))
push!(p_2, layer(x=t[1:n], y=imag(xₙ[1:n]), Geom.line, color=[colorant"red"]))

stack = vstack(p_1, p_2)

<?xml version=”1.0” encoding=”UTF-8”?>

Time [s] -0.15 -0.10 -0.05 0.00 0.05 0.10 0.15 0.20 0.25 -0.10 -0.09 -0.08 -0.07 -0.06 -0.05 -0.04 -0.03 -0.02 -0.01 0.00 0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08 0.09 0.10 0.11 0.12 0.13 0.14 0.15 0.16 0.17 0.18 0.19 0.20 0.21 -0.1 0.0 0.1 0.2 -0.100 -0.095 -0.090 -0.085 -0.080 -0.075 -0.070 -0.065 -0.060 -0.055 -0.050 -0.045 -0.040 -0.035 -0.030 -0.025 -0.020 -0.015 -0.010 -0.005 0.000 0.005 0.010 0.015 0.020 0.025 0.030 0.035 0.040 0.045 0.050 0.055 0.060 0.065 0.070 0.075 0.080 0.085 0.090 0.095 0.100 0.105 0.110 0.115 0.120 0.125 0.130 0.135 0.140 0.145 0.150 0.155 0.160 0.165 0.170 0.175 0.180 0.185 0.190 0.195 0.200 0.205 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -300 -250 -200 -150 -100 -50 0 50 100 150 200 250 300 350 400 -250 -200 -150 -100 -50 0 50 100 150 200 250 300 350 -400 -200 0 200 400 -250 -240 -230 -220 -210 -200 -190 -180 -170 -160 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 210 220 230 240 250 260 270 280 290 300 310 320 330 340 350 Amplitude xₙ (complex valued signal) Time [s] -0.15 -0.10 -0.05 0.00 0.05 0.10 0.15 0.20 0.25 -0.10 -0.09 -0.08 -0.07 -0.06 -0.05 -0.04 -0.03 -0.02 -0.01 0.00 0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08 0.09 0.10 0.11 0.12 0.13 0.14 0.15 0.16 0.17 0.18 0.19 0.20 0.21 -0.1 0.0 0.1 0.2 -0.100 -0.095 -0.090 -0.085 -0.080 -0.075 -0.070 -0.065 -0.060 -0.055 -0.050 -0.045 -0.040 -0.035 -0.030 -0.025 -0.020 -0.015 -0.010 -0.005 0.000 0.005 0.010 0.015 0.020 0.025 0.030 0.035 0.040 0.045 0.050 0.055 0.060 0.065 0.070 0.075 0.080 0.085 0.090 0.095 0.100 0.105 0.110 0.115 0.120 0.125 0.130 0.135 0.140 0.145 0.150 0.155 0.160 0.165 0.170 0.175 0.180 0.185 0.190 0.195 0.200 0.205 h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -300 -250 -200 -150 -100 -50 0 50 100 150 200 250 300 350 400 -250 -200 -150 -100 -50 0 50 100 150 200 250 300 350 -400 -200 0 200 400 -250 -240 -230 -220 -210 -200 -190 -180 -170 -160 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 210 220 230 240 250 260 270 280 290 300 310 320 330 340 350 Amplitude x (complex valued signal)

The frequency domain plot is more interesting. Here are plots of the noise (wheat-colored) and signal (blue) separately along with the average noise power (red) and the average signal power (in-band, dark red).

The signal power is is normalized to the noise so the average power across the whole band is the same, making the SNR 0 dB. However, the signal power that “in-band,” -10 kHz to 10 kHz is 7 dB higher, making the SNR 7 dB for the “in-band” portion.

The SNR of our signal is 7 dB, meaning that the signal power is 7 dB greater than then noise power. Since the signal rolloff is so sharp almost all of it’s power is contained from -10 kHz to 10 kHz. Since the sample rate is 50 kHz the results are verified by calculating 10 * log10(10e3 / 50e3) = -6.9 dB, which matches the SNR.

# PSD
fft_size = length(x)  # Use and FFT size of the entire signal, ensuring all the signal power is represented
xf = (abs.(fft(x[1:fft_size])) ./ fft_size).^2 ./ (fs)
nf = (abs.(fft(noise[1:fft_size])) ./ fft_size).^2 ./ (fs)
xₙf = (abs.(fft(xₙ[1:fft_size])) ./ fft_size).^2 ./ (fs)
freqs = FFTW.fftfreq(length(xf), fs)
noise_floor = 10 * log10.(mean(nf))
ind1 = Int64(ceil(fft_size/2 - f/(fs) * fft_size/2))
ind2 = Int64(floor(fft_size/2 + f/(fs) * fft_size/2))

xf_inband = fftshift(fft(x[1:fft_size]))
xf_inband_pwr = 10*log10(mean((abs.(xf_inband[ind1: ind2]) / fft_size).^2)  / fs)
xf_allband_pwr = 10*log10(mean(abs.(xf_inband / fft_size).^2) / fs)

freq_response = plot(Coord.cartesian(xmin=-fs/(2 * 1e3), xmax=fs/(2*1e3), ymin=-120, ymax=-70),
                     Guide.title("Frequency Response"), 
                     Guide.xlabel("Frequency [kHz]"), 
                     Guide.ylabel("Power Density [dB/Hz]", orientation=:vertical))

push!(freq_response, layer(x=freqs ./ 1e3, 
                           y=10 .* log10.(xf), 
                           Geom.line, 
                           color=[colorant"blue"]))
push!(freq_response, layer(x=[-fs/(2*1e3), fs/(2*1e3)], 
                           y=[xf_inband_pwr, xf_inband_pwr], 
                           Geom.line,  color=[colorant"darkred"],
                           style(line_width=.5mm, line_style=[:dash])))
push!(freq_response, layer(x=[-fs/(2*1e3), fs/(2*1e3)], 
                           y=[noise_floor, noise_floor], 
                           Geom.line, 
                           color=[colorant"red"]))
push!(freq_response, layer(x=freqs ./ 1e3, y=10 .* log10.(xₙf), 
                           Geom.line, 
                           color=[colorant"wheat"]))

push!(freq_response, Guide.manual_color_key("Legend", ["Signal", "Signal Power", "Noise Power", "Noise"], 
                                                      ["blue", "darkred", "red", "wheat"]))
Frequency [kHz] -200 -150 -100 -50 0 50 100 150 200 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 -200 -100 0 100 200 -150 -145 -140 -135 -130 -125 -120 -115 -110 -105 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 125 130 135 140 145 150 Signal Signal Power Noise Power Noise Legend h,j,k,l,arrows,drag to pan i,o,+,-,scroll,shift-drag to zoom r,dbl-click to reset c for coordinates ? for help ? -180 -170 -160 -150 -140 -130 -120 -110 -100 -90 -80 -70 -60 -50 -40 -30 -20 -10 -170 -165 -160 -155 -150 -145 -140 -135 -130 -125 -120 -115 -110 -105 -100 -95 -90 -85 -80 -75 -70 -65 -60 -55 -50 -45 -40 -35 -30 -25 -20 -15 -10 -5 0 -200 -150 -100 -50 0 -170 -168 -166 -164 -162 -160 -158 -156 -154 -152 -150 -148 -146 -144 -142 -140 -138 -136 -134 -132 -130 -128 -126 -124 -122 -120 -118 -116 -114 -112 -110 -108 -106 -104 -102 -100 -98 -96 -94 -92 -90 -88 -86 -84 -82 -80 -78 -76 -74 -72 -70 -68 -66 -64 -62 -60 -58 -56 -54 -52 -50 -48 -46 -44 -42 -40 -38 -36 -34 -32 -30 -28 -26 -24 -22 -20 -18 -16 -14 -12 -10 -8 -6 -4 -2 0 Power Density [dB/Hz] Frequency Response
x_pwr = power(x)
n_pwr = power(noise)
snr = x_pwr - n_pwr
xₙ_pwr = power(xₙ)

@printf "Time Domain Measurements\n"
@printf "Avg noise power %2.1f dB\n" n_pwr
@printf "Avg signal (all band) power %2.1f dB\n" x_pwr
@printf "SNR (all band) = %2.1f dB\n\n" x_pwr - n_pwr

@printf "Frequency Domain Measurements\n"
@printf "Avg noise power (red line) %2.1f dB\n" noise_floor
@printf "Avg in band power (dark red, dashed line) %2.1f dB\n" xf_inband_pwr
@printf "Avg signal power (blue line) %2.1f dB\n" xf_allband_pwr
@printf "SNR (inband) = %2.1f dB\n" xf_inband_pwr - noise_floor
@printf "SNR (allband) = %2.1f dB\n" xf_allband_pwr - noise_floor
Time Domain Measurements
Avg noise power -0.0 dB
Avg signal (all band) power 0.0 dB
SNR (all band) = 0.0 dB

Frequency Domain Measurements
Avg noise power (red line) -100.0 dB
Avg in band power (dark red, dashed line) -93.0 dB
Avg signal power (blue line) -100.0 dB
SNR (inband) = 7.0 dB
SNR (allband) = 0.0 dB