Chris Parmer — home

16-QAM constellation diagram with AWGN noise

Digital Comms

Example from the compendium of canonical charts

Digital Comms — 16-QAM constellation diagram with AWGN noise

Python Code

"""Digital Comms — 16-QAM constellation diagram with AWGN noise."""
from pathlib import Path

# ── Palette + theme (matching the Plotly Studio gallery these charts ship in) ──
VIOLET, TEAL, GREEN, PINK, ORANGE = "#845EEE", "#52B3D0", "#55B685", "#DA5597", "#E9A23B"
PRIMARY, SECONDARY = VIOLET, TEAL
COLORWAY = [VIOLET, TEAL, GREEN, PINK, ORANGE]
BG, TEXT, GRID, MUTED = "#ffffff", "#1c2024", "#d9d9e0", "#60646c"
FONT = "Inter, -apple-system, BlinkMacSystemFont, sans-serif"
COLORSCALE = [[0, "rgba(132, 94, 238, 0.05)"], [1, "rgba(132, 94, 238, 0.9)"]]


def apply_theme(fig):
    """Light gallery theme: white background, Inter font, soft gridlines."""
    fig.update_layout(
        paper_bgcolor=BG, plot_bgcolor=BG, colorway=COLORWAY,
        font=dict(family=FONT, color=TEXT, size=12),
        legend=dict(font=dict(color=TEXT)),
        hoverlabel=dict(bgcolor="#f0f0f3", font=dict(color=TEXT, family=FONT), bordercolor=GRID),
    )
    fig.update_xaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)
    fig.update_yaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)


def fetch_csv(url, **kwargs):
    import io
    import pandas as pd
    import requests
    r = requests.get(url, timeout=60)
    r.raise_for_status()
    return pd.read_csv(io.StringIO(r.text), **kwargs)


def fetch_json(url):
    import requests
    r = requests.get(url, timeout=60)
    r.raise_for_status()
    return r.json()

import numpy as np
import plotly.graph_objects as go


def generate():
    RNG = np.random.default_rng(42)

    # ── 16-QAM ideal constellation points ────────────────────────────────────────
    # Gray-coded 16-QAM: I,Q ∈ {-3,-1,+1,+3}/sqrt(10)
    levels = np.array([-3, -1, 1, 3]) / np.sqrt(10)
    I_ideal, Q_ideal = np.meshgrid(levels, levels)
    I_ideal = I_ideal.ravel()
    Q_ideal = Q_ideal.ravel()

    # ── Generate ~2000 random symbols with AWGN ───────────────────────────────────
    N = 2000
    idx = RNG.integers(0, 16, size=N)
    I_tx = I_ideal[idx]
    Q_tx = Q_ideal[idx]

    # AWGN: Es=1, SNR_dB=15
    SNR_dB = 15
    Es = 1.0
    sigma = np.sqrt(Es / (2 * 10 ** (SNR_dB / 10)))  # per-dimension std
    I_noisy = I_tx + RNG.normal(0, sigma, N)
    Q_noisy = Q_tx + RNG.normal(0, sigma, N)

    # ── Build figure ──────────────────────────────────────────────────────────────
    fig = go.Figure()

    fig.add_trace(go.Scattergl(
        x=I_noisy,
        y=Q_noisy,
        mode="markers",
        marker=dict(size=4, opacity=0.45, color=VIOLET),
        name="Received symbols",
    ))

    fig.add_trace(go.Scatter(
        x=I_ideal,
        y=Q_ideal,
        mode="markers",
        marker=dict(size=12, symbol="x", color="#e03030", line=dict(width=2)),
        name="Ideal points",
    ))

    fig.update_layout(
        xaxis=dict(
            title="In-phase (I)",
            scaleanchor="y",
            scaleratio=1,
            constrain="domain",
            zeroline=True,
            zerolinecolor="#cccccc",
            range=[-1.1, 1.1],
        ),
        yaxis=dict(
            title="Quadrature (Q)",
            zeroline=True,
            zerolinecolor="#cccccc",
            range=[-1.1, 1.1],
        ),
        legend=dict(x=0.01, y=0.99),
    )

    apply_theme(fig)
    return fig


fig = generate()
fig.show()

Made with Plotly