# make_heatmap.py — SANA-2026-01 substrate coverage, graded per report Sections 4–7 # Requires: pip install matplotlib numpy import matplotlib.pyplot as plt import numpy as np from matplotlib.colors import ListedColormap, BoundaryNorm from matplotlib.patches import Rectangle import matplotlib.cm as cm cols = ["Option 1", "Option 2", "Scenario 1\n(recommended)"] # Levels: 0 absent | 1 below reference | 2 present, no reference / unconfirmed | 3 claim or reference met data = [ ("Starch → maltose/dextrin", [(2, "9,600 U\namylase"), (2, "4,500 FCC DU"), (2, "4,500 FCC DU")]), ("Maltose/dextrin → glucose", [(0, "absent"), (2, "10 AGU"), (2, "10 AGU")]), ("Sucrose → glucose+fructose", [(0, "absent"), (2, "200 INV"), (2, "200 INV")]), ("Lactose → glucose+galactose", [(2, "1,600 U + 2,000 ALU\nTolerase®L\n(unit basis unconfirmed)"), (1, "650 FCC ALU\n(14.4% of claim)"), (3, "4,500 FCC ALU\n(claim met)")]), ("Raffinose/stachyose (legume FODMAP)", [(0, "absent"), (1, "150 GalU\n(12.5% of study dose)"), (1, "150 GalU\n(12.5% of study dose)")]), ("Cellulose / plant cell wall", [(2, "80 U"), (2, "250 CU"), (2, "250 CU")]), ("Phytate (mineral chelator)", [(0, "absent"), (2, "30 FTU"), (2, "30 FTU")]), ("Bulk dietary protein", [(2, "2,400 U +\nbromelain + papain"), (2, "1,950 + 5,580 HUT\n(alkaline fraction\npH-exposed)"), (2, "1,950 + 5,580 HUT\n(alkaline fraction\npH-exposed)")]), ("Gluten proline-rich peptides", [(1, "non-specific\nproteases only"), (2, "2 unidentified\nprotease lines, 225 mg"), (0, "removed")]), ("Triglycerides", [(1, "400 U\n(unit, acid resistance\nunknown)"), (1, "500 FIP\n(1.6% of study dose)"), (1, "500 FIP\n(1.6% of study dose)")]), ("Chloride / gastric acid", [(1, "332 mg betaine HCl\n= 76.6 mg Cl\n(not a permitted source)"), (0, "absent"), (3, "252 mg KCl\n= 120 mg Cl\n(claim met)")]), ] blues = plt.cm.Blues cmap = ListedColormap([blues(0.03), blues(0.30), blues(0.62), blues(0.97)]) norm = BoundaryNorm([-0.5, 0.5, 1.5, 2.5, 3.5], cmap.N) Z = np.array([[c[0] for c in cells] for _, cells in data]) fig, ax = plt.subplots(figsize=(11, 9), dpi=200) ax.pcolormesh(Z, cmap=cmap, norm=norm, edgecolors="white", linewidth=4) ax.invert_yaxis() ax.set_xticks(np.arange(len(cols)) + 0.5) ax.set_xticklabels(cols, fontsize=11) ax.xaxis.tick_bottom() ax.set_yticks(np.arange(len(data)) + 0.5) ax.set_yticklabels([r for r, _ in data], fontsize=11) for s in ax.spines.values(): s.set_visible(False) for i, (_, cells) in enumerate(data): for j, (lvl, txt) in enumerate(cells): ax.text(j + 0.5, i + 0.5, txt, ha="center", va="center", fontsize=8.8, color="white" if lvl >= 2 else ("#555555" if lvl == 0 else "#1a1a1a")) # Dashed amber outline where Scenario 1 differs from Option 2 if j == 2 and cells[2] != cells[1]: ax.add_patch(Rectangle((j + 0.05, i + 0.08), 0.9, 0.84, fill=False, ec="#e0a100", lw=2, ls="--")) sm = cm.ScalarMappable(cmap=cmap, norm=norm) cb = fig.colorbar(sm, ax=ax, ticks=[0, 1, 2, 3], fraction=0.035, pad=0.02, shrink=0.6) cb.ax.set_yticklabels(["absent", "below\nreference", "present,\nno reference", "claim /\nreference met"], fontsize=10) cb.outline.set_visible(False) ax.set_title("Scenario 1 reaches both authorised claims; Option 2 reaches neither\n" "and Option 1 leaves four substrate classes uncovered", loc="left", fontsize=13.5, pad=42) ax.text(0, 1.025, "Shading = activity vs reference dose or claim condition (white = absent, dark = met); " "text = declared activity", transform=ax.transAxes, fontsize=9.5, color="#555555") fig.text(0.01, 0.005, "Dashed outline = changed from Option 2. Grading per SANA-2026-01, Sections 4–7. " "Reference doses derive from single small trials.", fontsize=8.5, color="#555555") plt.savefig("substrate_coverage.png", bbox_inches="tight", facecolor="white") print("Saved substrate_coverage.png")