Rimuove il modello ai volumi finiti lasciando la sola analisi FEM

Il progetto conteneva due discretizzazioni indipendenti della stessa fisica:
i volumi finiti sulla sezione x-z di simulate.py (con generazione del dataset
CSV e le sue animazioni 2D/3D) e gli elementi finiti sulla mesh shell di
fem.py. Resta solo il secondo, che risolve la circonferenza invece di
collassarla in un'attenuazione gaussiana.

Eliminati simulate.py, plot_csv.py, plot_animazione.py e plot_animazione_3d.py.
Il FEM dipendeva da simulate.py per la cinematica del gruppo di sorgenti e per
la configurazione del run, quindi fem.py assorbe _x_riferimento_iniziale_m,
_x_riferimento_finale_m, _intervallo_attivo e configurazione_randomizzata; la
lettura del sensore (inerzia, rumore, quantizzazione) è ora applicata dentro
simula_campo_fem e la skin depth resta calcolata dalla frequenza dell'induttore
come sola grandezza diagnostica, dato che nel modello shell lo spessore è
collassato.

config.py perde i parametri che servivano solo ai volumi finiti (griglia
n_nodi_x/n_nodi_z, campionamento e cartella del dataset, numero di processi) e
guadagna FEM["durata_s"], FEM["seed"] e USCITA["cartella"]: le immagini e le
animazioni vanno in output/ invece che in dataset/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 11:44:58 +02:00
co-authored by Claude Opus 5
parent 6a55427b60
commit 1631d6c94e
13 changed files with 351 additions and 1252 deletions
+19 -26
View File
@@ -1,17 +1,15 @@
# Animazione 3D isometrica del barattolo con il campo di temperatura calcolato
# Animazione 3D isometrica della fascetta con il campo di temperatura calcolato
# a elementi finiti sulla mesh shell (fem.py).
#
# È l'equivalente di plot_animazione_3d.py ma senza ricostruzioni: lì il campo
# circonferenziale era estruso riusando la gaussiana della sorgente, qui la
# coordinata circonferenziale è una direzione risolta del modello FEM, quindi
# la temperatura dipinta su ogni elemento è quella effettivamente calcolata.
# La coordinata circonferenziale è una direzione risolta del modello FEM,
# quindi la temperatura dipinta su ogni elemento è quella effettivamente
# calcolata, senza ricostruzioni.
#
# Ogni elemento shell è disegnato come faccia piana con colore pari alla media
# dei suoi quattro valori nodali. L'ombreggiatura è calcolata a mano dalla
# normale radiale dell'elemento: Poly3DCollection ombreggia solo alla
# creazione, mentre qui i colori delle facce cambiano a ogni fotogramma.
import random
from pathlib import Path
import matplotlib
@@ -21,11 +19,13 @@ from matplotlib import cm
from matplotlib.animation import FuncAnimation, PillowWriter
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from config import SIMULAZIONE
from config import USCITA
from fem import simula_campo_fem
from mesh import genera_mesh, riepilogo_mesh
from plot_animazione import T_FINE_ANIMAZIONE_S, T_INIZIO_ANIMAZIONE_S
from simulate import configurazione_randomizzata
# Istante del primo fotogramma mostrato: i primi istanti sono ancora uniformi
# alla temperatura ambiente.
T_INIZIO_ANIMAZIONE_S = 0.40
# Tempo simulato tra un fotogramma e il successivo.
DT_FRAME_S = 0.15
@@ -65,16 +65,8 @@ def main() -> None:
mesh_dati = genera_mesh()
print(riepilogo_mesh(mesh_dati))
rng = random.Random(SIMULAZIONE["seed"])
cfg_run = configurazione_randomizzata(1, rng)
print("Integrazione FEM in corso...")
dati = simula_campo_fem(
cfg_run,
durata_s=T_FINE_ANIMAZIONE_S,
dt_frame_s=DT_FRAME_S,
mesh_dati=mesh_dati,
)
dati = simula_campo_fem(dt_frame_s=DT_FRAME_S, mesh_dati=mesh_dati)
tempi = dati["tempi"]
indice_inizio = int(np.searchsorted(tempi, T_INIZIO_ANIMAZIONE_S))
@@ -84,7 +76,8 @@ def main() -> None:
T_max = max(campo.max() for campo in dati["campi"])
print(
f"T massima nodale: {T_max:.1f} °C — "
f"T massima nel punto del sensore: {dati['T_vere'].max():.1f} °C"
f"T massima nel punto del sensore: {dati['T_vere'].max():.1f} °C\n"
f"Skin depth (diagnostica): {1000.0 * dati['skin_depth_m']:.3f} mm"
)
elementi = mesh_dati["elementi"]
@@ -133,17 +126,17 @@ def main() -> None:
# Pannello inferiore: storia della temperatura nel punto osservato dal
# sensore. T_vere è il valore nodale calcolato dal FEM, T_lette lo stesso
# segnale filtrato dall'inerzia del primo ordine del sensore.
# Con la costante di tempo del sensore le due curve sono quasi
# sovrapposte: la prima è tracciata spessa e trasparente perché la seconda,
# tratteggiata, resti leggibile sopra di essa.
# segnale degradato dal sensore reale (inerzia, rumore, quantizzazione).
# A parte il rumore le due curve sono quasi sovrapposte: la prima è
# tracciata spessa e trasparente perché la seconda resti leggibile sopra
# di essa.
linea_vera, = ax_storia.plot(
[], [], color="tab:blue", linewidth=3.0, alpha=0.4,
label="T nodo del sensore (FEM)",
)
linea_letta, = ax_storia.plot(
[], [], color="tab:red", linewidth=1.2, linestyle="--",
label="T misurata dal sensore (con inerzia)",
[], [], color="tab:red", linewidth=1.0,
label="T misurata dal sensore (inerzia + rumore + quantizzazione)",
)
cursore = ax_storia.axvline(tempi[indice_inizio], color="gray", linewidth=0.8)
ax_storia.set_xlim(0.0, tempi[-1])
@@ -189,7 +182,7 @@ def main() -> None:
)
if matplotlib.get_backend().lower() == "agg":
cartella = Path("dataset")
cartella = Path(USCITA["cartella"])
cartella.mkdir(parents=True, exist_ok=True)
percorso = cartella / "animazione_fem.gif"
animazione.save(