This commit is contained in:
davide committed 2026-08-03 00:18:00 +02:00
1 parent 560c6a6a62
commit 7883f3dead
13 files changed
+1460 -488

No files matched your search

+37 -47
View File
@@ -1,26 +1,26 @@
# Animazione 3D del barattolo in vista isometrica: colormap della temperatura
# sulla superficie esterna.
# sulla superficie cilindrica.
#
# Il modello risolve solo la sezione x-z (vedi CLAUDE.md): la coordinata
# circonferenziale y non è simulata, è collassata in un'attenuazione
# gaussiana del flusso. Per estrudere il campo attorno alla circonferenza si
# riusa la stessa gaussiana: la sovratemperatura rispetto al piano y=0 (dove
# si trova il sensore) viene scalata in funzione della distanza
# circonferenziale dal percorso delle sorgenti. È quindi una ricostruzione
# visiva, non un calcolo di diffusione in y.
# 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
from pathlib import Path
from config import FASCETTA, SIMULAZIONE
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_FINE_ANIMAZIONE_S, T_INIZIO_ANIMAZIONE_S, simula_campi
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ù
@@ -31,19 +31,12 @@ DT_FRAME_S = 0.15
# Millisecondi tra i fotogrammi in riproduzione.
INTERVALLO_RIPRODUZIONE_MS = 60
# Numero di punti lungo la circonferenza per l'estrusione della superficie.
N_THETA = 72
def attenuazione_circonferenziale(
y_m: np.ndarray, offset_y_m: float, sigma_m: float
) -> np.ndarray:
# Fattore che scala la sovratemperatura (T - T_ambiente) del piano y=0
# in funzione della distanza circonferenziale y dal piano stesso,
# normalizzato in modo da valere 1 in y=0.
sigma = max(sigma_m, 1e-9)
esponente = -0.5 * ((y_m - offset_y_m) ** 2 - offset_y_m ** 2) / (sigma * sigma)
return np.exp(esponente)
# 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:
@@ -56,26 +49,19 @@ def main() -> None:
passo = max(1, round(DT_FRAME_S / DT_FRAME_S_BASE))
indici_frame = list(range(indice_inizio, len(tempi), passo))
T_ambiente = cfg_run["aria"]["temperatura_ambiente_C"]
sorgente = cfg_run["sorgente"]
sigma_m = sorgente["sigma_punto_m"]
offset_y_m = sorgente["offset_y_percorso_m"]
numero_sorgenti = sorgente.get("numero_sorgenti", 1)
distanza_m = sorgente.get("distanza_sorgenti_m", 0.0)
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"]
raggio_m = (FASCETTA["diametro_mm"] / 1000.0) / 2.0
x_centri_m = dati["x_centri_mm"] / 1000.0
lunghezza_m = dati["lunghezza_mm"] / 1000.0
T_ambiente = dati["T_ambiente_C"]
sorgente = dati["sorgente"]
raggio_m = mesh["raggio_medio_m"]
lunghezza_m = mesh["lunghezza_m"]
theta = np.linspace(-np.pi, np.pi, N_THETA)
y_circ_m = theta * raggio_m
attenuazione = attenuazione_circonferenziale(y_circ_m, offset_y_m, sigma_m)
Xm, Ym, Zm = coordinate_3d(mesh)
Xm, Thetam = np.meshgrid(x_centri_m, theta)
Ym = raggio_m * np.sin(Thetam)
Zm = raggio_m * np.cos(Thetam)
T_max = max(c[:, 0].max() for c in dati["campi"])
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.
@@ -101,25 +87,29 @@ def main() -> None:
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()
T_lato_esterno = dati["campi"][k][:, 0]
T_superficie = T_ambiente + (T_lato_esterno[None, :] - T_ambiente) * attenuazione[:, None]
colori = cmap(norm(T_superficie))
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, linewidth=0,
antialiased=False, shade=True, lightsource=lightsource,
**(bordi if MOSTRA_MESH else {"linewidth": 0}),
)
x_sorgenti_m = dati["x_riferimenti"][k] + np.arange(numero_sorgenti) * distanza_m
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():
theta_sorgente = offset_y_m / raggio_m
ax.scatter(
x_sorgenti_m[visibili],
np.full(int(visibili.sum()), raggio_m * np.sin(theta_sorgente) * 1.05),