Engee documentation
Notebook

Analysis of modulation schemes

This study presents a systematic approach to the implementation and verification of test models of receiving and transmitting paths in the Engee environment. The work includes a sequential analysis of various aspects of modulation schemes, starting with basic characteristics and ending with spectral properties.

Auxiliary model management function


To ensure correct operation with models, the function is implemented run_model, which automates the process of loading and executing models. The function performs the following operations:

  • Generates the full path to the model file with the extension .engee

  • Checks the current state of the model in the system core

  • Downloads a model from a file, if necessary, or opens an already uploaded one.

  • Runs the model with output of detailed information about the process

  • Ensures that the work with the model is completed correctly

  • Returns execution results

This approach guarantees stable operation regardless of the initial state of the system.

In [ ]:
function run_model( name_model)
    Path = (@__DIR__) * "/" * name_model * ".engee"
    if name_model in [m.name for m in engee.get_all_models()] 
        model = engee.open( name_model ) 
        model_output = engee.run( model, verbose=true ); 
    else
        model = engee.load( Path, force=true ) 
        model_output = engee.run( model, verbose=true ); 
        engee.close( name_model, force=true ); 
    end
    sleep(0.1)
    return model_output
end
Out[0]:
run_model (generic function with 1 method)

Bit error probability analysis

A comparative study of the characteristics of four modulation schemes was carried out: BPSK, QPSK, 8-PSK and 16-QAM. The research methodology includes:

  1. Simulation in the signal-to-noise ratio (Eb/No) range from 0 to 10 dB in 2 dB increments

  2. Theoretical calculation of BER characteristics using mathematical models for each modulation

  3. Visualization of results on a logarithmic scale for visual comparison

A basic model containing identical signal processing paths for all modulations was used, which ensures the correctness of the comparative analysis, the model itself is demonstrated below.

image.png
In [ ]:
EbNoArr = collect(0:2:10);
Eb_No = 0;
ber_bpsk = zeros(length(EbNoArr));
ber_8psk = zeros(length(EbNoArr));
ber_qpsk = zeros(length(EbNoArr));
ber_16qam = zeros(length(EbNoArr));

for i in 1:length(EbNoArr)
    Eb_No = EbNoArr[i]

    run_model("modulations_1");

    ber_bpsk[i] = collect(BER_BPSK).value[end][1]
    ber_8psk[i] = collect(BER_8PSK).value[end][1]
    ber_qpsk[i] = collect(BER_QPSK).value[end][1]
    ber_16qam[i] = collect(BER_16QAM).value[end][1]
end
Building...
Progress 0%
Progress 5%
Progress 16%
Progress 27%
Progress 39%
Progress 49%
Progress 63%
Progress 74%
Progress 83%
Progress 94%
Progress 100%
Progress 100%
Building...
Progress 0%
Progress 7%
Progress 20%
Progress 31%
Progress 44%
Progress 59%
Progress 71%
Progress 82%
Progress 94%
Progress 100%
Progress 100%
Building...
Progress 0%
Progress 7%
Progress 26%
Progress 41%
Progress 61%
Progress 74%
Progress 84%
Progress 93%
Progress 100%
Progress 100%
Building...
Progress 0%
Progress 5%
Progress 15%
Progress 24%
Progress 31%
Progress 39%
Progress 59%
Progress 69%
Progress 80%
Progress 90%
Progress 100%
Progress 100%
Building...
Progress 0%
Progress 6%
Progress 25%
Progress 43%
Progress 59%
Progress 85%
Progress 100%
Progress 100%
Building...
Progress 0%
Progress 6%
Progress 21%
Progress 33%
Progress 44%
Progress 55%
Progress 71%
Progress 84%
Progress 94%
Progress 100%
Progress 100%
In [ ]:
using SpecialFunctions
colors = Dict(:BPSK => :blue, :QPSK => :red, :PSK8 => :green, :QAM16 => :purple)
function theoretical_ber(EbNo_dB, mod_type)
    EbNo = 10 .^ (EbNo_dB ./ 10)
    if mod_type == :BPSK
        0.5 .* erfc.(sqrt.(EbNo))
    elseif mod_type == :QPSK
        0.5 .* erfc.(sqrt.(EbNo)) 
    elseif mod_type == :PSK8
        (2/3) .* erfc.(sqrt.(3*EbNo) .* sin(π/8))
    elseif mod_type == :QAM16
        (3/8) .* erfc.(sqrt.(2 .* EbNo ./ 5))                    
    end
