"""Writes section.html from horse_ladder.csv (run.py) and horse_monotonic.png (plot.py)."""
import csv
from pathlib import Path

R = Path(__file__).resolve().parent
rows = list(csv.DictReader((R / "horse_ladder.csv").open()))
old, new = ({int(r["atoms"]): r for r in rows if r["version"].startswith(p)} for p in ("v5.5", "decision"))
cell = lambda r, k: (r[k] or "no surface") if r else "–"
trs = []
for T in sorted(set(old) | set(new)):
    a, b = old.get(T), new.get(T)
    ratio = f"{float(b['chamfer_pct']) / float(a['chamfer_pct']):.3f}" if a and b and a["chamfer_pct"] and b["chamfer_pct"] else "–"
    trs.append(f"<tr><td>{T}</td><td>{cell(a, 'chamfer_pct')}</td><td>{cell(b, 'chamfer_pct')}</td><td>{ratio}</td>"
               f"<td>{cell(a, 'p95_pct')}</td><td>{cell(b, 'p95_pct')}</td></tr>")
import pandas as pd  # noqa: E402

d = pd.read_csv(R / "ladder.csv")
ORDER = ["mug", "mug_ace", "bowl", "bowl_serving", "bowl_dog", "flower_pot", "shoe", "jogger", "dino", "horse", "spring", "knot"]


def stats(o, v):
    g = d[(d.object == o) & (d.version == v)]
    c = g.dropna(subset=["chamfer_pct"])
    up = (c.chamfer_pct / c.chamfer_pct.shift(1) - 1).dropna() * 100
    return g, c, up


def ratio(o, T):
    a, b = (d[(d.object == o) & (d.version == v) & (d.atoms == T)].chamfer_pct for v in ("v5.5", "new"))
    return f"{float(b.iloc[0]) / float(a.iloc[0]):.2f}" if len(a) and len(b) and a.notna().all() and b.notna().all() else "–"


per = []
for o in ORDER:
    ga, ca, ua = stats(o, "v5.5")
    gb, cb, ub = stats(o, "new")
    worst = lambda u: f"+{u.max():.1f} %" if u.max() > 0.5 else "–"
    full = float(cb.chamfer_pct.iloc[-1]) / float(ca.chamfer_pct.iloc[-1])
    per.append(f"<tr><td style='text-align:left'>{o.replace('_', ' ')}</td><td>{ga.parts.iloc[0]} → {gb.parts.iloc[0]}</td>"
               f"<td>{ca.atoms.min()} → {cb.atoms.min()}</td><td>{(ua > 0.5).sum()} → {(ub > 0.5).sum()}</td><td>{worst(ua)} → {worst(ub)}</td>"
               f"<td>{ratio(o, 32)}</td><td>{ratio(o, 64)}</td><td>{ratio(o, 256)}</td><td>{ratio(o, 1024)}</td><td>{ratio(o, 4096)}</td><td>{full:.2f}</td></tr>")

