141 lines
5.2 KiB
Python
141 lines
5.2 KiB
Python
# Animazione 3D del barattolo in vista isometrica: colormap della temperatura
|
|
# sulla superficie cilindrica.
|
|
#
|
|
# Il modello shell risolve la temperatura sull'intera superficie, quindi il
|
|
# campo disegnato qui è quello calcolato: non c'è più nessuna ricostruzione
|
|
# della distribuzione circonferenziale. La faccia esterna e quella interna
|
|
# hanno la stessa temperatura (una temperatura per nodo), quindi la superficie
|
|
# mostrata vale per entrambe.
|
|
|
|
import random
|
|
from pathlib import Path
|
|
|
|
import matplotlib
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
from matplotlib import cm
|
|
from matplotlib.animation import FuncAnimation, PillowWriter
|
|
|
|
import sorgente as src
|
|
from config import SIMULAZIONE
|
|
from mesh import chiudi_campo, coordinate_3d
|
|
from plot_animazione import DT_FRAME_S as DT_FRAME_S_BASE
|
|
from plot_animazione import T_INIZIO_ANIMAZIONE_S, simula_campi
|
|
from simulate import configurazione_randomizzata
|
|
|
|
# Tempo simulato tra un fotogramma e il successivo dell'animazione 3D. Più
|
|
# largo di DT_FRAME_S_BASE perché ricreare una superficie 3D a ogni
|
|
# fotogramma è più costoso della semplice imshow 2D.
|
|
DT_FRAME_S = 0.15
|
|
|
|
# Millisecondi tra i fotogrammi in riproduzione.
|
|
INTERVALLO_RIPRODUZIONE_MS = 60
|
|
|
|
# Disegna i bordi degli elementi shell sulla superficie del cilindro.
|
|
# Con la mesh di default (100 x 110, elementi di 1 x 2 mm) il reticolo satura
|
|
# il disegno e sbiadisce i colori: 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 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))
|
|
passo = max(1, round(DT_FRAME_S / DT_FRAME_S_BASE))
|
|
indici_frame = list(range(indice_inizio, len(tempi), passo))
|
|
|
|
mesh = dati["mesh"]
|
|
# simula_campi ruota le colonne per centrare il sensore nella mappa
|
|
# sviluppata: qui serve l'ordine originale della mesh.
|
|
rotazione = dati["rotazione"]
|
|
|
|
T_ambiente = dati["T_ambiente_C"]
|
|
sorgente = dati["sorgente"]
|
|
raggio_m = mesh["raggio_medio_m"]
|
|
lunghezza_m = mesh["lunghezza_m"]
|
|
|
|
Xm, Ym, Zm = coordinate_3d(mesh)
|
|
|
|
T_max = max(c.max() for c in dati["campi"])
|
|
# vmin più basso della temperatura ambiente reale: altrimenti le zone
|
|
# fredde cadrebbero sul nero puro di "inferno" e, essendo lo shading
|
|
# moltiplicativo, nessuna illuminazione basterebbe a renderle visibili.
|
|
norm = matplotlib.colors.Normalize(
|
|
vmin=T_ambiente - 0.4 * (T_max - T_ambiente), vmax=T_max
|
|
)
|
|
cmap = matplotlib.colormaps["inferno"]
|
|
lightsource = matplotlib.colors.LightSource(azdeg=315, altdeg=45)
|
|
|
|
fig = plt.figure(figsize=(9, 7))
|
|
ax = fig.add_subplot(projection="3d")
|
|
ax.view_init(elev=35.264, azim=45)
|
|
ax.set_box_aspect((lunghezza_m, 2 * raggio_m, 2 * raggio_m))
|
|
ax.set_xlabel("x [m]")
|
|
ax.set_axis_off()
|
|
|
|
# La colorbar mostra il range reale delle temperature: il norm esteso
|
|
# verso il basso serve solo a schiarire il colore di base della
|
|
# superficie fredda, non deve comparire nella scala mostrata all'utente.
|
|
norm_colorbar = matplotlib.colors.Normalize(vmin=T_ambiente, vmax=T_max)
|
|
mappabile = cm.ScalarMappable(cmap=cmap, norm=norm_colorbar)
|
|
mappabile.set_array([])
|
|
barra = fig.colorbar(mappabile, ax=ax, shrink=0.6, pad=0.05)
|
|
barra.set_label("T [°C]")
|
|
|
|
theta_sorgente = src.theta_percorso_rad(sorgente, raggio_m)
|
|
|
|
def disegna_frame(k: int):
|
|
ax.cla()
|
|
ax.view_init(elev=35.264, azim=45)
|
|
ax.set_box_aspect((lunghezza_m, 2 * raggio_m, 2 * raggio_m))
|
|
ax.set_axis_off()
|
|
|
|
campo = np.roll(dati["campi"][k], -rotazione, axis=1)
|
|
colori = cmap(norm(chiudi_campo(campo)))
|
|
|
|
# Ogni faccia disegnata è un elemento shell della mesh: con
|
|
# MOSTRA_MESH se ne vedono i bordi.
|
|
bordi = {"edgecolor": (1.0, 1.0, 1.0, 0.25), "linewidth": 0.15}
|
|
ax.plot_surface(
|
|
Xm, Ym, Zm, facecolors=colori, rstride=1, cstride=1,
|
|
antialiased=False, shade=True, lightsource=lightsource,
|
|
**(bordi if MOSTRA_MESH else {"linewidth": 0}),
|
|
)
|
|
|
|
x_sorgenti_m = src.posizioni_sorgenti_m(sorgente, dati["x_riferimenti"][k])
|
|
visibili = (x_sorgenti_m >= 0.0) & (x_sorgenti_m <= lunghezza_m)
|
|
if visibili.any():
|
|
ax.scatter(
|
|
x_sorgenti_m[visibili],
|
|
np.full(int(visibili.sum()), raggio_m * np.sin(theta_sorgente) * 1.05),
|
|
np.full(int(visibili.sum()), raggio_m * np.cos(theta_sorgente) * 1.05),
|
|
color="cyan", s=25, depthshade=False,
|
|
)
|
|
|
|
ax.set_title(f"t = {tempi[k]:.3f} s")
|
|
return ()
|
|
|
|
animazione = FuncAnimation(
|
|
fig, disegna_frame, frames=indici_frame,
|
|
interval=INTERVALLO_RIPRODUZIONE_MS, blit=False,
|
|
)
|
|
|
|
if matplotlib.get_backend().lower() == "agg":
|
|
percorso = Path("dataset") / "animazione_3d.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()
|