end
EbNoArr_dense = range(minimum(EbNoArr), maximum(EbNoArr), length=1000)
plot(yscale=:log10, ylims=(1e-6, 1), grid=true, xlabel="Eb/No (dB)", ylabel="BER", title="Theoretical and simulated BER")

for mod in [(:BPSK, ber_bpsk), (:QPSK, ber_qpsk), (:PSK8, ber_8psk), (:QAM16, ber_16qam)]
    mod_type, ber_sim = mod
    c = colors[mod_type]
    plot!(EbNoArr_dense, theoretical_ber(EbNoArr_dense, mod_type), line=:solid, color=c, label="$mod_type (theory)")
    scatter!(EbNoArr, ber_sim, marker=:circle, color=c, label="$mod_type (simul.)", markersize=5)
end
plot!(legend=:bottom)
Out[0]:

Analysis of power characteristics and signal constellations

The study was expanded by using a model with Nyquist filters in the receiving and transmitting paths. The analysis was carried out:

  • Signal strengths before and after filtering to assess the effect of filters

  • Signal constellations demodulated signals for visual assessment of demodulation quality

Constellations for all considered modulation schemes with reference points are constructed.
\ The model itself is presented below.

image.png
In [ ]:
run_model("modulations_2")
Building...
Progress 0%
Progress 5%
Progress 11%
Progress 16%
Progress 22%
Progress 27%
Progress 32%
Progress 71%
Progress 76%
Progress 83%
Progress 88%
Progress 93%
Progress 99%
Progress 100%
Progress 100%
Out[0]:
SimulationResult(
    run_id => 17,
    "16qam_demod" => WorkspaceArray{Vector{ComplexF64}}("modulations_2/16qam_demod")
,
    "Find Delay.delay" => WorkspaceArray{Float64}("modulations_2/Find Delay.delay")
,
    "awgn_bpsk" => WorkspaceArray{Matrix{ComplexF64}}("modulations_2/awgn_bpsk")
,
    "ber_qpsk" => WorkspaceArray{Vector{Float64}}("modulations_2/ber_qpsk")
,
    "16qam" => WorkspaceArray{ComplexF64}("modulations_2/16qam")
,
    "qpsk_demod" => WorkspaceArray{Vector{ComplexF64}}("modulations_2/qpsk_demod")
,
    "8psk_demod" => WorkspaceArray{Vector{ComplexF64}}("modulations_2/8psk_demod")
,
    "8psk" => WorkspaceArray{ComplexF64}("modulations_2/8psk")
,
    "bpsk_demod" => WorkspaceArray{Vector{ComplexF64}}("modulations_2/bpsk_demod")
,
    "qpsk" => WorkspaceArray{ComplexF64}("modulations_2/qpsk")
,
    "ber_bpsk" => WorkspaceArray{Float64}("modulations_2/ber_bpsk")
,
    "ber_8psk" => WorkspaceArray{Vector{Float64}}("modulations_2/ber_8psk")
,
    "awgn_16qam" => WorkspaceArray{Vector{ComplexF64}}("modulations_2/awgn_16qam")
,
    "awgn_qpsk" => WorkspaceArray{Matrix{ComplexF64}}("modulations_2/awgn_qpsk")
,
    "16qam_f" => WorkspaceArray{Vector{ComplexF64}}("modulations_2/16qam_f")
,
    "8psk_f" => WorkspaceArray{Vector{ComplexF64}}("modulations_2/8psk_f")
,
    "bpsk" => WorkspaceArray{ComplexF64}("modulations_2/bpsk")
,
    "qpsk_f" => WorkspaceArray{Vector{ComplexF64}}("modulations_2/qpsk_f")
,
    "bpsk_f" => WorkspaceArray{Vector{ComplexF64}}("modulations_2/bpsk_f")
,
    "awgn_8psk" => WorkspaceArray{Matrix{ComplexF64}}("modulations_2/awgn_8psk")
,
    "ber_16qam" => WorkspaceArray{Vector{Float64}}("modulations_2/ber_16qam")

)
In [ ]:
using Statistics 
println("Before the filter:")
bpsk = collect(simout["modulations_2/bpsk"]).value
power_bpsk = mean(abs2.(x[1]) for x in bpsk)
println("BPSK Power: $power_bpsk")

qpsk = collect(simout["modulations_2/qpsk"]).value
power_qpsk = mean(abs2.(x[1]) for x in qpsk)
println("QPSK Power: $power_qpsk")

