Skip to content

zymera.viz.report

report

Self-contained HTML report: embedded episode GIF + curves + heatmap.

make_report

make_report(traj, path, *, env=None, title='zymera report', fps=6, comm_radius=None)

Write a single-file HTML report for a rollout(..., keep="all") dict.

Sections: episode GIF, coverage-over-time, per-term reward curves (when the rollout used collect=("reward_terms",)), final visit heatmap, and the env composition when env is given.

Source code in zymera/viz/report.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def make_report(traj, path: str, *, env=None, title: str = "zymera report",
                fps: int = 6, comm_radius: Optional[int] = None) -> str:
    """Write a single-file HTML report for a ``rollout(..., keep="all")`` dict.

    Sections: episode GIF, coverage-over-time, per-term reward curves (when the
    rollout used ``collect=("reward_terms",)``), final visit heatmap, and the
    env composition when ``env`` is given.
    """
    worlds = traj["world"]
    if comm_radius is None and env is not None:
        topo = getattr(getattr(env, "channel", None), "topology", None)
        comm_radius = getattr(topo, "radius", None)

    frames = render_frames(worlds, comm_radius=comm_radius)
    gif64 = _b64_gif(frames, fps)

    covered = np.asarray(worlds.seen_by).any(1)                # (T+1, H, W)
    cov_curve = covered.reshape(covered.shape[0], -1).mean(-1)
    fig, ax = plt.subplots(figsize=(5, 2.4))
    ax.plot(cov_curve, color="#8e2c1f")
    ax.set_xlabel("step"), ax.set_ylabel("coverage"), ax.set_ylim(0, 1)
    ax.grid(alpha=0.25)
    cov64 = _b64_fig(fig)

    terms_html = ""
    terms = traj.get("info", {}).get("reward_terms")
    if terms:
        fig, ax = plt.subplots(figsize=(5, 2.4))
        for name, arr in sorted(terms.items()):
            ax.plot(np.asarray(arr).mean(-1), label=name, lw=1.2)
        ax.set_xlabel("step"), ax.set_ylabel("unweighted term mean")
        ax.legend(fontsize=7), ax.grid(alpha=0.25)
        terms_html = (
            "<h2>Reward terms</h2>"
            f'<img alt="per-term reward curves" src="data:image/png;base64,{_b64_fig(fig)}">'
        )

    fig, ax = plt.subplots(figsize=(3.2, 3.2))
    ax.imshow(np.asarray(worlds.explored)[-1], cmap="YlGn", origin="upper")
    ax.set_xticks([]), ax.set_yticks([]), ax.set_title("visit counts", fontsize=9)
    heat64 = _b64_fig(fig)

    env_html = ""
    if env is not None:
        env_html = f"<h2>Env composition</h2><pre>{html.escape(repr(env))}</pre>"

    doc = f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>{html.escape(title)}</title>
<style>
 body {{ font-family: Charter, 'Iowan Old Style', Georgia, serif; margin: 2rem auto;
        max-width: 46rem; color: #1a1816; background: #f7f4ee; }}
 h1, h2 {{ font-family: 'Avenir Next', Avenir, 'Helvetica Neue', sans-serif; }}
 h1 {{ border-bottom: 2px solid #8e2c1f; padding-bottom: .3rem; }}
 img {{ max-width: 100%; border: 1px solid #d8d2c4; border-radius: 6px; }}
 pre {{ background: #211e1b; color: #f0ece2; padding: .8rem 1rem; border-radius: 6px;
       font: 12px 'SF Mono', Menlo, monospace; overflow-x: auto; }}
</style></head><body>
<h1>{html.escape(title)}</h1>
<h2>Episode</h2><img alt="episode gif" src="data:image/gif;base64,{gif64}">
<h2>Coverage over time</h2><img alt="coverage curve" src="data:image/png;base64,{cov64}">
{terms_html}
<h2>Final visit heatmap</h2><img alt="visit heatmap" src="data:image/png;base64,{heat64}">
{env_html}
</body></html>"""
    with open(path, "w") as f:
        f.write(doc)
    return path