temp
This commit is contained in:
+110
-59
@@ -1,9 +1,14 @@
|
||||
# Animazione della sezione della fascetta durante il passaggio delle sorgenti.
|
||||
# Animazione della superficie della fascetta durante il passaggio delle
|
||||
# sorgenti.
|
||||
#
|
||||
# Riproduce la fisica di run_0001 (stesso seed di simulate.py) e mostra:
|
||||
# - il profilo di flusso termico q(x) sul lato esterno e le sorgenti in moto;
|
||||
# - il campo di temperatura T(x, z) nella sezione lunghezza × spessore;
|
||||
# - 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.
|
||||
#
|
||||
# La mappa sviluppata è periodica in verticale: il bordo superiore e quello
|
||||
# inferiore sono lo stesso punto della fascetta.
|
||||
|
||||
import random
|
||||
from pathlib import Path
|
||||
@@ -12,13 +17,17 @@ import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from matplotlib.animation import FuncAnimation, PillowWriter
|
||||
from matplotlib.collections import LineCollection
|
||||
|
||||
import sensore as sens
|
||||
import sorgente as src
|
||||
from config import SIMULAZIONE
|
||||
from mesh import campo_su_griglia, chiudi_campo
|
||||
from simulate import (
|
||||
campo_iniziale,
|
||||
configurazione_randomizzata,
|
||||
passo_implicito,
|
||||
passo_termico,
|
||||
prepara_stato_termico,
|
||||
profilo_flusso_incidente_W_m2,
|
||||
)
|
||||
|
||||
# Istante di inizio dei fotogrammi mostrati (la simulazione parte comunque da 0).
|
||||
@@ -33,27 +42,41 @@ DT_FRAME_S = 0.05
|
||||
# Millisecondi tra i fotogrammi in riproduzione.
|
||||
INTERVALLO_RIPRODUZIONE_MS = 30
|
||||
|
||||
# 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.
|
||||
#
|
||||
# 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"]
|
||||
|
||||
stato = prepara_stato_termico(fascetta, aria, sorgente)
|
||||
n_x = stato["n_x"]
|
||||
n_z = stato["n_z"]
|
||||
x_centri = stato["x_centri_m"]
|
||||
stato = prepara_stato_termico(fascetta, aria, sorgente, sensore)
|
||||
mesh = stato["mesh"]
|
||||
dt = stato["dt_s"]
|
||||
interpolatore = stato["interpolatore_sensore"]
|
||||
|
||||
x_sensore = sensore["x_mm"] / 1000.0
|
||||
i_sensore = min(n_x - 1, max(0, int(x_sensore / stato["dx_m"])))
|
||||
# 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"]
|
||||
|
||||
T = np.full((n_x, n_z), aria["temperatura_ambiente_C"], dtype=float)
|
||||
T_sensore = T[i_sensore, -1]
|
||||
tau_sensore = max(sensore["costante_tempo_s"], 1e-9)
|
||||
T = campo_iniziale(stato)
|
||||
T_letta = sens.temperatura_osservata_C(interpolatore, T)
|
||||
|
||||
tempi, campi, flussi, x_riferimenti = [], [], [], []
|
||||
T_vere, T_lette = [], []
|
||||
@@ -61,18 +84,22 @@ def simula_campi(cfg_run: dict) -> dict:
|
||||
prossimo_frame_t = 0.0
|
||||
t = 0.0
|
||||
while t <= T_FINE_ANIMAZIONE_S + 1e-12:
|
||||
x_rif, q_x = profilo_flusso_incidente_W_m2(sorgente, x_sensore, t, x_centri)
|
||||
T = passo_implicito(stato, T, q_x)
|
||||
|
||||
T_sensore += (T[i_sensore, -1] - T_sensore) * dt / tau_sensore
|
||||
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:
|
||||
_, q_nodi = src.profilo_flusso_x_W_m2(
|
||||
sorgente, stato["x_sensore_m"], t, mesh["x_nodi_m"]
|
||||
)
|
||||
tempi.append(t)
|
||||
campi.append(T.copy())
|
||||
flussi.append(q_x.copy())
|
||||
x_riferimenti.append(x_rif)
|
||||
T_vere.append(T[i_sensore, -1])
|
||||
T_lette.append(T_sensore)
|
||||
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
|
||||
@@ -84,11 +111,18 @@ def simula_campi(cfg_run: dict) -> dict:
|
||||
"x_riferimenti": np.array(x_riferimenti),
|
||||
"T_vere": np.array(T_vere),
|
||||
"T_lette": np.array(T_lette),
|
||||
"x_centri_mm": x_centri * 1000.0,
|
||||
"spessore_mm": fascetta["spessore_mm"],
|
||||
"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"],
|
||||
"sorgente": cfg_run["sorgente"],
|
||||
"offset_sorgente_mm": sorgente["offset_y_percorso_m"] * 1000.0,
|
||||
"sorgente": sorgente,
|
||||
"T_ambiente_C": aria["temperatura_ambiente_C"],
|
||||
}
|
||||
|
||||
|
||||
@@ -102,23 +136,27 @@ def main() -> None:
|
||||
n_frame = len(tempi) - indice_inizio
|
||||
|
||||
lunghezza_mm = dati["lunghezza_mm"]
|
||||
spessore_mm = dati["spessore_mm"]
|
||||
x_vista_mm = (-10.0, lunghezza_mm + 10.0)
|
||||
arco_mm = dati["arco_chiuso_mm"]
|
||||
arco_min_mm, arco_max_mm = arco_mm[0], arco_mm[-1]
|
||||
|
||||
q_max_MW = max(q.max() for q in dati["flussi"]) / 1e6
|
||||
# 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
|
||||
offset_mm = dati["offset_sorgente_mm"]
|
||||
|
||||
fig, (ax_flusso, ax_sezione, ax_storia) = plt.subplots(
|
||||
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},
|
||||
)
|
||||
fig.suptitle("Sezione della fascetta: sorgenti in transito e sensore")
|
||||
fig.suptitle("Superficie sviluppata della fascetta: sorgenti in transito e sensore")
|
||||
|
||||
# Pannello 1: profilo di flusso sul lato esterno e posizioni delle sorgenti.
|
||||
# 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
|
||||
@@ -135,37 +173,50 @@ def main() -> None:
|
||||
ax_flusso.grid(True, alpha=0.3)
|
||||
ax_flusso.set_xticklabels([])
|
||||
|
||||
# Pannello 2: campo di temperatura nella sezione (z verso il basso,
|
||||
# origine nel vertice in alto a sinistra come nel modello).
|
||||
immagine = ax_sezione.imshow(
|
||||
dati["campi"][indice_inizio].T,
|
||||
extent=(0.0, lunghezza_mm, spessore_mm, 0.0),
|
||||
aspect="auto",
|
||||
# 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",
|
||||
vmin=cfg_run["aria"]["temperatura_ambiente_C"],
|
||||
vmin=dati["T_ambiente_C"],
|
||||
vmax=T_max,
|
||||
interpolation="bilinear",
|
||||
)
|
||||
ax_sezione.set_xlim(*x_vista_mm)
|
||||
ax_sezione.set_ylim(3.2 * spessore_mm, -0.6 * spessore_mm)
|
||||
ax_sezione.set_ylabel("z [mm]")
|
||||
ax_sezione.set_xlabel("x [mm]")
|
||||
# Sensore infrarosso sotto la parete interna (posizione schematica,
|
||||
# non in scala) con linea di vista tratteggiata.
|
||||
x_sens = dati["x_sensore_mm"]
|
||||
ax_sezione.plot([x_sens], [2.4 * spessore_mm], "^", color="tab:blue", markersize=12)
|
||||
ax_sezione.plot(
|
||||
[x_sens, x_sens], [1.1 * spessore_mm, 2.1 * spessore_mm],
|
||||
linestyle="--", color="tab:blue", linewidth=1,
|
||||
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
|
||||
)
|
||||
ax_sezione.text(
|
||||
x_sens + 3, 2.4 * spessore_mm, "sensore IR",
|
||||
color="tab:blue", va="center",
|
||||
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
|
||||
# quello della sezione, mantenendo allineati gli assi x.
|
||||
# quello della superficie, mantenendo allineati gli assi x.
|
||||
barra = fig.colorbar(
|
||||
immagine, ax=(ax_flusso, ax_sezione, ax_storia), pad=0.02, aspect=35
|
||||
mappa, ax=(ax_flusso, ax_superficie, ax_storia), pad=0.02, aspect=35
|
||||
)
|
||||
barra.set_label("T [°C]")
|
||||
|
||||
@@ -186,7 +237,7 @@ def main() -> None:
|
||||
k = indice_inizio + frame
|
||||
t = tempi[k]
|
||||
|
||||
linea_flusso.set_data(dati["x_centri_mm"], dati["flussi"][k] / 1e6)
|
||||
linea_flusso.set_data(dati["x_nodi_mm"], dati["flussi"][k] / 1e6)
|
||||
|
||||
x_sorgenti_mm = (
|
||||
dati["x_riferimenti"][k] * 1000.0
|
||||
@@ -198,7 +249,7 @@ def main() -> None:
|
||||
np.full(int(visibili.sum()), q_max_MW * 1.12),
|
||||
)
|
||||
|
||||
immagine.set_data(dati["campi"][k].T)
|
||||
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])
|
||||
@@ -206,7 +257,7 @@ def main() -> None:
|
||||
testo_tempo.set_text(f"t = {t:.3f} s")
|
||||
|
||||
return (
|
||||
linea_flusso, marker_sorgenti, immagine,
|
||||
linea_flusso, marker_sorgenti, mappa,
|
||||
linea_vera, linea_letta, cursore, testo_tempo,
|
||||
)
|
||||
|
||||
@@ -216,7 +267,7 @@ def main() -> None:
|
||||
|
||||
# Se il backend non è interattivo si salva una GIF invece di mostrare la finestra.
|
||||
if matplotlib.get_backend().lower() == "agg":
|
||||
percorso = Path("dataset") / "animazione_sezione.gif"
|
||||
percorso = Path("dataset") / "animazione_superficie.gif"
|
||||
animazione.save(
|
||||
percorso, writer=PillowWriter(fps=1000 // INTERVALLO_RIPRODUZIONE_MS)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user