psk8 = collect(simout["modulations_2/8psk"]).value
power_8psk = mean(abs2.(x[1]) for x in psk8)
println("8PSK Power: $power_8psk")

qam16 = collect(simout["modulations_2/16qam"]).value
power_16qam = mean(abs2.(x[1]) for x in qam16)
println("16QAM Power: $power_16qam")

println("After the filter:")
bpsk = collect(simout["modulations_2/bpsk_f"]).value
power_bpsk = mean(abs2.(x[1]) for x in bpsk)
println("BPSK Power: $power_bpsk")

qpsk = collect(simout["modulations_2/qpsk_f"]).value
power_qpsk = mean(abs2.(x[1]) for x in qpsk)
println("QPSK Power: $power_qpsk")

psk8 = collect(simout["modulations_2/8psk_f"]).value
power_8psk = mean(abs2.(x[1]) for x in psk8)
println("8PSK Power: $power_8psk")

qam16 = collect(simout["modulations_2/16qam_f"]).value
power_16qam = mean(abs2.(x[1]) for x in qam16)
println("16QAM Power: $power_16qam")
До фильтра:
Мощность BPSK: 1.0
Мощность QPSK: 1.0
Мощность 8PSK: 1.0
Мощность 16QAM: 10.25582944703531
После фильтра:
Мощность BPSK: 0.1409425649351918
Мощность QPSK: 0.17159058274740896
Мощность 8PSK: 0.14019923876567178
Мощность 16QAM: 1.4458938239675563

Let's perform the construction of a guide for each of the modulations.

In [ ]:
bpsk = collect(simout["modulations_2/bpsk_demod"]).value
bpsk = [x[1] for x in bpsk]  # Extracting the first element of each vector
plot(title="BPSK")
plot!(bpsk, seriestype=:scatter)
plot!([-1+0im, 1+0im], seriestype=:scatter)
Out[0]:
In [ ]:
qpsk = collect(simout["modulations_2/qpsk_demod"]).value;
qpsk = [x[1] for x in qpsk]  # Extracting the first element of each vector
plot(title="QPSK")
plot!(ComplexF64.(qpsk), seriestype=:scatter)
plot!([0.75+0.75im, 0.75-0.75im, -0.75+0.75im, -0.75-0.75im], seriestype=:scatter)
Out[0]:
In [ ]:
psk8 = collect(simout["modulations_2/8psk_demod"]).value;
psk8 = [x[1] for x in psk8]  # Extracting the first element of each vector
plot(title="8-PSK")
plot!(ComplexF64.(psk8), seriestype=:scatter)
plot!(cis.(2pi*[0:7...]/8), seriestype=:scatter)
Out[0]:
In [ ]:
qam16 = collect(simout["modulations_2/16qam_demod"]).value;
qam16 = [(i...)+0 for i in qam16];
plot(title="16QAM")
plot!(ComplexF64.(qam16), seriestype=:scatter)
plot!([a + b*im for a in -3:2:3, b in -3:2:3][:], seriestype=:scatter)
Out[0]:

Spectral analysis of modulated signals

For an in-depth study of the properties of modulated signals, we performed:

  1. Calculation of the spectral power density using:

    • Hanning window function to reduce spectral leakage effects

    • Median filtering for smoothing spectral characteristics

  2. Comparison with the theoretical model of the Nyquist spectrum with a smoothing coefficient of 0.2

  3. Visualization of spectral characteristics in the frequency domain

The implemented model demonstrates the possibilities of analyzing multi-frequency systems using data buffering.

image.png
In [ ]:
using FFTW, DSP, Statistics, SpecialFunctions

function compute_smoothed_spectrum(signal, fs, window_size=20)
    window = hanning(length(signal))
    windowed_signal = signal .* window
    power_spectrum = abs.(fft(windowed_signal)).^2 / (sum(abs2, window) * fs)
    power_spectrum_db = 10*log10.(power_spectrum)
    
    function my_medfilt(signal, window_size)
        half_window = window_size ÷ 2
        smoothed = similar(signal)
        n = length(signal)
        for i in 1:n
            start_idx = max(1, i - half_window)
            end_idx = min(n, i + half_window)
            window_data = signal[start_idx:end_idx]
            smoothed[i] = median(window_data)
        end
        return smoothed
    end
    power_spectrum_db_smoothed = my_medfilt(power_spectrum_db, window_size)
    freqs = fftfreq(length(signal), fs)
    return fftshift(freqs), fftshift(power_spectrum_db_smoothed)
end