html = f"""<h2>Monotonicity: error at each prefix, v5.5 against the new version</h2>
<p>Does the error always fall as atoms are added? One encode per object and version, every atom kept, decoded at 23–27 prefixes on a 128³ grid; a rise is a
step up of more than 0.5 % in Chamfer. <code>parts="tracked-fit"</code>, 256 layers, N 64, M 24; the 12 objects of the other pages, one run each.
v5.5 is 7ccd2a39. The new version is the <code>v5.5-encoder-fixes</code> branch (ca4ad834): a part's centre stored with its first shape atom (decision 33), plus
decision 32 and the changes to <code>_parts.py</code> in that branch. It keeps more parts (jogger 78 → 183, shoe 35 → 162), so <b>this compares the two versions, not decision 33 alone</b>.</p>
<figure><img src="sections/monotonic/ladder_monotonic.png" alt="Twelve log-log panels of Chamfer against atoms for v5.5 and the new version; rises are ringed, all at 64 atoms or fewer">
<figcaption>Chamfer against atoms, log-log, one panel per object. Ringed: rises of more than 0.5 %.</figcaption></figure>
<p><b>Monotonicity:</b> from 64 atoms up the error falls at every step on all 12 objects in both versions (largest rise above that: +0.07 %). Below it neither version is
monotone: 6 rises on 5 objects in v5.5 (worst +30 %, dog bowl), 4 rises on 3 objects in the new version (worst +25 %, mug ace); the rises move rather than go away.
<b>Low budgets:</b> the new version is mixed: much better on the dino (surface from 8 atoms, against 32) and the horse, worse on others (flower pot 3.1× at 8 atoms, serving bowl 2.3× at 16, mug 1.6× at 32),
and the spring needs 192 atoms for a surface, against 128. <b>From 256 atoms:</b> the two versions stay within 13 % of each other; the shoe and the jogger, where the part count jumps, are 5–13 % worse in the new version at 512–4096 atoms.
<b>All atoms:</b> the new version is better on 10 objects (by up to 13 %) and identical on the spring and the knot.</p>
<div class="tablewrap"><table>
<thead><tr><th style='text-align:left'>object</th><th>parts</th><th>first surface, atoms</th><th>rises</th><th>worst rise</th>
<th>new / v5.5 at 32 atoms</th><th>at 64</th><th>at 256</th><th>at 1024</th><th>at 4096</th><th>all atoms</th></tr></thead>
<tbody>
{chr(10).join(per)}
</tbody>
</table></div>
<p>Pairs read v5.5 → new. Ratios below 1 favour the new version; – where a version has no surface.</p>
<h2>The horse in detail</h2>
<p>One encode of the horse per version (<code>parts="tracked-fit"</code>, 256 layers, N 64, M 24, every atom), decoded on a 128³ grid at
25 prefixes. Current version: v5.5 (7ccd2a39), 20 parts, 55446 atoms. New: the <code>v5.5-encoder-fixes</code> branch
(ca4ad834), where a part's centre is stored with its first shape atom (decision 33), 21 parts, 55587 atoms. Chamfer and p95 in % of the
diagonal. One object, one run.</p>
<figure><img src="sections/monotonic/horse_monotonic.png" alt="Log-log plots of Chamfer and p95 error against atoms for v5.5 and the new version; the curves coincide from about 256 atoms; each has one small rise below 16 atoms">
<figcaption>Error against atoms, log-log. Ringed: the steps where the error rises.</figcaption></figure>
<figure><img src="sections/monotonic/horse_centre_order.png" alt="Horse decoded at 8 to 128 atoms: the same atoms in the current order (top, no surface at 8 atoms) and with each part's centre ranked with its first shape atom (bottom), beside the ground truth">
<figcaption>The order alone: one encode (21 parts) with the same atoms and values, decoded at 8–128 atoms. Top, the order before decision 33; bottom, each part's centre ranked with its first shape atom, which draws a surface at 8 atoms and the legs earlier. A prototype of the reorder, so its numbers differ slightly from the new version's curve above (4.54 % against 4.35 % at 8 atoms).</figcaption></figure>
<p><b>Low budgets:</b> the new order gives a surface at 8 atoms (4.35 %, v5.5 has none) and a lower error at every prefix up to 192 atoms
(1.15 % against 1.59 % at 64 atoms). <b>Monotonicity:</b> neither version is strictly monotone. v5.5 rises once, 12→16 atoms (+1.3 %);
the new version rises once by a similar amount, 8→12 (+1.1 %), then stays flat at 12→16 (+0.01 %) and 24→32 (−0.04 %). Every later step falls
in both. <b>High budgets:</b> from 256 atoms the two agree within 0.72 % at every prefix.</p>
<div class="tablewrap"><table>
<thead><tr><th>atoms</th><th>Chamfer %, v5.5</th><th>Chamfer %, new</th><th>new / v5.5</th><th>p95 %, v5.5</th><th>p95 %, new</th></tr></thead>
<tbody>
{chr(10).join(trs)}
</tbody>
</table></div>
"""
(R / "section.html").write_text(html)
