"""Plot the measured decay times against the ones the equations predict.

One figure. Every point is one frequency band at one setting, taken from
the table in the section on measurements: the horizontal position is the
band target equation (30) asks for, the vertical position is what the
offline tool measured from a rendered impulse response. The diagonal is
exact agreement.

The numbers are typed in from that table rather than recomputed, because
the measurements come from the compiled plugin and cannot be reproduced
here. The script prints the spread it draws, so the caption and the plot
cannot drift apart.

Writes decay.svg beside this file.
"""
import numpy as np
import matplotlib
matplotlib.use("Agg")
matplotlib.rcParams["svg.fonttype"] = "path"
import matplotlib.pyplot as plt

TEAL, INDIGO, ROSE = "#00706B", "#3A49A6", "#A93356"
INK, MUTED, GRID = "#0D1620", "#5A6874", "#D3DBE3"

PROPERTY = {TEAL: "--teal", INDIGO: "--indigo", ROSE: "--rose",
            INK: "--ink", MUTED: "--muted", GRID: "--line"}

# setting, (low, mid, high) predicted, (low, mid, high) measured
ROWS = [
    ("Hall 2.4 s, Size 0.5",     (3.12, 2.40, 1.74), (2.90, 2.27, 1.93)),
    ("Hall 2.4 s, Size 0",       (3.12, 2.40, 2.16), (2.95, 2.36, 2.24)),
    ("Hall 2.4 s, Size 1",       (3.12, 2.40, 1.32), (2.93, 2.20, 1.60)),
    ("Cathedral 6 s, Size 0.5",  (7.80, 6.00, 4.35), (7.36, 5.65, 4.80)),
    ("Hall 2.4 s, Tilt +1",      (2.34, 2.40, 2.78), (2.35, 2.53, 2.65)),
    ("Hall 2.4 s, Tilt -1",      (3.90, 2.40, 0.70), (3.48, 2.24, 1.09)),
]
BANDS = [("low", INDIGO), ("mid", TEAL), ("high", ROSE)]


def save(fig, name):
    """Write the figure, then make it the page's own drawing.

    Matplotlib writes every colour into the SVG as a literal, so a figure
    saved the ordinary way carries fixed values and cannot follow the page
    between light and dark. Each literal becomes var(--prop, literal) here.
    """
    import re

    fig.tight_layout()
    fig.savefig(name, transparent=True, bbox_inches="tight", pad_inches=0.04)
    text = open(name, encoding="utf-8").read()
    stem = re.sub(r"[^A-Za-z0-9]+", "-", name.rsplit(".", 1)[0]).strip("-")
    text = re.sub(r'(\sid=")([^"]+)"', lambda m: f'{m.group(1)}{stem}-{m.group(2)}"', text)
    text = re.sub(r'((?:xlink:)?href="#)([^"]+)"',
                  lambda m: f'{m.group(1)}{stem}-{m.group(2)}"', text)
    text = re.sub(r'(url\(#)([^)]+)\)', lambda m: f'{m.group(1)}{stem}-{m.group(2)})', text)
    for literal, prop in PROPERTY.items():
        text = re.sub(re.escape(literal), f"var({prop}, {literal.lower()})",
                      text, flags=re.IGNORECASE)
    with open(name, "w", encoding="utf-8") as f:
        f.write(text)
    print(f"{name} written")


fig, ax = plt.subplots(figsize=(5.8, 5.0))

lo, hi = 0.4, 8.6
ax.plot([lo, hi], [lo, hi], lw=1.2, color=MUTED, ls=(0, (5, 3)), zorder=1)
# The diagonal is at 45 degrees in data terms, and both axes are logarithmic
# and equal, so the label follows it at 45 degrees on the page as well.
ax.annotate("exact agreement", (1.25, 1.25), ha="left", va="bottom",
            fontsize=9, color=MUTED, rotation=45, rotation_mode="anchor")

for b, (band, colour) in enumerate(BANDS):
    xs = [row[1][b] for row in ROWS]
    ys = [row[2][b] for row in ROWS]
    ax.plot(xs, ys, "o", ms=7, color=colour, alpha=0.85, zorder=3, label=band)
    err = [100.0 * (y / x - 1.0) for x, y in zip(xs, ys)]
    print(f"{band:5s} band: error {min(err):+6.1f}% to {max(err):+6.1f}%, "
          f"largest magnitude {max(abs(e) for e in err):.1f}%")

allerr = [100.0 * (row[2][b] / row[1][b] - 1.0)
          for row in ROWS for b in range(3)]
mid = [100.0 * (row[2][1] / row[1][1] - 1.0) for row in ROWS]
print(f"mid band across every setting: {min(mid):+.1f}% to {max(mid):+.1f}%")
print(f"every band: worst {max(abs(e) for e in allerr):.1f}%")

ax.set_xscale("log")
ax.set_yscale("log")
ax.set_xlim(lo, hi)
ax.set_ylim(lo, hi)
ax.set_aspect("equal")
# Matplotlib adds its own minor decade labels on a log axis, which collide
# with these. Turning the minor ticks off leaves only the ones named here.
ticks = [0.5, 1, 2, 4, 8]
ax.set_xticks(ticks); ax.set_yticks(ticks)
ax.set_xticklabels([str(t) for t in ticks])
ax.set_yticklabels([str(t) for t in ticks])
ax.minorticks_off()
ax.set_xlabel("decay time the equations predict (s)", fontsize=10, color=INK)
ax.set_ylabel("decay time measured (s)", fontsize=10, color=INK)
ax.tick_params(which="both", colors=MUTED, labelsize=9)
ax.grid(True, which="major", lw=0.6, color=GRID, alpha=0.7)
for side in ("top", "right"):
    ax.spines[side].set_visible(False)
for side in ("left", "bottom"):
    ax.spines[side].set_color(GRID)
leg = ax.legend(loc="upper left", frameon=False, fontsize=9.5, title="band")
leg.get_title().set_color(INK)
for t in leg.get_texts():
    t.set_color(INK)

save(fig, "decay.svg")
