Compare commits
5 Commits
7f540ed108
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 560c6a6a62 | |||
| 288ae81b3e | |||
| 4d83ac4e70 | |||
| a04465a674 | |||
| 4369d2799f |
@@ -48,7 +48,7 @@ Generatore di dataset per misurazioni termiche pseudo-realistiche di una fascett
|
|||||||
- Le sorgenti gaussiane in movimento producono un profilo di flusso termico superficiale q(x) sul lato esterno, variabile nel tempo (`profilo_flusso_incidente_W_m2`)
|
- Le sorgenti gaussiane in movimento producono un profilo di flusso termico superficiale q(x) sul lato esterno, variabile nel tempo (`profilo_flusso_incidente_W_m2`)
|
||||||
- Quel flusso è ridistribuito volumetricamente attraverso lo spessore con decadimento esponenziale in z (`profilo_deposizione_z_1_m`): q_vol(x, z) = q(x) · p(z)
|
- Quel flusso è ridistribuito volumetricamente attraverso lo spessore con decadimento esponenziale in z (`profilo_deposizione_z_1_m`): q_vol(x, z) = q(x) · p(z)
|
||||||
- Uno schema 2D a volumi finiti con Eulero implicito integra l'equazione del calore su `n_nodi_x × n_nodi_z` celle: `prepara_stato_termico` costruisce griglia, coefficienti e matrice sparsa fattorizzata LU una volta per run (`costruisci_solutore_implicito_2d`, che restituisce l'oggetto `splu`), poi `passo_implicito` avanza il campo risolvendo solo il sistema triangolare
|
- Uno schema 2D a volumi finiti con Eulero implicito integra l'equazione del calore su `n_nodi_x × n_nodi_z` celle: `prepara_stato_termico` costruisce griglia, coefficienti e matrice sparsa fattorizzata LU una volta per run (`costruisci_solutore_implicito_2d`, che restituisce l'oggetto `splu`), poi `passo_implicito` avanza il campo risolvendo solo il sistema triangolare
|
||||||
- Le condizioni al contorno sono incorporate nella matrice: convezione su tutti e quattro i lati della sezione, più un termine di conduzione circonferenziale (y) verso il resto della fascetta assunto a temperatura ambiente (q_y = -k·(T - T_amb)/L_y², con L_y = `lunghezza_conduzione_y_mm`)
|
- Le condizioni al contorno sono incorporate nella matrice: convezione su tutti e quattro i lati della sezione, più un termine di conduzione circonferenziale (y) verso il resto della fascetta assunto a temperatura ambiente. Il termine è un'equazione di aletta ricavata sull'intero volume del cilindro: il calore conduce lungo y attraverso l'intero spessore mentre le superfici esterna e interna dell'intero cilindro scambiano per convezione, dando q_y = -(h_esterno + h_interno)/spessore · (T - T_amb), senza parametri di conduzione y configurabili a parte
|
||||||
- La temperatura iniziale del campo è la temperatura ambiente (randomizzata per run)
|
- La temperatura iniziale del campo è la temperatura ambiente (randomizzata per run)
|
||||||
- L'output del sensore aggiunge inerzia del primo ordine, rumore gaussiano e quantizzazione
|
- L'output del sensore aggiunge inerzia del primo ordine, rumore gaussiano e quantizzazione
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,11 @@ pratico di dataset. La catena di approssimazioni è la seguente.
|
|||||||
(`h_interno_W_m2K`) e sui due bordi in x (`h_bordi_W_m2K`);
|
(`h_interno_W_m2K`) e sui due bordi in x (`h_bordi_W_m2K`);
|
||||||
- **conduzione circonferenziale**: la sezione cede calore per conduzione lungo `y`
|
- **conduzione circonferenziale**: la sezione cede calore per conduzione lungo `y`
|
||||||
al resto della fascetta, assunto a temperatura ambiente. Il termine è
|
al resto della fascetta, assunto a temperatura ambiente. Il termine è
|
||||||
`q_y = -k·(T - T_amb)/L_y²` con `L_y = lunghezza_conduzione_y_mm`, la distanza
|
un'equazione di aletta ricavata sull'intero volume del cilindro: il calore
|
||||||
caratteristica su cui si sviluppa il gradiente circonferenziale (valore più
|
conduce lungo `y` attraverso l'intero spessore mentre le superfici esterna e
|
||||||
piccolo = pozzo termico più aggressivo).
|
interna dell'intero cilindro perdono calore per convezione, dando
|
||||||
|
`q_y = -(h_esterno + h_interno)/spessore · (T - T_amb)` — nessun parametro di
|
||||||
|
conduzione `y` aggiuntivo da configurare.
|
||||||
|
|
||||||
5. **Temperatura iniziale** — il campo parte uniformemente alla temperatura ambiente
|
5. **Temperatura iniziale** — il campo parte uniformemente alla temperatura ambiente
|
||||||
del run (che è randomizzata, quindi varia run per run).
|
del run (che è randomizzata, quindi varia run per run).
|
||||||
@@ -162,6 +164,15 @@ Punti da conoscere:
|
|||||||
- **Materiale**: `FASCETTA["materiale"]` deve essere una chiave di `MATERIALI` in
|
- **Materiale**: `FASCETTA["materiale"]` deve essere una chiave di `MATERIALI` in
|
||||||
`materials.py`. Per aggiungere un materiale basta una nuova voce nel dizionario
|
`materials.py`. Per aggiungere un materiale basta una nuova voce nel dizionario
|
||||||
(conducibilità, densità, calore specifico, resistività elettrica, permeabilità).
|
(conducibilità, densità, calore specifico, resistività elettrica, permeabilità).
|
||||||
|
- **Proprietà di `banda_stagnata`**: la banda stagnata è un nastro di acciaio a basso
|
||||||
|
tenore di carbonio (0,15–0,25% C) rivestito su entrambe le facce da un sottile
|
||||||
|
strato di stagno elettrolitico, dello spessore di pochi micrometri — trascurabile
|
||||||
|
rispetto allo spessore tipico della parete (es. 0,18 mm) e quindi ininfluente sulle
|
||||||
|
proprietà termiche, elettriche e magnetiche in massa. I valori in `materials.py`
|
||||||
|
sono quindi quelli dell'acciaio dolce sottostante:
|
||||||
|
[Banda stagnata: tutto quello che c'è da sapere (MUNDOLATAS)](https://mundolatas.com/it/banda-stagnata-tutto-quello-che-ce-da-sapere/),
|
||||||
|
[Bande stagnate elettrolitiche (EUROPERF)](https://www.europerf.it/it/banda-stagnata-elettrolitica.php),
|
||||||
|
[Differenza tra banda stagnata e acciaio inossidabile (Wuxi Bright Packing)](https://it.brightmetalcan.com/info/difference-between-tinplate-and-stainless-stee-48700260.html).
|
||||||
- **Randomizzazione**: ogni run perturba velocità, flusso di picco, sigma, offset y,
|
- **Randomizzazione**: ogni run perturba velocità, flusso di picco, sigma, offset y,
|
||||||
temperatura ambiente e rumore del sensore con estrazioni da un RNG a seed fisso
|
temperatura ambiente e rumore del sensore con estrazioni da un RNG a seed fisso
|
||||||
(`SIMULAZIONE["seed"]`): il dataset è riproducibile.
|
(`SIMULAZIONE["seed"]`): il dataset è riproducibile.
|
||||||
|
|||||||
@@ -15,8 +15,12 @@
|
|||||||
# - Il riscaldamento a induzione è approssimato come riscaldamento volumetrico
|
# - Il riscaldamento a induzione è approssimato come riscaldamento volumetrico
|
||||||
# che decade esponenzialmente con la profondità z secondo la skin depth.
|
# che decade esponenzialmente con la profondità z secondo la skin depth.
|
||||||
# - La sezione scambia per convezione con l'aria su tutti e quattro i lati
|
# - La sezione scambia per convezione con l'aria su tutti e quattro i lati
|
||||||
# (esterno, interno e i due bordi in x) e per conduzione lungo y con il
|
# (esterno, interno e i due bordi in x). Scambia inoltre per conduzione
|
||||||
# resto della fascetta, assunto a temperatura ambiente.
|
# lungo y con il resto della fascetta, assunto a temperatura ambiente:
|
||||||
|
# il calore conduce attraverso l'intero volume dello spessore mentre le
|
||||||
|
# superfici esterna e interna dell'intero cilindro perdono calore per
|
||||||
|
# convezione (equazione dell'aletta), derivato da h_esterno, h_interno e
|
||||||
|
# spessore_mm senza parametri di conduzione y aggiuntivi.
|
||||||
# - La temperatura iniziale della fascetta è pari alla temperatura ambiente.
|
# - La temperatura iniziale della fascetta è pari alla temperatura ambiente.
|
||||||
# - Il sensore è un pirometro a infrarossi posto all'interno della fascetta,
|
# - Il sensore è un pirometro a infrarossi posto all'interno della fascetta,
|
||||||
# a distanza "distanza_parete_mm" dalla parete interna: misura senza
|
# a distanza "distanza_parete_mm" dalla parete interna: misura senza
|
||||||
@@ -31,7 +35,7 @@
|
|||||||
|
|
||||||
SIMULAZIONE = {
|
SIMULAZIONE = {
|
||||||
# Numero di file CSV da generare.
|
# Numero di file CSV da generare.
|
||||||
"num_run": 2,
|
"num_run": 1,
|
||||||
|
|
||||||
# Tempo simulato totale.
|
# Tempo simulato totale.
|
||||||
"durata_s": 30.0,
|
"durata_s": 30.0,
|
||||||
@@ -49,6 +53,10 @@ SIMULAZIONE = {
|
|||||||
|
|
||||||
# Cartella di output.
|
# Cartella di output.
|
||||||
"cartella_output": "dataset",
|
"cartella_output": "dataset",
|
||||||
|
|
||||||
|
# Numero di processi paralleli per la generazione dei run.
|
||||||
|
# None = usa tutti i core disponibili.
|
||||||
|
"num_processi": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
FASCETTA = {
|
FASCETTA = {
|
||||||
@@ -59,7 +67,7 @@ FASCETTA = {
|
|||||||
"lunghezza_mm": 100.0,
|
"lunghezza_mm": 100.0,
|
||||||
|
|
||||||
# Spessore della parete [mm].
|
# Spessore della parete [mm].
|
||||||
"spessore_mm": 0.12,
|
"spessore_mm": 0.18,
|
||||||
|
|
||||||
# Numero di celle del volume finito lungo x (lunghezza).
|
# Numero di celle del volume finito lungo x (lunghezza).
|
||||||
"n_nodi_x": 100,
|
"n_nodi_x": 100,
|
||||||
@@ -68,11 +76,6 @@ FASCETTA = {
|
|||||||
# Più nodi = maggiore risoluzione spaziale, simulazione più lenta.
|
# Più nodi = maggiore risoluzione spaziale, simulazione più lenta.
|
||||||
"n_nodi_z": 15,
|
"n_nodi_z": 15,
|
||||||
|
|
||||||
# Distanza caratteristica lungo y (direzione circonferenziale) su cui la
|
|
||||||
# sezione scambia calore per conduzione con il resto della fascetta,
|
|
||||||
# assunto a temperatura ambiente.
|
|
||||||
"lunghezza_conduzione_y_mm": 25.0,
|
|
||||||
|
|
||||||
# Deve corrispondere a una chiave in materials.py.
|
# Deve corrispondere a una chiave in materials.py.
|
||||||
"materiale": "banda_stagnata",
|
"materiale": "banda_stagnata",
|
||||||
}
|
}
|
||||||
@@ -94,7 +97,7 @@ ARIA = {
|
|||||||
SORGENTE = {
|
SORGENTE = {
|
||||||
# Distanza dal sensore, all'inizio della corsa, della sorgente del
|
# Distanza dal sensore, all'inizio della corsa, della sorgente del
|
||||||
# gruppo più vicina al sensore (quella che lo raggiunge per prima).
|
# gruppo più vicina al sensore (quella che lo raggiunge per prima).
|
||||||
"x_inizio_m": 1.0,
|
"x_inizio_m": 5.0,
|
||||||
|
|
||||||
# Distanza dal sensore, alla fine della corsa, della sorgente del
|
# Distanza dal sensore, alla fine della corsa, della sorgente del
|
||||||
# gruppo più lontana dal sensore (quella che lo supera per ultima).
|
# gruppo più lontana dal sensore (quella che lo supera per ultima).
|
||||||
@@ -106,20 +109,20 @@ SORGENTE = {
|
|||||||
"offset_y_percorso_m": 0.0,
|
"offset_y_percorso_m": 0.0,
|
||||||
|
|
||||||
# Velocità delle sorgenti lungo x. Il segno indica il verso di marcia.
|
# Velocità delle sorgenti lungo x. Il segno indica il verso di marcia.
|
||||||
"velocita_m_s": -2.0,
|
"velocita_m_s": -1.0,
|
||||||
|
|
||||||
# Numero di sorgenti equidistanti che si muovono insieme come un gruppo
|
# Numero di sorgenti equidistanti che si muovono insieme come un gruppo
|
||||||
# rigido (stessa velocità, sigma, flusso ed efficienza).
|
# rigido (stessa velocità, sigma, flusso ed efficienza).
|
||||||
"numero_sorgenti": 3,
|
"numero_sorgenti": 3,
|
||||||
|
|
||||||
# Distanza lungo x tra sorgenti consecutive del gruppo.
|
# Distanza lungo x tra sorgenti consecutive del gruppo.
|
||||||
"distanza_sorgenti_m": 0.5,
|
"distanza_sorgenti_m": 1.0,
|
||||||
|
|
||||||
# Raggio del punto gaussiano. Valore più alto = impronta termica più larga.
|
# Raggio del punto gaussiano. Valore più alto = impronta termica più larga.
|
||||||
"sigma_punto_m": 0.012,
|
"sigma_punto_m": 0.012,
|
||||||
|
|
||||||
# Flusso termico incidente massimo prima della correzione per efficienza.
|
# Flusso termico incidente massimo prima della correzione per efficienza.
|
||||||
"flusso_termico_picco_W_m2": 5500000.0,
|
"flusso_termico_picco_W_m2": 4434000.0,
|
||||||
|
|
||||||
# Frazione del flusso incidente che diventa effettivamente calore nella fascetta.
|
# Frazione del flusso incidente che diventa effettivamente calore nella fascetta.
|
||||||
"efficienza_riscaldamento": 0.35,
|
"efficienza_riscaldamento": 0.35,
|
||||||
@@ -146,7 +149,7 @@ SENSORE = {
|
|||||||
|
|
||||||
# Inerzia del sensore del primo ordine.
|
# Inerzia del sensore del primo ordine.
|
||||||
# Valore più alto = risposta del sensore più lenta.
|
# Valore più alto = risposta del sensore più lenta.
|
||||||
"costante_tempo_s": 0.05,
|
"costante_tempo_s": 0.01,
|
||||||
|
|
||||||
# Deviazione standard del rumore gaussiano.
|
# Deviazione standard del rumore gaussiano.
|
||||||
"rumore_std_C": 0.15,
|
"rumore_std_C": 0.15,
|
||||||
@@ -159,7 +162,7 @@ SENSORE = {
|
|||||||
|
|
||||||
RANDOMIZZAZIONE = {
|
RANDOMIZZAZIONE = {
|
||||||
# Se abilitata, ogni run varia leggermente alcuni parametri.
|
# Se abilitata, ogni run varia leggermente alcuni parametri.
|
||||||
"abilitata": True,
|
"abilitata": False,
|
||||||
|
|
||||||
# Deviazioni standard relative.
|
# Deviazioni standard relative.
|
||||||
"velocita_std_rel": 0.007,
|
"velocita_std_rel": 0.007,
|
||||||
|
|||||||
+5
-5
@@ -37,10 +37,10 @@ MATERIALI = {
|
|||||||
"permeabilita_relativa": 1.0,
|
"permeabilita_relativa": 1.0,
|
||||||
},
|
},
|
||||||
"banda_stagnata": {
|
"banda_stagnata": {
|
||||||
"conducibilita_termica_W_mK": 60.0,
|
"conducibilita_termica_W_mK": 54.0,
|
||||||
"densita_kg_m3": 7860.0,
|
"densita_kg_m3": 7850.0,
|
||||||
"calore_specifico_J_kgK": 460.0,
|
"calore_specifico_J_kgK": 470.0,
|
||||||
"resistivita_elettrica_ohm_m": 1.2e-7,
|
"resistivita_elettrica_ohm_m": 1.6e-7,
|
||||||
"permeabilita_relativa": 150.0,
|
"permeabilita_relativa": 100.0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -25,13 +25,13 @@ from simulate import (
|
|||||||
T_INIZIO_ANIMAZIONE_S = 0.40
|
T_INIZIO_ANIMAZIONE_S = 0.40
|
||||||
|
|
||||||
# Istante di fine dell'animazione.
|
# Istante di fine dell'animazione.
|
||||||
T_FINE_ANIMAZIONE_S = 20
|
T_FINE_ANIMAZIONE_S = 30
|
||||||
|
|
||||||
# Tempo simulato tra un fotogramma e il successivo.
|
# Tempo simulato tra un fotogramma e il successivo.
|
||||||
DT_FRAME_S = 0.001
|
DT_FRAME_S = 0.05
|
||||||
|
|
||||||
# Millisecondi tra i fotogrammi in riproduzione.
|
# Millisecondi tra i fotogrammi in riproduzione.
|
||||||
INTERVALLO_RIPRODUZIONE_MS = 1
|
INTERVALLO_RIPRODUZIONE_MS = 30
|
||||||
|
|
||||||
|
|
||||||
def simula_campi(cfg_run: dict) -> dict:
|
def simula_campi(cfg_run: dict) -> dict:
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# Animazione 3D del barattolo in vista isometrica: colormap della temperatura
|
||||||
|
# sulla superficie esterna.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
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
|
||||||
|
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 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
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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, 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"])
|
||||||
|
# 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]")
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
ax.plot_surface(
|
||||||
|
Xm, Ym, Zm, facecolors=colori, rstride=1, cstride=1,
|
||||||
|
antialiased=False, shade=True, lightsource=lightsource, linewidth=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
x_sorgenti_m = dati["x_riferimenti"][k] + np.arange(numero_sorgenti) * distanza_m
|
||||||
|
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),
|
||||||
|
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()
|
||||||
+32
-14
@@ -1,7 +1,9 @@
|
|||||||
import csv
|
import csv
|
||||||
import math
|
import math
|
||||||
|
import os
|
||||||
import random
|
import random
|
||||||
import shutil
|
import shutil
|
||||||
|
from concurrent.futures import ProcessPoolExecutor
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -140,11 +142,11 @@ def costruisci_solutore_implicito_2d(
|
|||||||
dt_s: float,
|
dt_s: float,
|
||||||
dx_m: float,
|
dx_m: float,
|
||||||
dz_m: float,
|
dz_m: float,
|
||||||
|
spessore_m: float,
|
||||||
materiale: dict,
|
materiale: dict,
|
||||||
h_esterno_W_m2K: float,
|
h_esterno_W_m2K: float,
|
||||||
h_interno_W_m2K: float,
|
h_interno_W_m2K: float,
|
||||||
h_bordi_W_m2K: float,
|
h_bordi_W_m2K: float,
|
||||||
lunghezza_conduzione_y_m: float,
|
|
||||||
):
|
):
|
||||||
# Costruisce e fattorizza (LU sparsa) la matrice A per Eulero implicito 2D:
|
# Costruisce e fattorizza (LU sparsa) la matrice A per Eulero implicito 2D:
|
||||||
# A * T_next = rhs
|
# A * T_next = rhs
|
||||||
@@ -153,8 +155,12 @@ def costruisci_solutore_implicito_2d(
|
|||||||
# appiattiti in ordine C (indice = i * n_z + j). Tutti e quattro i lati
|
# appiattiti in ordine C (indice = i * n_z + j). Tutti e quattro i lati
|
||||||
# della sezione includono la convezione verso l'ambiente; su ogni cella
|
# della sezione includono la convezione verso l'ambiente; su ogni cella
|
||||||
# agisce inoltre la conduzione circonferenziale (y) verso il resto della
|
# agisce inoltre la conduzione circonferenziale (y) verso il resto della
|
||||||
# fascetta, assunto a temperatura ambiente e modellato come
|
# fascetta, assunto a temperatura ambiente. Il termine è ricavato
|
||||||
# q_y = -k * (T - T_amb) / L_y².
|
# considerando l'intero volume della fascia cilindrica (equazione
|
||||||
|
# dell'aletta): il calore conduce lungo y attraverso l'intero spessore
|
||||||
|
# mentre le superfici esterna e interna dell'intero cilindro scambiano
|
||||||
|
# per convezione, dando un sink distribuito uniformemente su ogni cella
|
||||||
|
# q_y = -(h_esterno + h_interno) / spessore * (T - T_amb).
|
||||||
k = materiale["conducibilita_termica_W_mK"]
|
k = materiale["conducibilita_termica_W_mK"]
|
||||||
rho = materiale["densita_kg_m3"]
|
rho = materiale["densita_kg_m3"]
|
||||||
cp = materiale["calore_specifico_J_kgK"]
|
cp = materiale["calore_specifico_J_kgK"]
|
||||||
@@ -165,7 +171,7 @@ def costruisci_solutore_implicito_2d(
|
|||||||
b_esterno = h_esterno_W_m2K * dt_s / (rho * cp * dz_m)
|
b_esterno = h_esterno_W_m2K * dt_s / (rho * cp * dz_m)
|
||||||
b_interno = h_interno_W_m2K * dt_s / (rho * cp * dz_m)
|
b_interno = h_interno_W_m2K * dt_s / (rho * cp * dz_m)
|
||||||
b_bordo = h_bordi_W_m2K * dt_s / (rho * cp * dx_m)
|
b_bordo = h_bordi_W_m2K * dt_s / (rho * cp * dx_m)
|
||||||
c_y = alpha * dt_s / (lunghezza_conduzione_y_m * lunghezza_conduzione_y_m)
|
c_y = (h_esterno_W_m2K + h_interno_W_m2K) * dt_s / (rho * cp * spessore_m)
|
||||||
|
|
||||||
n = n_x * n_z
|
n = n_x * n_z
|
||||||
scambio = np.full(n, c_y)
|
scambio = np.full(n, c_y)
|
||||||
@@ -194,7 +200,6 @@ def prepara_stato_termico(fascetta: dict, aria: dict, sorgente: dict) -> dict:
|
|||||||
n_z = fascetta["n_nodi_z"]
|
n_z = fascetta["n_nodi_z"]
|
||||||
dx = lunghezza / n_x
|
dx = lunghezza / n_x
|
||||||
dz = spessore / n_z
|
dz = spessore / n_z
|
||||||
lunghezza_y = fascetta["lunghezza_conduzione_y_mm"] / 1000.0
|
|
||||||
|
|
||||||
dt = SIMULAZIONE["dt_interno_s"]
|
dt = SIMULAZIONE["dt_interno_s"]
|
||||||
|
|
||||||
@@ -224,7 +229,7 @@ def prepara_stato_termico(fascetta: dict, aria: dict, sorgente: dict) -> dict:
|
|||||||
"b_esterno": aria["h_esterno_W_m2K"] * dt / (rho * cp * dz),
|
"b_esterno": aria["h_esterno_W_m2K"] * dt / (rho * cp * dz),
|
||||||
"b_interno": aria["h_interno_W_m2K"] * dt / (rho * cp * dz),
|
"b_interno": aria["h_interno_W_m2K"] * dt / (rho * cp * dz),
|
||||||
"b_bordo": aria["h_bordi_W_m2K"] * dt / (rho * cp * dx),
|
"b_bordo": aria["h_bordi_W_m2K"] * dt / (rho * cp * dx),
|
||||||
"c_y": alpha * dt / (lunghezza_y * lunghezza_y),
|
"c_y": (aria["h_esterno_W_m2K"] + aria["h_interno_W_m2K"]) * dt / (rho * cp * spessore),
|
||||||
"T_ambiente_C": aria["temperatura_ambiente_C"],
|
"T_ambiente_C": aria["temperatura_ambiente_C"],
|
||||||
"solutore": costruisci_solutore_implicito_2d(
|
"solutore": costruisci_solutore_implicito_2d(
|
||||||
n_x=n_x,
|
n_x=n_x,
|
||||||
@@ -232,11 +237,11 @@ def prepara_stato_termico(fascetta: dict, aria: dict, sorgente: dict) -> dict:
|
|||||||
dt_s=dt,
|
dt_s=dt,
|
||||||
dx_m=dx,
|
dx_m=dx,
|
||||||
dz_m=dz,
|
dz_m=dz,
|
||||||
|
spessore_m=spessore,
|
||||||
materiale=materiale,
|
materiale=materiale,
|
||||||
h_esterno_W_m2K=aria["h_esterno_W_m2K"],
|
h_esterno_W_m2K=aria["h_esterno_W_m2K"],
|
||||||
h_interno_W_m2K=aria["h_interno_W_m2K"],
|
h_interno_W_m2K=aria["h_interno_W_m2K"],
|
||||||
h_bordi_W_m2K=aria["h_bordi_W_m2K"],
|
h_bordi_W_m2K=aria["h_bordi_W_m2K"],
|
||||||
lunghezza_conduzione_y_m=lunghezza_y,
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,7 +414,6 @@ def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
|||||||
"diametro_m": fascetta["diametro_mm"] / 1000.0,
|
"diametro_m": fascetta["diametro_mm"] / 1000.0,
|
||||||
"lunghezza_m": fascetta["lunghezza_mm"] / 1000.0,
|
"lunghezza_m": fascetta["lunghezza_mm"] / 1000.0,
|
||||||
"spessore_m": fascetta["spessore_mm"] / 1000.0,
|
"spessore_m": fascetta["spessore_mm"] / 1000.0,
|
||||||
"lunghezza_conduzione_y_m": fascetta["lunghezza_conduzione_y_mm"] / 1000.0,
|
|
||||||
"n_nodi_x": n_x,
|
"n_nodi_x": n_x,
|
||||||
"n_nodi_z": n_z,
|
"n_nodi_z": n_z,
|
||||||
"durata_s": durata,
|
"durata_s": durata,
|
||||||
@@ -440,19 +444,33 @@ def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _esegui_run(indice_e_seme: tuple[int, int]) -> dict:
|
||||||
|
# Ogni run riceve un seme indipendente derivato dal seed globale, così
|
||||||
|
# l'esecuzione in parallelo resta riproducibile indipendentemente
|
||||||
|
# dall'ordine in cui i processi la completano.
|
||||||
|
indice, seme = indice_e_seme
|
||||||
|
rng = random.Random(seme)
|
||||||
|
cfg_run = configurazione_randomizzata(indice, rng)
|
||||||
|
cartella_output = Path(SIMULAZIONE["cartella_output"])
|
||||||
|
percorso_csv = cartella_output / f"{cfg_run['id_run']}.csv"
|
||||||
|
return simula_singolo(cfg_run, percorso_csv, rng)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
cartella_output = Path(SIMULAZIONE["cartella_output"])
|
cartella_output = Path(SIMULAZIONE["cartella_output"])
|
||||||
if cartella_output.exists():
|
if cartella_output.exists():
|
||||||
shutil.rmtree(cartella_output)
|
shutil.rmtree(cartella_output)
|
||||||
cartella_output.mkdir(parents=True, exist_ok=True)
|
cartella_output.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
rng = random.Random(SIMULAZIONE["seed"])
|
rng_semi = random.Random(SIMULAZIONE["seed"])
|
||||||
|
num_run = SIMULAZIONE["num_run"]
|
||||||
|
semi = [rng_semi.randrange(2**63) for _ in range(num_run)]
|
||||||
|
|
||||||
righe_metadata = []
|
num_processi = SIMULAZIONE["num_processi"] or os.cpu_count() or 1
|
||||||
for i in range(1, SIMULAZIONE["num_run"] + 1):
|
with ProcessPoolExecutor(max_workers=num_processi) as executor:
|
||||||
cfg_run = configurazione_randomizzata(i, rng)
|
righe_metadata = list(
|
||||||
percorso_csv = cartella_output / f"{cfg_run['id_run']}.csv"
|
executor.map(_esegui_run, enumerate(semi, start=1))
|
||||||
righe_metadata.append(simula_singolo(cfg_run, percorso_csv, rng))
|
)
|
||||||
|
|
||||||
percorso_metadata = cartella_output / "metadata.csv"
|
percorso_metadata = cartella_output / "metadata.csv"
|
||||||
with percorso_metadata.open("w", newline="") as f:
|
with percorso_metadata.open("w", newline="") as f:
|
||||||
|
|||||||
Reference in New Issue
Block a user