codex/fano-error-entropy-budget.py

fano-error-entropy-budget.py
"""Plot the exact entropy budget for a symmetric four-symbol toy channel.

Save the PNG basename to cwd; run from _media with
MPLCONFIGDIR=/tmp/codex-wiki-matplotlib. Uses the repository's matplotlib/numpy
environment; tested with Python 3.14. No experimental data are used.
"""

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def binary_entropy(q):
    q = np.asarray(q, dtype=float)
    result = np.zeros_like(q)
    mask = (q > 0) & (q < 1)
    p = q[mask]
    result[mask] = -p * np.log2(p) - (1 - p) * np.log2(1 - p)
    return result


def main():
    q = np.linspace(0, 1, 1001)
    flag = binary_entropy(q)
    alternatives = q * np.log2(3)
    total = flag + alternatives
    assert np.isclose(total[750], 2)
    assert np.isclose(total[-1], np.log2(3))
    assert np.all(np.diff(total[:751]) > 0)
    assert np.all(np.diff(total[750:]) < 0)

    plt.rcParams.update({"font.size": 12, "font.family": "DejaVu Sans"})
    fig, ax = plt.subplots(figsize=(9.0, 5.7), facecolor="white")
    fig.subplots_adjust(left=0.10, right=0.97, bottom=0.21, top=0.85)
    ax.fill_between(q, 0, flag, color="#bfdcee", label=r"Error flag: $h_2(q)$")
    ax.fill_between(q, flag, total, color="#f8d494",
                    label=r"Wrong-symbol uncertainty: $q\log_2 3$")
    ax.plot(q, total, color="#163b61", linewidth=2.5,
            label=r"Total: $H(X\mid Y)=h_2(q)+q\log_2 3$")
    ax.plot(q, flag, color="#5790b4", linewidth=1.5)
    ax.axvline(0.75, color="#516275", linestyle="--", linewidth=1.3)
    for value in (0.3, 0.75, 1.0):
        height = float(binary_entropy([value])[0] + value * np.log2(3))
        ax.scatter([value], [height], color="#163b61", zorder=5, s=32)
    ax.annotate("q = 0.3\n1.3568 bits", (0.3, float(total[300])), (0.07, 1.65),
                arrowprops={"arrowstyle": "->", "color": "#516275"}, fontsize=11)
    ax.annotate("q = 3/4: independent\n2 bits (maximum)", (0.75, 2), (0.49, 2.14),
                arrowprops={"arrowstyle": "->", "color": "#516275"}, fontsize=11)
    ax.annotate("q = 1: always wrong\n1.5850 bits remain", (1, float(total[-1])), (0.69, 1.17),
                arrowprops={"arrowstyle": "->", "color": "#516275"}, fontsize=11,
                bbox={"facecolor": "white", "edgecolor": "none", "alpha": 0.9, "pad": 3})
    ax.set(xlim=(0, 1.01), ylim=(0, 2.42), xlabel="Error probability q of the identity decoder",
           ylabel="Conditional entropy (bits)", xticks=[0, 0.25, 0.5, 0.75, 1])
    ax.set_axisbelow(True)
    ax.grid(alpha=0.2)
    ax.legend(loc="lower left", fontsize=10, framealpha=0.95)
    fig.suptitle("Fano's bound is exact for this four-symbol toy model", fontsize=15, y=0.97)
    fig.text(0.5, 0.91, "Two uncertainties add; more raw errors need not mean more uncertainty",
             ha="center", fontsize=11, color="#516275")
    fig.text(0.5, 0.10,
             "Identity decoder minimizes error for 0 ≤ q ≤ 3/4; for q > 3/4, guess another symbol.",
             ha="center", fontsize=10)
    fig.text(0.5, 0.045, "Uniform input; symmetric channel; theoretical probabilities, not measured data",
             ha="center", fontsize=10, color="#516275")
    fig.savefig(Path.cwd() / "fano-error-entropy-budget.png", dpi=120,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()