# -*- coding: utf-8 -*- """Heatmap reutilizable de posicion solar real (MicrotrackingFer). Uso: py -3.12 make_heatmap.py [ruta_salida.png] Mapeo adc_manager.cpp: C=canal1(fotocorriente), P=canal2(piranometro), V=canal3(Voc). Calibracion parse_log.py (ADS122C04 24-bit, Vref 2.048 V). P del log es 24-bit 2's complement sin signo; de noche ~16.7M => se extiende signo y se apoda <0 a 0. """ import re import sys import os import numpy as np import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib.colors import Normalize ADC_MAX = 8388607.0 GAIN_SHUNT = 20.0 R_SHUNT = 0.075 def isc_ma(c_raw): vsh_mV = c_raw / ADC_MAX * 2048.0 / GAIN_SHUNT return (vsh_mV / 1000.0) / R_SHUNT * 1000.0 def g_wm2(p_signed): return p_signed / ADC_MAX * 2048.0 / 2.0 def sign24(raw): raw &= 0xFFFFFF if raw & 0x00800000: raw -= 0x1000000 return raw RE = re.compile( r"LOG:\[(?P[^\]]+)\]\s*" r"X:(?P-?\d+),Y:(?P-?\d+),C:(?P-?\d+),V:(?P-?\d+)," r"P:(?P

-?\d+),S:(?P-?\d+),er:(?P-?\d+)" ) def main(): if len(sys.argv) < 2: print("Uso: make_heatmap.py [ruta_salida.png]") return log_path = sys.argv[1] base = os.path.splitext(os.path.basename(log_path))[0] out_png = sys.argv[2] if len(sys.argv) > 2 else \ os.path.join(os.path.dirname(os.path.abspath(__file__)), base + ".png") ts, xs, ys, cs, ps = [], [], [], [], [] with open(log_path, "r", encoding="utf-8", errors="replace") as f: for line in f: m = RE.search(line) if m: ts.append(m.group("t")) xs.append(int(m.group("x"))) ys.append(int(m.group("y"))) cs.append(int(m.group("c"))) ps.append(sign24(int(m.group("p")))) if len(xs) < 10: print("!! %s: solo %d lineas LOG parseadas; se omite." % (base, len(xs))) return xs = np.array(xs); ys = np.array(ys); cs = np.array(cs, dtype=float) ps = np.array(ps, dtype=float) p_irr = np.maximum(0.0, ps) c_mA = isc_ma(cs) g_w = g_wm2(p_irr) print("%s: %d muestras, %s -> %s" % (base, len(xs), ts[0], ts[-1])) print(" X %d..%d Y %d..%d" % (xs.min(), xs.max(), ys.min(), ys.max())) print(" Pico C : X=%d Y=%d Isc=%.1f mA" % (xs[np.argmax(c_mA)], ys[np.argmax(c_mA)], c_mA.max())) print(" Pico P : X=%d Y=%d G=%.1f W/m2" % (xs[np.argmax(p_irr)], ys[np.argmax(p_irr)], g_w.max())) XMIN, XMAX, YMIN, YMAX = 0, 120000, 30000, 85000 XMIN = min(XMIN, xs.min()); XMAX = max(XMAX, xs.max()) YMIN = min(YMIN, ys.min()); YMAX = max(YMAX, ys.max()) xbins = np.linspace(XMIN, XMAX, 61) ybins = np.linspace(YMIN, YMAX, 46) def bin_max(x, y, vals): xi = np.clip(np.searchsorted(xbins, x, side="right") - 1, 0, len(xbins) - 2) yi = np.clip(np.searchsorted(ybins, y, side="right") - 1, 0, len(ybins) - 2) grid = np.full((len(ybins) - 1, len(xbins) - 1), -np.inf) np.maximum.at(grid, (yi, xi), vals) grid[grid == -np.inf] = np.nan return grid maxC = bin_max(xs, ys, c_mA) maxP = bin_max(xs, ys, g_w) nC = np.nan_to_num(maxC / np.nanmax(maxC)) nP = np.nan_to_num(maxP / np.nanmax(maxP)) co = np.ma.masked_invalid(np.minimum(nC, nP)) xc = (xbins[:-1] + xbins[1:]) / 2 yc = (ybins[:-1] + ybins[1:]) / 2 w = np.where(co > 0.5 * np.nanmax(co), co, 0.0) if w.sum() > 0: com_x = np.nansum(w * xc[None, :]) / np.nansum(w) com_y = np.nansum(w * yc[:, None]) / np.nansum(w) else: com_x = com_y = np.nan pi, pj = np.unravel_index(np.nanargmax(co), co.shape) co_x, co_y = xc[pj], yc[pi] print(" Co-pico: X~%.0f Y~%.0f | Centro de masa: X~%.0f Y~%.0f" % (co_x, co_y, com_x, com_y)) fig, axes = plt.subplots(1, 3, figsize=(20, 6.2), sharex=True, sharey=True) cmap = "inferno" def draw_heat(ax, grid, title, cunit): im = ax.imshow(np.ma.masked_invalid(grid), extent=[xbins[0], xbins[-1], ybins[0], ybins[-1]], origin="lower", aspect="auto", cmap=cmap, norm=Normalize(vmin=0, vmax=np.nanmax(grid))) ax.set_title(title) ax.set_xlabel("X (paso)") ax.set_ylabel("Y (paso)") fig.colorbar(im, ax=ax, label=cunit) draw_heat(axes[0], maxC, "Fotocorriente C (max por celda)", "Isc [mA]") draw_heat(axes[1], maxP, "Piranometro P (max por celda)", "G [W/m$^2$]") draw_heat(axes[2], co, "Co-pico min(C*,P*) -> pos. solar real", "normalizado") ci, cj = np.unravel_index(np.nanargmax(maxC), maxC.shape) axes[0].plot(xc[cj], yc[ci], "w*", ms=16, mec="white", mew=1.2, zorder=5) pi_, pj_ = np.unravel_index(np.nanargmax(maxP), maxP.shape) axes[1].plot(xc[pj_], yc[pi_], "w*", ms=16, mec="white", mew=1.2, zorder=5) axes[2].plot(xs, ys, "-", color="cyan", lw=0.25, alpha=0.35, zorder=2, label="trayectoria (t)") axes[2].plot(co_x, co_y, "o", ms=16, mfc="none", mec="lime", mew=2.2, zorder=6, label="co-pico (sol real)") axes[2].plot(com_x, com_y, "x", ms=14, color="yellow", mew=2.4, zorder=7, label="centro de masa") axes[2].plot(xc[cj], yc[ci], "+", ms=14, color="red", mew=2.0, zorder=7, label="pico C") axes[2].plot(xc[pj_], yc[pi_], "+", ms=14, color="orange", mew=2.0, zorder=7, label="pico P") axes[2].legend(loc="upper left", fontsize=8, framealpha=0.8) fig.suptitle("Heatmap de posicion solar real - MicrotrackingFer %s (%d muestras)" % (base, len(xs)), fontsize=13, y=1.00) fig.tight_layout() fig.savefig(out_png, dpi=150, bbox_inches="tight") print(" PNG: %s" % out_png) if __name__ == "__main__": main()