Files
simulatore-induttori/plot_animazione.py
T

282 lines
10 KiB
Python
Raw Normal View History

2026-08-03 00:18:00 +02:00
# Animazione della superficie della fascetta durante il passaggio delle
# sorgenti.
#
# Riproduce la fisica di run_0001 (stesso seed di simulate.py) e mostra:
2026-08-03 00:18:00 +02:00
# - il profilo di flusso termico q(x) lungo la generatrice delle sorgenti;
# - il campo di temperatura sulla superficie cilindrica sviluppata in piano,
# cioè T(x, arco) con l'arco centrato sulla generatrice del sensore;
# - il sensore infrarosso e la temperatura nel punto osservato.
2026-08-03 00:18:00 +02:00
#
# La mappa sviluppata è periodica in verticale: il bordo superiore e quello
# inferiore sono lo stesso punto della fascetta.
import random
from pathlib import Path
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation, PillowWriter
2026-08-03 00:18:00 +02:00
from matplotlib.collections import LineCollection
2026-08-03 00:18:00 +02:00
import sensore as sens
import sorgente as src
from config import SIMULAZIONE
2026-08-03 00:18:00 +02:00
from mesh import campo_su_griglia, chiudi_campo
from simulate import (
2026-08-03 00:18:00 +02:00
campo_iniziale,
configurazione_randomizzata,
2026-08-03 00:18:00 +02:00
passo_termico,
prepara_stato_termico,
)
# Istante di inizio dei fotogrammi mostrati (la simulazione parte comunque da 0).
T_INIZIO_ANIMAZIONE_S = 0.40
# Istante di fine dell'animazione.
T_FINE_ANIMAZIONE_S = 30
# Tempo simulato tra un fotogramma e il successivo.
DT_FRAME_S = 0.05
# Millisecondi tra i fotogrammi in riproduzione.
INTERVALLO_RIPRODUZIONE_MS = 30
2026-08-03 00:18:00 +02:00
# Disegna i bordi degli elementi shell sopra il campo di temperatura.
# Con la mesh di default (100 x 110, elementi di 1 x 2 mm) il reticolo è più
# fitto dei pixel disponibili e copre il campo: per ispezionare la mesh
# conviene attivarlo e insieme abbassare n_elementi_x e n_elementi_theta in
# config.py, per esempio a 25 x 28.
MOSTRA_MESH = False
def simula_campi(cfg_run: dict) -> dict:
# Esegue la simulazione fino a T_FINE_ANIMAZIONE_S salvando, a ogni
# fotogramma, campo di temperatura, profilo di flusso e stato del sensore.
2026-08-03 00:18:00 +02:00
#
# I campi sono restituiti come griglie (n_nodi_x, n_elementi_theta) con la
# colonna dell'arco ruotata in modo da portare la generatrice del sensore
# (theta = 0) al centro: è solo una scelta di visualizzazione.
fascetta = cfg_run["fascetta"]
aria = cfg_run["aria"]
sorgente = cfg_run["sorgente"]
sensore = cfg_run["sensore"]
2026-08-03 00:18:00 +02:00
stato = prepara_stato_termico(fascetta, aria, sorgente, sensore)
mesh = stato["mesh"]
dt = stato["dt_s"]
2026-08-03 00:18:00 +02:00
interpolatore = stato["interpolatore_sensore"]
2026-08-03 00:18:00 +02:00
# Dopo np.roll(..., rotazione) la colonna theta = 0 finisce all'indice
# "rotazione", quindi l'arco con segno della colonna m vale
# (m - rotazione) * lato_arco.
rotazione = mesh["n_elementi_theta"] // 2
arco_centrato_m = (
np.arange(mesh["n_elementi_theta"]) - rotazione
) * mesh["lato_arco_m"]
2026-08-03 00:18:00 +02:00
T = campo_iniziale(stato)
T_letta = sens.temperatura_osservata_C(interpolatore, T)
tempi, campi, flussi, x_riferimenti = [], [], [], []
T_vere, T_lette = [], []
prossimo_frame_t = 0.0
t = 0.0
while t <= T_FINE_ANIMAZIONE_S + 1e-12:
2026-08-03 00:18:00 +02:00
T, info = passo_termico(stato, T, t)
T_vera = sens.temperatura_osservata_C(interpolatore, T)
T_letta = sens.aggiorna_inerzia_C(
T_letta, T_vera, dt, sensore["costante_tempo_s"]
)
if t + 1e-12 >= prossimo_frame_t:
2026-08-03 00:18:00 +02:00
_, q_nodi = src.profilo_flusso_x_W_m2(
sorgente, stato["x_sensore_m"], t, mesh["x_nodi_m"]
)
tempi.append(t)
2026-08-03 00:18:00 +02:00
campi.append(np.roll(campo_su_griglia(mesh, T), rotazione, axis=1))
flussi.append(q_nodi)
x_riferimenti.append(info["x_riferimento_m"])
T_vere.append(T_vera)
T_lette.append(T_letta)
prossimo_frame_t += DT_FRAME_S
t += dt
return {
"tempi": np.array(tempi),
"campi": campi,
"flussi": flussi,
"x_riferimenti": np.array(x_riferimenti),
"T_vere": np.array(T_vere),
"T_lette": np.array(T_lette),
2026-08-03 00:18:00 +02:00
"mesh": mesh,
"rotazione": rotazione,
"x_nodi_mm": mesh["x_nodi_m"] * 1000.0,
"arco_centrato_mm": arco_centrato_m * 1000.0,
"arco_chiuso_mm": np.append(
arco_centrato_m, arco_centrato_m[-1] + mesh["lato_arco_m"]
) * 1000.0,
"lunghezza_mm": fascetta["lunghezza_mm"],
"x_sensore_mm": sensore["x_mm"],
2026-08-03 00:18:00 +02:00
"offset_sorgente_mm": sorgente["offset_y_percorso_m"] * 1000.0,
"sorgente": sorgente,
"T_ambiente_C": aria["temperatura_ambiente_C"],
}
def main() -> None:
rng = random.Random(SIMULAZIONE["seed"])
cfg_run = configurazione_randomizzata(1, rng)
dati = simula_campi(cfg_run)
tempi = dati["tempi"]
indice_inizio = int(np.searchsorted(tempi, T_INIZIO_ANIMAZIONE_S))
n_frame = len(tempi) - indice_inizio
lunghezza_mm = dati["lunghezza_mm"]
x_vista_mm = (-10.0, lunghezza_mm + 10.0)
2026-08-03 00:18:00 +02:00
arco_mm = dati["arco_chiuso_mm"]
arco_min_mm, arco_max_mm = arco_mm[0], arco_mm[-1]
2026-08-03 00:18:00 +02:00
# Il minimo evita un asse degenere se la finestra dell'animazione si
# chiude prima che le sorgenti entrino nella fascetta.
q_max_MW = max(max(q.max() for q in dati["flussi"]) / 1e6, 1e-3)
T_max = max(c.max() for c in dati["campi"])
sorgente = dati["sorgente"]
numero_sorgenti = sorgente.get("numero_sorgenti", 1)
distanza_mm = sorgente.get("distanza_sorgenti_m", 0.0) * 1000.0
2026-08-03 00:18:00 +02:00
offset_mm = dati["offset_sorgente_mm"]
2026-08-03 00:18:00 +02:00
fig, (ax_flusso, ax_superficie, ax_storia) = plt.subplots(
3, 1, figsize=(10, 8), height_ratios=[1.0, 1.6, 1.2],
gridspec_kw={"hspace": 0.45},
)
2026-08-03 00:18:00 +02:00
fig.suptitle("Superficie sviluppata della fascetta: sorgenti in transito e sensore")
2026-08-03 00:18:00 +02:00
# Pannello 1: profilo di flusso lungo x e posizioni delle sorgenti.
linea_flusso, = ax_flusso.plot([], [], color="tab:red")
marker_sorgenti, = ax_flusso.plot(
[], [], "v", color="tab:red", markersize=10, clip_on=False
)
ax_flusso.annotate(
"verso di marcia",
xy=(0.28, 0.85), xytext=(0.55, 0.85), xycoords="axes fraction",
arrowprops={"arrowstyle": "->", "color": "gray"},
color="gray", va="center",
)
ax_flusso.set_xlim(*x_vista_mm)
ax_flusso.set_ylim(0.0, q_max_MW * 1.25)
ax_flusso.set_ylabel("q(x) [MW/m²]")
ax_flusso.grid(True, alpha=0.3)
ax_flusso.set_xticklabels([])
2026-08-03 00:18:00 +02:00
# Pannello 2: temperatura sulla superficie sviluppata. L'asse verticale è
# l'arco circonferenziale, con 0 sulla generatrice del sensore.
#
# pcolormesh con shading "gouraud" interpola linearmente tra i valori
# nodali: è esattamente ciò che fanno le funzioni di forma bilineari
# dell'elemento Q4, quindi il disegno è la soluzione dell'elemento finito
# e non un'interpolazione grafica arbitraria.
Xg, Yg = np.meshgrid(dati["x_nodi_mm"], dati["arco_chiuso_mm"], indexing="ij")
mappa = ax_superficie.pcolormesh(
Xg, Yg,
chiudi_campo(dati["campi"][indice_inizio]),
shading="gouraud",
cmap="inferno",
2026-08-03 00:18:00 +02:00
vmin=dati["T_ambiente_C"],
vmax=T_max,
)
2026-08-03 00:18:00 +02:00
if MOSTRA_MESH:
# Bordi degli elementi shell: linee lungo x a ogni nodo
# circonferenziale e viceversa.
segmenti = [
[(x, arco_min_mm), (x, arco_max_mm)] for x in dati["x_nodi_mm"]
] + [
[(0.0, a), (lunghezza_mm, a)] for a in dati["arco_chiuso_mm"]
]
ax_superficie.add_collection(
LineCollection(segmenti, colors="white", linewidths=0.5, alpha=0.55)
)
ax_superficie.set_xlim(*x_vista_mm)
ax_superficie.set_ylim(arco_min_mm, arco_max_mm)
ax_superficie.set_ylabel("arco circonferenziale [mm]")
ax_superficie.set_xlabel("x [mm]")
ax_superficie.plot(
[dati["x_sensore_mm"]], [0.0], "^", color="tab:blue", markersize=11
)
2026-08-03 00:18:00 +02:00
ax_superficie.text(
dati["x_sensore_mm"] + 3, 0.0, "sensore IR", color="tab:blue", va="center"
)
ax_superficie.axhline(
offset_mm, color="tab:cyan", linewidth=0.8, linestyle="--", alpha=0.7
)
# La colorbar è agganciata a tutti i pannelli per non restringere solo
2026-08-03 00:18:00 +02:00
# quello della superficie, mantenendo allineati gli assi x.
barra = fig.colorbar(
2026-08-03 00:18:00 +02:00
mappa, ax=(ax_flusso, ax_superficie, ax_storia), pad=0.02, aspect=35
)
barra.set_label("T [°C]")
# Pannello 3: temperatura nel punto osservato dal sensore.
linea_vera, = ax_storia.plot([], [], label="T vera lato interno")
linea_letta, = ax_storia.plot([], [], label="T sensore (con inerzia)")
cursore = ax_storia.axvline(tempi[indice_inizio], color="gray", linewidth=0.8)
ax_storia.set_xlim(0.0, T_FINE_ANIMAZIONE_S)
ax_storia.set_ylim(15.0, max(dati["T_vere"].max(), dati["T_lette"].max()) * 1.08)
ax_storia.set_xlabel("Tempo [s]")
ax_storia.set_ylabel("T [°C]")
ax_storia.legend(loc="upper left")
ax_storia.grid(True, alpha=0.3)
testo_tempo = ax_flusso.set_title(f"t = {tempi[indice_inizio]:.3f} s", loc="right")
def aggiorna(frame: int):
k = indice_inizio + frame
t = tempi[k]
2026-08-03 00:18:00 +02:00
linea_flusso.set_data(dati["x_nodi_mm"], dati["flussi"][k] / 1e6)
x_sorgenti_mm = (
dati["x_riferimenti"][k] * 1000.0
+ np.arange(numero_sorgenti) * distanza_mm
)
visibili = (x_sorgenti_mm >= x_vista_mm[0]) & (x_sorgenti_mm <= x_vista_mm[1])
marker_sorgenti.set_data(
x_sorgenti_mm[visibili],
np.full(int(visibili.sum()), q_max_MW * 1.12),
)
2026-08-03 00:18:00 +02:00
mappa.set_array(chiudi_campo(dati["campi"][k]))
linea_vera.set_data(tempi[: k + 1], dati["T_vere"][: k + 1])
linea_letta.set_data(tempi[: k + 1], dati["T_lette"][: k + 1])
cursore.set_xdata([t, t])
testo_tempo.set_text(f"t = {t:.3f} s")
return (
2026-08-03 00:18:00 +02:00
linea_flusso, marker_sorgenti, mappa,
linea_vera, linea_letta, cursore, testo_tempo,
)
animazione = FuncAnimation(
fig, aggiorna, frames=n_frame, interval=INTERVALLO_RIPRODUZIONE_MS, blit=False
)
# Se il backend non è interattivo si salva una GIF invece di mostrare la finestra.
if matplotlib.get_backend().lower() == "agg":
2026-08-03 00:18:00 +02:00
percorso = Path("dataset") / "animazione_superficie.gif"
animazione.save(
percorso, writer=PillowWriter(fps=1000 // INTERVALLO_RIPRODUZIONE_MS)
)
print(f"Backend non interattivo: animazione salvata in {percorso}")
return
plt.show()
if __name__ == "__main__":
main()