{"spec_id":"scatter-constellation-diagram","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nscatter-constellation-diagram: Digital Modulation Constellation Diagram\nLibrary: letsplot 4.10.1 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-06-18\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\nfrom lets_plot.export import ggsave as export_ggsave\n\n\nLetsPlot.setup_html()\n\n# Theme tokens (Imprint palette)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\nRECEIVED_COLOR = \"#009E73\"  # Imprint position 1 — received symbols (primary data)\nIDEAL_COLOR = \"#BD8233\"  # Imprint position 4 ochre — ideal markers (colorblind-safe)\n\n# Data\nnp.random.seed(42)\n\n# 16-QAM ideal constellation points on a 4x4 grid at +/-1, +/-3\ngrid_vals = np.array([-3, -1, 1, 3])\nideal_i, ideal_q = np.meshgrid(grid_vals, grid_vals)\nideal_i = ideal_i.flatten()\nideal_q = ideal_q.flatten()\n\n# Generate received symbols with additive Gaussian noise (~20 dB SNR)\nn_symbols = 1000\nsymbol_indices = np.random.randint(0, 16, n_symbols)\nsnr_db = 20\nsignal_power = np.mean(ideal_i**2 + ideal_q**2)\nnoise_std = np.sqrt(signal_power / (2 * 10 ** (snr_db / 10)))\n\nreceived_i = ideal_i[symbol_indices] + np.random.normal(0, noise_std, n_symbols)\nreceived_q = ideal_q[symbol_indices] + np.random.normal(0, noise_std, n_symbols)\n\n# Compute EVM\nerror_vectors = np.sqrt((received_i - ideal_i[symbol_indices]) ** 2 + (received_q - ideal_q[symbol_indices]) ** 2)\nrms_reference = np.sqrt(np.mean(ideal_i**2 + ideal_q**2))\nevm_percent = np.sqrt(np.mean(error_vectors**2)) / rms_reference * 100\n\n# DataFrames\nreceived_df = pd.DataFrame({\"I\": received_i, \"Q\": received_q})\nideal_df = pd.DataFrame({\"I\": ideal_i, \"Q\": ideal_q})\n\n# Decision boundary rectangles — theme-adaptive shading\nif THEME == \"light\":\n    colors_alt = [\"#F0EDE5\", \"#E8E5DE\"]\nelse:\n    colors_alt = [\"#222220\", \"#1E1E1B\"]\n\nrects = []\nboundary_edges = [-4.5, -2, 0, 2, 4.5]\nfor ri, (y0, y1) in enumerate(zip(boundary_edges[:-1], boundary_edges[1:], strict=True)):\n    for ci, (x0, x1) in enumerate(zip(boundary_edges[:-1], boundary_edges[1:], strict=True)):\n        rects.append({\"xmin\": x0, \"xmax\": x1, \"ymin\": y0, \"ymax\": y1, \"fill\": colors_alt[(ri + ci) % 2]})\nrects_df = pd.DataFrame(rects)\n\n# Decision boundary line positions\nboundary_vals = np.array([-2, 0, 2])\nboundary_v = pd.DataFrame({\"x\": boundary_vals})\nboundary_h = pd.DataFrame({\"y\": boundary_vals})\n\n# EVM annotation\nevm_df = pd.DataFrame({\"x\": [3.8], \"y\": [4.1], \"label\": [f\"EVM = {evm_percent:.1f}%\"]})\n\n# Custom tick positions at constellation grid values\ntick_vals = [-4, -3, -2, -1, 0, 1, 2, 3, 4]\n\n# Title with scaled font size — calibrated for ggsize(600,600)\ntitle = \"16-QAM Constellation · scatter-constellation-diagram · python · letsplot · anyplot.ai\"\ntitle_size = max(9, round(13 * 67 / len(title)))\n\n# Plot\nplot = (\n    ggplot()\n    # Shaded decision regions\n    + geom_rect(\n        data=rects_df,\n        mapping=aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\", fill=\"fill\"),\n        alpha=1.0,\n        color=PAGE_BG,\n        size=0.3,\n    )\n    + scale_fill_identity()\n    # Decision boundary lines\n    + geom_vline(\n        data=boundary_v,\n        mapping=aes(xintercept=\"x\"),\n        linetype=\"dashed\",\n        color=INK_SOFT,\n        size=0.5,\n    )\n    + geom_hline(\n        data=boundary_h,\n        mapping=aes(yintercept=\"y\"),\n        linetype=\"dashed\",\n        color=INK_SOFT,\n        size=0.5,\n    )\n    # Axis lines through origin\n    + geom_hline(yintercept=0, color=INK_SOFT, size=0.7)\n    + geom_vline(xintercept=0, color=INK_SOFT, size=0.7)\n    # 2D density contours — lets-plot stat showing cluster density of received symbols\n    + geom_density2d(\n        data=received_df,\n        mapping=aes(x=\"I\", y=\"Q\"),\n        color=RECEIVED_COLOR,\n        alpha=0.4,\n        size=0.5,\n    )\n    # Received symbols\n    + geom_point(\n        data=received_df,\n        mapping=aes(x=\"I\", y=\"Q\"),\n        color=RECEIVED_COLOR,\n        size=2.0,\n        alpha=0.35,\n    )\n    # Ideal constellation markers (cross shape, Imprint ochre — colorblind-safe)\n    + geom_point(\n        data=ideal_df,\n        mapping=aes(x=\"I\", y=\"Q\"),\n        color=IDEAL_COLOR,\n        size=7,\n        shape=4,\n        stroke=2.5,\n    )\n    # EVM annotation with lets-plot geom_label styling\n    + geom_label(\n        data=evm_df,\n        mapping=aes(x=\"x\", y=\"y\", label=\"label\"),\n        size=4,\n        color=INK,\n        fill=ELEVATED_BG,\n        alpha=0.9,\n        hjust=1,\n        label_padding=0.5,\n        label_r=0.2,\n        label_size=0.6,\n    )\n    + labs(\n        x=\"In-Phase (I)\", y=\"Quadrature (Q)\", title=title\n    )\n    + coord_fixed()\n    + scale_x_continuous(limits=[-4.5, 4.5], breaks=tick_vals)\n    + scale_y_continuous(limits=[-4.5, 4.5], breaks=tick_vals)\n    + ggsize(600, 600)\n    + theme(\n        plot_title=element_text(size=title_size, color=INK, face=\"bold\"),\n        axis_title=element_text(size=12, color=INK),\n        axis_text=element_text(size=10, color=INK_SOFT),\n        panel_background=element_rect(fill=PAGE_BG, color=INK_SOFT, size=0.5),\n        plot_background=element_rect(fill=PAGE_BG),\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        axis_ticks=element_line(color=INK_SOFT, size=0.5),\n        axis_line=element_line(color=INK_SOFT, size=0.5),\n        plot_margin=[20, 20, 20, 20],\n    )\n)\n\n# Save\nexport_ggsave(plot, filename=f\"plot-{THEME}.png\", path=\".\", scale=4)\nexport_ggsave(plot, filename=f\"plot-{THEME}.html\", path=\".\")\n"}