function nyquist_spectrum(frequencies, rolloff_factor=0.5, symbol_rate=1.0)
    T = 1.0 / symbol_rate
    f_N = 1.0 / (2 * T)
    spectrum = zeros(length(frequencies))
    for (i, f) in enumerate(frequencies)
        f_abs = abs(f)
        if f_abs <= (1 - rolloff_factor) * f_N
            spectrum[i] = T
        elseif f_abs <= (1 + rolloff_factor) * f_N && f_abs > (1 - rolloff_factor) * f_N
            spectrum[i] = T/2 * (1 + cos(π * T / rolloff_factor * (f_abs - (1 - rolloff_factor) * f_N)))
        else
            spectrum[i] = 0.0
        end
    end
    spectrum_db = 10 * log10.(spectrum .+ 1e-12)
    return spectrum_db
end

fs = 400
window_size = 15
symbol_rate = 50.0
rolloff = 0.2

run_model("modulations_3")

bpsk = collect(simout["modulations_3/bpsk_f"]).value
bpsk = [(i...)+0 for i in bpsk]
qpsk = collect(simout["modulations_3/qpsk_f"]).value
qpsk = [(i...)+0 for i in qpsk]
psk8 = collect(simout["modulations_3/8psk_f"]).value
psk8 = [(i...)+0 for i in psk8]
qam16 = collect(simout["modulations_3/16qam_f"]).value
qam16 = [(i...)+0 for i in qam16]

freqs_bpsk, spectrum_bpsk = compute_smoothed_spectrum(bpsk, fs, window_size)
freqs_qpsk, spectrum_qpsk = compute_smoothed_spectrum(qpsk, fs, window_size)
freqs_psk8, spectrum_psk8 = compute_smoothed_spectrum(psk8, fs, window_size)
freqs_qam16, spectrum_qam16 = compute_smoothed_spectrum(qam16, fs, window_size)
freqs_theoretical = range(-fs/2, fs/2, length=1000)
spectrum_nyquist_02 = nyquist_spectrum(freqs_theoretical, 0.2, symbol_rate)
max_experimental = maximum([maximum(spectrum_bpsk), maximum(spectrum_qpsk), maximum(spectrum_psk8), maximum(spectrum_qam16)])
max_theoretical_02 = maximum(spectrum_nyquist_02)
spectrum_nyquist_02_normalized = spectrum_nyquist_02 .- (max_theoretical_02 - max_experimental)

plot(freqs_bpsk, spectrum_bpsk, label="BPSK", linewidth=2, grid=true)
plot!(freqs_qpsk, spectrum_qpsk, label="QPSK", linewidth=2)
plot!(freqs_psk8, spectrum_psk8, label="8-PSK", linewidth=2)
plot!(freqs_qam16, spectrum_qam16, label="16-QAM", linewidth=2)
plot!(freqs_theoretical, spectrum_nyquist_02_normalized, label="Nyquist α=0.2", linewidth=3, linestyle=:dash, color=:red)
title!("Energy spectrum of modulated signals (fs = $fs Hz)\nmedian filter with window $window_size")
xlabel!("Frequency, Hz")
ylabel!("Spectral power density, dB/Hz")
xlims!(-fs/2, fs/2)
Building...
Progress 0%
Progress 6%
Progress 23%
Progress 32%
Progress 40%
Progress 49%
Progress 59%
Progress 68%
Progress 78%
Progress 88%
Progress 98%
Progress 100%
Progress 100%
Out[0]:

The results obtained make it possible to conduct a comprehensive analysis of the modulation schemes and justify the choice of optimal modulation for specific operating conditions of communication systems.

Conclusion.

Based
on a comprehensive analysis of the characteristics of the modulation schemes, the following conclusions can be drawn.

Parameter BPSK QPSK 8-PSK 16-QAM
Efficiency 1 b/s/Hz 2 b/s/Hz 3 b/s/Hz 4 b/s/Hz
Required Eb/No for BER=10-3 ~7 dB ~7 dB ~11 dB ~15 dB
Complexity of demodulation Low Low Average High

Best choice for different scenarios:

  1. For maximum noise immunity → BPSK
  2. Optimal compromise → QPSK ⭐
  3. With limited bandwidth and good SNR → 8-PSK
  4. For maximum speed in ideal conditions → 16-QAM

To summarize, QPSK is the most balanced and practical choice for most real-world communication systems, providing the optimal ratio of noise immunity, spectral efficiency, and ease of implementation. BPSK should be used in systems with extreme reliability requirements, and higher-level modulations should only be used with guaranteed good communication channel quality.