#!/usr/bin/env python # -*- coding: utf-8 -*- """ plot_comparison.py — Figura comparativa entre dos dias de campo (14 Sep vs 21 Sep 2025, Aguascalientes, UTC-6). Cada dia se evalua contra su PROPIO techo de referencia P_ref(t) (alineacion focal ideal, Ecuacion del techo de potencia), de modo que la comparativa muestra la reproducion entre sesiones y no depende de un dataset externo de control. Lee los salidas ya generadas por parse_log.py: articulo/resultados_/samples.csv (t, G_POA, P_CPV, P_ref, ...) articulo/resultados_/metrics.json (E, H, E/H, eta_tr, MAE_P, ...) y produce: articulo/figs/fig_comparison_14sep_21sep.{pdf,png} Uso: py -3.12 plot_comparison.py (Requiere haber corrido antes: py -3.12 parse_log.py gyuggyhoy.txt y py -3.12 parse_log.py 21092025.txt ) """ import io import os import json from pathlib import Path import numpy as np import pandas as pd import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import matplotlib.dates as mdates BASE = Path(__file__).resolve().parent FIGS = BASE / "articulo" / "figs" # (tag del parser = stem del log, etiqueta para la figura) # 14 Sep esta representado por su captura B (gyuggyhoy); 21 Sep por la # sesion completa que es la sesion "cabecera" del articulo. DAYS = [ ("gyuggyhoy", "14 September 2025 (capture B)"), ("21092025", "21 September 2025 (headline session)"), ] def load(tag): d = BASE / "articulo" / f"resultados_{tag}" csv = d / "samples.csv" if not csv.exists(): raise SystemExit( f"Falta {csv}. Ejecuta antes: py -3.12 parse_log.py {tag}.txt") df = pd.read_csv(csv, parse_dates=["timestamp"]) with open(d / "metrics.json", encoding="utf-8") as f: m = json.load(f) return df, m def savefig_safe(fig, stem): """Renderiza en memoria y escribe en disco (delete-then-create). Evita el bloqueo de archivo que OneDrive causa con savefig directo.""" FIGS.mkdir(parents=True, exist_ok=True) for ext, kw in (("pdf", {}), ("png", {"dpi": 200})): path = FIGS / f"{stem}.{ext}" if path.exists(): os.remove(str(path)) buf = io.BytesIO() fig.savefig(buf, format=ext, **kw) buf.seek(0) with open(str(path), "wb") as f: f.write(buf.read()) print(" saved", path) def main(): fig, axes = plt.subplots(len(DAYS), 1, figsize=(9.2, 6.6), dpi=200) axes = np.atleast_1d(axes) for ax, (tag, label) in zip(axes, DAYS): df, m = load(tag) fs = m.get("full_session") or {} t = df["timestamp"].values lit = (df["G_POA"].values > 50.0) ax.plot(t, df["P_CPV"].values * 1000, color="tab:red", lw=0.9, label="$P_{\\mathrm{CPV}}$ (tracked)") ax.plot(t, df["P_ref"].values * 1000, color="k", ls="--", lw=1.0, label="$P_{\\mathrm{ref}}$ (reference ceiling)") ax.fill_between(t, df["P_CPV"].values * 1000, df["P_ref"].values * 1000, where=lit, color="tab:red", alpha=0.10, label="Loss vs. $P_{\\mathrm{ref}}$") ax2 = ax.twinx() ax2.plot(t, df["G_POA"].values, color="tab:orange", lw=0.6, alpha=0.6, label="$G_{\\mathrm{POA}}$") ax2.set_ylabel("Irradiance $G_{\\mathrm{POA}}$ (W/m$^2$)", color="tab:orange") ax2.tick_params(axis="y", labelcolor="tab:orange") ax.set_ylabel("Power (mW)", color="tab:red") ax.tick_params(axis="y", labelcolor="tab:red") ax.set_ylim(bottom=0) ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) ax.tick_params(axis="x", labelsize=8) h1, l1 = ax.get_legend_handles_labels() h2, l2 = ax2.get_legend_handles_labels() ax.legend(h1 + h2, l1 + l2, loc="upper right", fontsize=7, framealpha=0.6) ax.grid(alpha=0.3) ax.set_title( f"{label} — $\\eta_{{tr}}$ = {fs.get('eta_tr_pct', 'n/a')}% " f"MAE$_P$ = {fs.get('MAE_P_mW', 'n/a')} mW " f"$E$ = {fs.get('E_Wh', 'n/a')} Wh " f"$H$ = {fs.get('H_kWh_m2', 'n/a')} kWh/m$^2$", fontsize=9.5) # No global suptitle: it overflowed the figure width (clipped at both # edges) and duplicated the LaTeX \caption. The per-panel titles below # carry the per-day metrics; the article caption carries the description. fig.tight_layout() savefig_safe(fig, "fig_comparison_14sep_21sep") plt.close(fig) print("DONE ->", FIGS) if __name__ == "__main__": main()