Spaces:
Runtime error
Runtime error
Commit ·
9511d23
1
Parent(s): 721a1d8
feat(cp5): viz.png — per-frame titled PNGs via matplotlib Agg
Browse filesCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- proteus/viz/__init__.py +2 -0
- proteus/viz/png.py +58 -0
- tests/viz/test_png.py +36 -0
proteus/viz/__init__.py
CHANGED
|
@@ -6,6 +6,7 @@ panel) or to per-frame PNGs. matplotlib (the `viz` extra) is imported lazily by
|
|
| 6 |
the PNG writer only, so importing this package stays offline-safe.
|
| 7 |
"""
|
| 8 |
|
|
|
|
| 9 |
from proteus.viz.reconstruct import (
|
| 10 |
FrameMeta,
|
| 11 |
FrameStep,
|
|
@@ -20,4 +21,5 @@ __all__ = [
|
|
| 20 |
"TraceReconstructionError",
|
| 21 |
"reconstruct",
|
| 22 |
"render_terminal",
|
|
|
|
| 23 |
]
|
|
|
|
| 6 |
the PNG writer only, so importing this package stays offline-safe.
|
| 7 |
"""
|
| 8 |
|
| 9 |
+
from proteus.viz.png import write_pngs
|
| 10 |
from proteus.viz.reconstruct import (
|
| 11 |
FrameMeta,
|
| 12 |
FrameStep,
|
|
|
|
| 21 |
"TraceReconstructionError",
|
| 22 |
"reconstruct",
|
| 23 |
"render_terminal",
|
| 24 |
+
"write_pngs",
|
| 25 |
]
|
proteus/viz/png.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Write reconstructed frames as per-frame PNGs (headless, matplotlib Agg).
|
| 2 |
+
|
| 3 |
+
Each frame is upscaled via the vendored frame_to_rgb_array and saved as
|
| 4 |
+
frame_000.png, frame_001.png ... with a one-line title (turn + action). The rich
|
| 5 |
+
side panel is a terminal feature; PNGs stay clean images with a label. matplotlib
|
| 6 |
+
is the optional `viz` extra and is imported lazily so the offline core never
|
| 7 |
+
needs it.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Union
|
| 14 |
+
|
| 15 |
+
from proteus.arc_grid.rendering import COLOR_MAP, frame_to_rgb_array
|
| 16 |
+
from proteus.viz.reconstruct import FrameStep
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _title(step: FrameStep) -> str:
|
| 20 |
+
meta = step.meta
|
| 21 |
+
if meta.phase == "cut":
|
| 22 |
+
return f"Cut {meta.index} (pre-roll)"
|
| 23 |
+
mark = "✓" if meta.was_congruent else "✗"
|
| 24 |
+
title = f"Turn {meta.turn_idx}: action={meta.action} {mark}"
|
| 25 |
+
if meta.terminal:
|
| 26 |
+
title += f" [{meta.terminal}]"
|
| 27 |
+
return title
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def write_pngs(
|
| 31 |
+
steps: list[FrameStep],
|
| 32 |
+
out_dir: Union[str, Path],
|
| 33 |
+
*,
|
| 34 |
+
scale: int = 32,
|
| 35 |
+
) -> list[Path]:
|
| 36 |
+
"""Render every frame to `<out_dir>/frame_NNN.png`; return the paths in order."""
|
| 37 |
+
import matplotlib
|
| 38 |
+
|
| 39 |
+
matplotlib.use("Agg") # headless: no window, no display required
|
| 40 |
+
import matplotlib.pyplot as plt
|
| 41 |
+
|
| 42 |
+
out_dir = Path(out_dir)
|
| 43 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 44 |
+
paths: list[Path] = []
|
| 45 |
+
for i, step in enumerate(steps):
|
| 46 |
+
# Real signature: frame_to_rgb_array(steps: int, frame: ndarray,
|
| 47 |
+
# scale: int = 4, color_map: Optional[dict] = None) -> ndarray
|
| 48 |
+
# The first `steps` arg is a display counter (ignored inside the function).
|
| 49 |
+
rgb = frame_to_rgb_array(0, step.frame, scale, COLOR_MAP)
|
| 50 |
+
fig, ax = plt.subplots(figsize=(4.0, 4.4))
|
| 51 |
+
ax.imshow(rgb, interpolation="nearest")
|
| 52 |
+
ax.axis("off")
|
| 53 |
+
ax.set_title(_title(step), fontsize=9)
|
| 54 |
+
path = out_dir / f"frame_{i:03d}.png"
|
| 55 |
+
fig.savefig(path, dpi=100, bbox_inches="tight")
|
| 56 |
+
plt.close(fig)
|
| 57 |
+
paths.append(path)
|
| 58 |
+
return paths
|
tests/viz/test_png.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
pytest.importorskip("matplotlib") # PNG path needs the `viz` extra
|
| 4 |
+
|
| 5 |
+
from proteus.agents import VanillaAgent
|
| 6 |
+
from proteus.providers import FakeProvider
|
| 7 |
+
from proteus.runtime.session import SessionRunner
|
| 8 |
+
from proteus.viz import reconstruct, write_pngs
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _steps():
|
| 12 |
+
agent = VanillaAgent(FakeProvider(["ACTION: up"]))
|
| 13 |
+
trace = SessionRunner(
|
| 14 |
+
"predator_evade", agent, seed=42, play_turns=4, use_probe=False,
|
| 15 |
+
).run()
|
| 16 |
+
return reconstruct(trace)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def test_write_pngs_one_nonempty_file_per_frame(tmp_path):
|
| 20 |
+
steps = _steps()
|
| 21 |
+
out_dir = tmp_path / "frames"
|
| 22 |
+
paths = write_pngs(steps, out_dir)
|
| 23 |
+
assert len(paths) == len(steps)
|
| 24 |
+
for p in paths:
|
| 25 |
+
assert p.exists()
|
| 26 |
+
assert p.stat().st_size > 0
|
| 27 |
+
# Files are zero-padded and ordered.
|
| 28 |
+
assert (out_dir / "frame_000.png").exists()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def test_write_pngs_creates_missing_dir(tmp_path):
|
| 32 |
+
steps = _steps()
|
| 33 |
+
nested = tmp_path / "a" / "b" / "frames"
|
| 34 |
+
paths = write_pngs(steps, nested)
|
| 35 |
+
assert nested.is_dir()
|
| 36 |
+
assert len(paths) == len(steps)
|