Skip to content

Commit e0a336c

Browse files
results: publish scenario PNG figures (top numeric series per scenario)
1 parent a0031d1 commit e0a336c

File tree

2 files changed

+28
-66
lines changed

2 files changed

+28
-66
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ figures/*
88
data/*
99
!data/.gitkeep
1010
.coverage
11+
# Matplotlib cache (do not commit)
12+
results/_mplconfig/
13+
results/_mplconfig/**

scripts/generate_figures.py

Lines changed: 25 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22

33
import json
44
import math
5-
import os
65
from pathlib import Path
7-
from typing import Any, Iterable, List, Optional, Tuple
6+
from typing import Any, Iterable, List, Tuple
87

9-
# Headless backend (no GUI)
108
import matplotlib
119
matplotlib.use("Agg", force=True)
1210

@@ -15,18 +13,14 @@
1513
from matplotlib.ticker import NullLocator
1614

1715
RESULTS_DIR = Path("results")
16+
MAX_PNG_PER_SCENARIO = 6 # profesional: no inundar el repo
1817

1918
def _is_num(x: Any) -> bool:
20-
if isinstance(x, (int, float)) and not isinstance(x, bool):
21-
return math.isfinite(float(x))
22-
return False
19+
return isinstance(x, (int, float)) and not isinstance(x, bool) and math.isfinite(float(x))
2320

2421
def _looks_like_series(v: Any) -> bool:
25-
if not isinstance(v, list):
22+
if not isinstance(v, list) or len(v) < 3:
2623
return False
27-
if len(v) < 3:
28-
return False
29-
# must be mostly numeric
3024
nums = sum(1 for x in v if _is_num(x))
3125
return nums >= int(0.9 * len(v))
3226

@@ -42,47 +36,28 @@ def _walk(obj: Any, prefix: str = "") -> Iterable[Tuple[str, Any]]:
4236
yield (p, v)
4337
yield from _walk(v, p)
4438

45-
def _pick_series(candidates: List[Tuple[str, List[float]]], prefer_terms: List[str]) -> Optional[Tuple[str, List[float]]]:
46-
# deterministic: stable sort by (priority, -len, key)
47-
scored = []
48-
for k, s in candidates:
49-
k_low = k.lower()
50-
pr = min([prefer_terms.index(t) for t in prefer_terms if t in k_low], default=10_000)
51-
scored.append((pr, -len(s), k, s))
52-
scored.sort(key=lambda x: (x[0], x[1], x[2]))
53-
if not scored:
54-
return None
55-
_, _, k, s = scored[0]
56-
return (k, s)
57-
58-
def _safe_plot_series(out_png: Path, title: str, y: List[float]) -> None:
39+
def _safe_plot(out_png: Path, title: str, y: List[float]) -> None:
5940
out_png.parent.mkdir(parents=True, exist_ok=True)
6041

6142
fig = Figure(figsize=(10, 4), dpi=140)
6243
canvas = FigureCanvas(fig)
63-
6444
ax = fig.add_subplot(111)
6545

66-
# CRÍTICO: eliminar ticks/locators para esquivar el bug de deepcopy en Py3.14 + mpl (ticks crean MarkerStyle)
46+
# Evita ticks/markers que en algunos entornos Py3.14+mpl disparan deepcopy recursivo
6747
ax.xaxis.set_major_locator(NullLocator())
6848
ax.yaxis.set_major_locator(NullLocator())
6949
ax.tick_params(bottom=False, left=False, labelbottom=False, labelleft=False)
7050

71-
# Plot
7251
x = list(range(len(y)))
7352
ax.plot(x, y)
7453

75-
# Etiquetas "manuales" (sin ticks)
7654
fig.text(0.01, 0.98, title, ha="left", va="top")
77-
fig.text(0.01, 0.02, f"n={len(y)} min={min(y):.3g} max={max(y):.3g}", ha="left", va="bottom")
55+
fig.text(0.01, 0.02, f"n={len(y)} min={min(y):.6g} max={max(y):.6g}", ha="left", va="bottom")
7856

79-
fig.tight_layout(rect=[0, 0.05, 1, 0.92])
57+
fig.tight_layout(rect=[0, 0.06, 1, 0.92])
8058
canvas.draw()
8159
fig.savefig(out_png)
8260

83-
def _load_json(path: Path) -> Any:
84-
return json.loads(path.read_text(encoding="utf-8"))
85-
8661
def generate() -> int:
8762
if not RESULTS_DIR.exists():
8863
raise SystemExit("Missing results/ directory. Run: python -m scripts.run_scenarios")
@@ -94,50 +69,34 @@ def generate() -> int:
9469
wrote = 0
9570

9671
for jf in json_files:
97-
obj = _load_json(jf)
72+
obj = json.loads(jf.read_text(encoding="utf-8"))
9873

99-
series_candidates: List[Tuple[str, List[float]]] = []
74+
candidates: List[Tuple[str, List[float]]] = []
10075
for k, v in _walk(obj):
10176
if _looks_like_series(v):
102-
series_candidates.append((k, [float(x) for x in v]))
103-
104-
# Si NO hay series numéricas, reporta claves reales para debugging (determinista)
105-
if not series_candidates:
106-
keys = []
107-
if isinstance(obj, dict):
108-
keys = sorted(list(obj.keys()))
109-
print(f"[generate_figures] {jf.name}: NO numeric series found. top-level keys={keys}")
110-
continue
111-
112-
# Selección determinista por prioridad textual
113-
o2_pick = _pick_series(series_candidates, prefer_terms=["o2", "oxygen"])
114-
w_pick = _pick_series(series_candidates, prefer_terms=["water", "h2o"])
77+
candidates.append((k, [float(x) for x in v]))
11578

116-
# Si no hay match semántico, NO adivinamos: reportamos candidatos y seguimos
117-
if o2_pick is None or w_pick is None:
118-
print(f"[generate_figures] {jf.name}: cannot select o2/water deterministically.")
119-
print(" candidates:")
120-
for k, s in sorted(series_candidates, key=lambda x: x[0])[:50]:
121-
print(f" - {k} (len={len(s)})")
79+
# Reporte determinista
80+
if not candidates:
81+
top_keys = sorted(list(obj.keys())) if isinstance(obj, dict) else []
82+
print(f"[generate_figures] {jf.name}: NO numeric series found. top-level keys={top_keys}")
12283
continue
12384

124-
base = jf.stem
125-
o2_key, o2_series = o2_pick
126-
w_key, w_series = w_pick
127-
128-
out_o2 = RESULTS_DIR / f"{base}_o2.png"
129-
out_w = RESULTS_DIR / f"{base}_water.png"
85+
# Ordena por longitud desc, y luego por key asc (determinista)
86+
candidates.sort(key=lambda kv: (-len(kv[1]), kv[0]))
13087

131-
_safe_plot_series(out_o2, f"{base} :: O2 (source={o2_key})", o2_series)
132-
_safe_plot_series(out_w, f"{base} :: Water (source={w_key})", w_series)
133-
134-
print(f"[generate_figures] wrote: {out_o2.as_posix()} {out_w.as_posix()}")
135-
wrote += 2
88+
base = jf.stem
89+
for i, (k, s) in enumerate(candidates[:MAX_PNG_PER_SCENARIO], start=1):
90+
out = RESULTS_DIR / f"{base}_series_{i:02d}.png"
91+
title = f"{base} :: series_{i:02d} (source={k})"
92+
_safe_plot(out, title, s)
93+
print(f"[generate_figures] wrote: {out.as_posix()} (len={len(s)})")
94+
wrote += 1
13695

13796
return wrote
13897

13998
if __name__ == "__main__":
14099
n = generate()
141100
if n == 0:
142-
raise SystemExit("No PNGs created. Either no numeric series exist in results/S*.json or selection could not be deterministic.")
101+
raise SystemExit("No PNGs created. Your results JSON contain no numeric series arrays.")
143102
print(f"OK: created {n} PNGs")

0 commit comments

Comments
 (0)