codex/huffman-merge-tree-example.py

huffman-merge-tree-example.py
"""Original Huffman tree; save its PNG basename to cwd.

Tested with Python 3.14.4 and Matplotlib 3.10.7 (repository dependencies).
Run from _media: MPLCONFIGDIR=/tmp/codex-wiki-mpl python3 ../huffman-merge-tree-example.py
"""

from fractions import Fraction
from pathlib import Path

import matplotlib.pyplot as plt


def main():
    probabilities = dict(zip("ABCD", map(Fraction, ("0.4", "0.3", "0.2", "0.1"))))
    codes = {"A": "0", "B": "10", "C": "110", "D": "111"}
    children = {"root": ("A", "BCD"), "BCD": ("B", "CD"), "CD": ("C", "D")}
    weights = probabilities | {"CD": probabilities["C"] + probabilities["D"]}
    weights["BCD"] = probabilities["B"] + weights["CD"]
    weights["root"] = probabilities["A"] + weights["BCD"]
    assert [weights[x] for x in ("CD", "BCD", "root")] == list(map(Fraction, ("0.3", "0.6", "1")))
    assert sum(probabilities[s] * len(codes[s]) for s in codes) == Fraction(19, 10)

    def walk(node, prefix=""):
        if node not in children:
            assert codes[node] == prefix
        else:
            for bit, child in enumerate(children[node]):
                walk(child, prefix + str(bit))

    walk("root")
    positions = {"root": (0.43, 0.91), "A": (0.15, 0.66), "BCD": (0.63, 0.66),
                 "B": (0.43, 0.40), "CD": (0.82, 0.40), "C": (0.71, 0.14), "D": (0.94, 0.14)}
    colors = {"A": "#2d6faa", "B": "#168475", "C": "#b16813", "D": "#9059a0"}
    plt.rcParams.update({"font.size": 12, "font.family": "DejaVu Sans"})
    fig, ax = plt.subplots(figsize=(8.2, 5.5), layout="constrained", facecolor="white")
    ax.set(xlim=(0, 1.04), ylim=(-0.21, 1.04), facecolor="white")
    ax.axis("off")
    for parent, pair in children.items():
        x, y = positions[parent]
        for bit, child in enumerate(pair):
            xx, yy = positions[child]
            ax.plot([x, xx], [y, yy], color="#526173", linewidth=2, zorder=1)
            ax.text((x + xx) / 2, (y + yy) / 2, str(bit), ha="center", va="center",
                    fontsize=13, color="#182638", bbox={"facecolor": "white", "edgecolor": "none", "pad": 3}, zorder=3)
    for node, (x, y) in positions.items():
        label = f"{node}  {float(weights[node]):.1f}" if node in probabilities else f"{float(weights[node]):.1f}"
        ax.text(x, y, label, ha="center", va="center", fontsize=13,
                color="white" if node in colors else "#172638",
                bbox={"boxstyle": "round,pad=0.5", "facecolor": colors.get(node, "#e2eaf2"),
                      "edgecolor": "white", "linewidth": 1.5}, zorder=4)
    ax.text(0.02, 0.35, "Build upwards:\n1.  D + C = 0.3\n2.  B + CD = 0.6\n3.  A + BCD = 1.0",
            ha="left", va="center", fontsize=11, linespacing=1.65, color="#26384b")
    ax.text(0.52, -0.035, "Code legend:  A = 0     B = 10     C = 110     D = 111", ha="center", fontsize=12)
    ax.text(0.52, -0.145, "Read down from the root; each leaf ends one codeword.", ha="center", fontsize=11, color="#526173")
    ax.set_title("Huffman coding: merge the two smallest current weights", fontsize=14, pad=12)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=120, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()