Trasforma il modello termico da 1D a 2D sulla sezione della fascetta
La lastra diventa una fascetta cilindrica (diametro, lunghezza, spessore) di cui si simula la sezione rettangolare lunghezza x spessore, con origine nel vertice in alto a sinistra: x = lunghezza, z = spessore, y = coordinata circonferenziale non risolta (offset collassato in attenuazione gaussiana). - Le sorgenti si muovono in direzione -x e producono un profilo di flusso q(x) su tutto il lato esterno; corretto il posizionamento del gruppo per velocita' negative - Sensore a infrarossi con coordinata x e distanza dalla parete interna, misura la superficie interna senza contatto - Volumi finiti 2D con Eulero implicito: matrice sparsa fattorizzata LU una volta per run (scipy.sparse.linalg.splu), a ogni passo solo i sistemi triangolari - Convezione sui lati esterno/interno, bordi adiabatici in x - Schema CSV invariato; metadata con geometria e griglia 2D Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,11 +31,13 @@ Non sono configurati test o linter.
|
|||||||
|
|
||||||
## Architettura
|
## Architettura
|
||||||
|
|
||||||
Generatore di dataset per misurazioni termiche pseudo-realistiche di una piastra riscaldata da una sorgente a induzione in movimento.
|
Generatore di dataset per misurazioni termiche pseudo-realistiche di una fascetta (anello cilindrico sottile) riscaldata da sorgenti a induzione in movimento.
|
||||||
|
|
||||||
|
**Geometria:** la fascetta ha diametro, spessore e lunghezza configurabili. Il dominio simulato è la sezione rettangolare lunghezza × spessore, con origine (0, 0) nel vertice in alto a sinistra: x = lunghezza (le sorgenti si muovono in direzione -x sul lato esterno), z = spessore (0 = lato esterno, spessore = lato interno). La coordinata circonferenziale y non è risolta: l'offset y delle sorgenti è collassato in un'attenuazione gaussiana del flusso. Il sensore è un pirometro a infrarossi dentro la fascetta che misura la superficie interna in un punto x fisso.
|
||||||
|
|
||||||
**Flusso dei dati:**
|
**Flusso dei dati:**
|
||||||
|
|
||||||
1. `config.py` — tutti i parametri configurabili (dizionari SIMULAZIONE, PIASTRA, ARIA, SORGENTE, SENSORE, RANDOMIZZAZIONE)
|
1. `config.py` — tutti i parametri configurabili (dizionari SIMULAZIONE, FASCETTA, ARIA, SORGENTE, SENSORE, RANDOMIZZAZIONE)
|
||||||
2. `materials.py` — dizionario MATERIALI con proprietà termofisiche ed elettriche per materiale
|
2. `materials.py` — dizionario MATERIALI con proprietà termofisiche ed elettriche per materiale
|
||||||
3. `simulate.py` — motore principale: genera N run randomizzati, scrive i CSV, scrive `metadata.csv`
|
3. `simulate.py` — motore principale: genera N run randomizzati, scrive i CSV, scrive `metadata.csv`
|
||||||
4. `plot_csv.py` — visualizzazione autonoma per un singolo run
|
4. `plot_csv.py` — visualizzazione autonoma per un singolo run
|
||||||
@@ -43,10 +45,10 @@ Generatore di dataset per misurazioni termiche pseudo-realistiche di una piastra
|
|||||||
**Pipeline fisica dentro `simula_singolo()` in [simulate.py](simulate.py):**
|
**Pipeline fisica dentro `simula_singolo()` in [simulate.py](simulate.py):**
|
||||||
|
|
||||||
- La skin depth è calcolata dalla resistività elettrica del materiale e dalla frequenza di induzione (`calcola_skin_depth_m`)
|
- La skin depth è calcolata dalla resistività elettrica del materiale e dalla frequenza di induzione (`calcola_skin_depth_m`)
|
||||||
- La sorgente gaussiana in movimento è proiettata sulla linea del sensore fisso per produrre un flusso termico superficiale variabile nel tempo (`flusso_termico_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 (`riscaldamento_volumetrico_W_m3`)
|
- 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 1D a volumi finiti con Eulero implicito integra l'equazione del calore su `n_nodi` celle (`costruisci_matrice_implicita`, poi `np.linalg.solve` ad ogni passo)
|
- Uno schema 2D a volumi finiti con Eulero implicito integra l'equazione del calore su `n_nodi_x × n_nodi_z` celle: la matrice sparsa è fattorizzata LU una volta per run (`costruisci_solutore_implicito_2d`, che restituisce l'oggetto `splu`), poi ad ogni passo si risolve solo il sistema triangolare
|
||||||
- Le condizioni al contorno di convezione sono incorporate nella matrice
|
- Le condizioni al contorno sono incorporate nella matrice: convezione sui lati esterno (z = 0) e interno (z = spessore), bordi adiabatici a x = 0 e x = lunghezza
|
||||||
- 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
|
||||||
|
|
||||||
**Randomizzazione per run** (`configurazione_randomizzata`): ogni run perturba velocità, flusso di picco, sigma del punto, offset y, temperatura ambiente e rumore del sensore con estrazioni gaussiane/uniformi da un RNG con seed fisso, garantendo riproducibilità.
|
**Randomizzazione per run** (`configurazione_randomizzata`): ogni run perturba velocità, flusso di picco, sigma del punto, offset y, temperatura ambiente e rumore del sensore con estrazioni gaussiane/uniformi da un RNG con seed fisso, garantendo riproducibilità.
|
||||||
@@ -59,7 +61,8 @@ Ogni parametro in [config.py](config.py) ha un commento che spiega solo cos'è (
|
|||||||
|
|
||||||
## Vincoli progettuali chiave
|
## Vincoli progettuali chiave
|
||||||
|
|
||||||
- Il modello è strettamente 1D (solo attraverso lo spessore). Il movimento laterale dell'induttore non è risolto spazialmente — è collassato in un flusso scalare variabile nel tempo sulla linea del sensore.
|
- Il modello è 2D nella sezione (x = lunghezza, z = spessore). La coordinata circonferenziale y non è risolta spazialmente — l'offset y del percorso delle sorgenti è collassato in un'attenuazione gaussiana del flusso; il diametro è registrato solo come geometria del setup.
|
||||||
- `costruisci_matrice_implicita` è calcolata una volta per run (proprietà del materiale costanti, nessun coefficiente dipendente dalla temperatura). Se si aggiungono proprietà dipendenti dalla temperatura, la matrice deve essere ricostruita ad ogni passo temporale.
|
- Le posizioni di inizio/fine corsa delle sorgenti (`x_inizio_m`, `x_fine_m`) sono distanze dal punto x del sensore lungo il verso di marcia; il segno di `velocita_m_s` determina il verso (negativo = -x).
|
||||||
|
- La matrice implicita è costruita e fattorizzata una volta per run (proprietà del materiale costanti, nessun coefficiente dipendente dalla temperatura). Se si aggiungono proprietà dipendenti dalla temperatura, la matrice deve essere ricostruita e rifattorizzata ad ogni passo temporale.
|
||||||
- `simulate.py` cancella e ricrea l'intera cartella di output ad ogni esecuzione (`shutil.rmtree`).
|
- `simulate.py` cancella e ricrea l'intera cartella di output ad ogni esecuzione (`shutil.rmtree`).
|
||||||
- Aggiungere un nuovo materiale richiede solo una nuova voce nel dizionario `MATERIALI` in [materials.py](materials.py); la chiave del materiale va poi impostata in `PIASTRA["materiale"]` in [config.py](config.py).
|
- Aggiungere un nuovo materiale richiede solo una nuova voce nel dizionario `MATERIALI` in [materials.py](materials.py); la chiave del materiale va poi impostata in `FASCETTA["materiale"]` in [config.py](config.py).
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
# Configurazione per il simulatore termico 1D attraverso lo spessore.
|
# Configurazione per il simulatore termico 2D della sezione di una fascetta.
|
||||||
#
|
#
|
||||||
# Modello fisico:
|
# Geometria e modello fisico:
|
||||||
# - La piastra è ridotta a una dimensione: z = direzione dello spessore.
|
# - La fascetta è un anello cilindrico con diametro "diametro_mm", spessore
|
||||||
# - La sorgente di calore si trova sul lato caldo, z = 0.
|
# "spessore_mm" e lunghezza "lunghezza_mm".
|
||||||
# - Il sensore si trova sul lato opposto, z = spessore.
|
# - Il dominio simulato è la sezione rettangolare lunghezza × spessore.
|
||||||
# - L'induttore si muove lungo x. Poiché il modello è 1D, il movimento è
|
# - Sistema di coordinate: origine (0, 0) nel vertice in alto a sinistra
|
||||||
# rappresentato come un flusso termico variabile nel tempo nel punto
|
# della sezione. x = direzione della lunghezza (da 0 a lunghezza),
|
||||||
# allineato con il sensore fisso.
|
# z = direzione dello spessore (0 = lato esterno, dove agiscono le
|
||||||
|
# sorgenti; spessore = lato interno, osservato dal sensore).
|
||||||
|
# - y è la coordinata circonferenziale: non è risolta spazialmente, l'offset
|
||||||
|
# y del percorso delle sorgenti è collassato in un'attenuazione gaussiana
|
||||||
|
# del flusso.
|
||||||
|
# - Le sorgenti a induzione si muovono in direzione -x sul lato esterno.
|
||||||
# - Il riscaldamento a induzione è approssimato come riscaldamento volumetrico
|
# - Il riscaldamento a induzione è approssimato come riscaldamento volumetrico
|
||||||
# che decade esponenzialmente con la profondità secondo un parametro skin depth.
|
# che decade esponenzialmente con la profondità z secondo la skin depth.
|
||||||
|
# - Il sensore è un pirometro a infrarossi posto all'interno della fascetta,
|
||||||
|
# a distanza "distanza_parete_mm" dalla parete interna: misura senza
|
||||||
|
# contatto la temperatura della superficie interna nel punto x = "x_mm".
|
||||||
#
|
#
|
||||||
# Unità di misura:
|
# Unità di misura:
|
||||||
# - lunghezza: m
|
# - lunghezza: m (mm dove indicato dal suffisso)
|
||||||
# - tempo: s
|
# - tempo: s
|
||||||
# - temperatura: °C
|
# - temperatura: °C
|
||||||
# - flusso termico: W/m²
|
# - flusso termico: W/m²
|
||||||
@@ -39,13 +47,22 @@ SIMULAZIONE = {
|
|||||||
"cartella_output": "dataset",
|
"cartella_output": "dataset",
|
||||||
}
|
}
|
||||||
|
|
||||||
PIASTRA = {
|
FASCETTA = {
|
||||||
# Spessore della piastra [mm].
|
# Diametro della fascetta [mm].
|
||||||
|
"diametro_mm": 70.0,
|
||||||
|
|
||||||
|
# Lunghezza della fascetta lungo x [mm].
|
||||||
|
"lunghezza_mm": 100.0,
|
||||||
|
|
||||||
|
# Spessore della parete [mm].
|
||||||
"spessore_mm": 0.12,
|
"spessore_mm": 0.12,
|
||||||
|
|
||||||
# Numero di celle del volume finito attraverso lo spessore.
|
# Numero di celle del volume finito lungo x (lunghezza).
|
||||||
|
"n_nodi_x": 100,
|
||||||
|
|
||||||
|
# Numero di celle del volume finito lungo z (spessore).
|
||||||
# Più nodi = maggiore risoluzione spaziale, simulazione più lenta.
|
# Più nodi = maggiore risoluzione spaziale, simulazione più lenta.
|
||||||
"n_nodi": 61,
|
"n_nodi_z": 15,
|
||||||
|
|
||||||
# Temperatura iniziale uniforme.
|
# Temperatura iniziale uniforme.
|
||||||
"temperatura_iniziale_C": 25.0,
|
"temperatura_iniziale_C": 25.0,
|
||||||
@@ -58,11 +75,11 @@ ARIA = {
|
|||||||
# Temperatura dell'aria ambiente.
|
# Temperatura dell'aria ambiente.
|
||||||
"temperatura_ambiente_C": 25.0,
|
"temperatura_ambiente_C": 25.0,
|
||||||
|
|
||||||
# Coefficiente di convezione sul lato caldo (sorgente).
|
# Coefficiente di convezione sul lato esterno (z = 0, lato sorgenti).
|
||||||
"h_caldo_W_m2K": 12.0,
|
"h_esterno_W_m2K": 12.0,
|
||||||
|
|
||||||
# Coefficiente di convezione sul lato freddo (sensore).
|
# Coefficiente di convezione sul lato interno (z = spessore, lato sensore).
|
||||||
"h_freddo_W_m2K": 8.0,
|
"h_interno_W_m2K": 8.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
SORGENTE = {
|
SORGENTE = {
|
||||||
@@ -74,16 +91,13 @@ SORGENTE = {
|
|||||||
# gruppo più lontana dal sensore (quella che lo supera per ultima).
|
# gruppo più lontana dal sensore (quella che lo supera per ultima).
|
||||||
"x_fine_m": 5.0,
|
"x_fine_m": 5.0,
|
||||||
|
|
||||||
# Coordinata laterale fissa della proiezione del sensore sul lato caldo.
|
# Offset circonferenziale (y) tra il percorso delle sorgenti e il punto
|
||||||
# L'effetto della sorgente è massimo quando x_sorgente == x_sensore e offset_y_m == 0.
|
# osservato dal sensore. Se diverso da zero, la sorgente passa a lato,
|
||||||
"x_sensore_m": 0.0,
|
# riducendo il picco.
|
||||||
|
|
||||||
# Offset laterale tra il percorso della sorgente e la linea del sensore.
|
|
||||||
# Se diverso da zero, la sorgente passa a lato del sensore, riducendo il picco.
|
|
||||||
"offset_y_percorso_m": 0.0,
|
"offset_y_percorso_m": 0.0,
|
||||||
|
|
||||||
# Velocità della sorgente lungo x.
|
# Velocità delle sorgenti lungo x. Il segno indica il verso di marcia.
|
||||||
"velocita_m_s": 2.0,
|
"velocita_m_s": -2.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).
|
||||||
@@ -98,7 +112,7 @@ SORGENTE = {
|
|||||||
# 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": 5500000.0,
|
||||||
|
|
||||||
# Frazione del flusso incidente che diventa effettivamente calore nella piastra.
|
# Frazione del flusso incidente che diventa effettivamente calore nella fascetta.
|
||||||
"efficienza_riscaldamento": 0.35,
|
"efficienza_riscaldamento": 0.35,
|
||||||
|
|
||||||
# Frequenza di induzione usata per stimare la skin depth se skin_depth_fissa_m è None.
|
# Frequenza di induzione usata per stimare la skin depth se skin_depth_fissa_m è None.
|
||||||
@@ -113,8 +127,13 @@ SORGENTE = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SENSORE = {
|
SENSORE = {
|
||||||
# Il sensore si trova sul lato freddo della piastra.
|
# Coordinata x del punto della superficie interna osservato dal sensore [mm].
|
||||||
"posizione": "lato_freddo",
|
"x_mm": 50.0,
|
||||||
|
|
||||||
|
# Distanza del sensore dalla parete interna lungo z [mm].
|
||||||
|
# Il sensore è a infrarossi: la distanza non influenza la misura,
|
||||||
|
# è registrata solo come geometria del setup.
|
||||||
|
"distanza_parete_mm": 10.0,
|
||||||
|
|
||||||
# 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.
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# Database dei materiali per il simulatore termico 1D.
|
# Database dei materiali per il simulatore termico.
|
||||||
#
|
#
|
||||||
# Tutte le unità sono SI:
|
# Tutte le unità sono SI:
|
||||||
# - conducibilita_termica_W_mK
|
# - conducibilita_termica_W_mK
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ def main() -> None:
|
|||||||
plt.plot(df["tempo_s"], df["flusso_termico_sorgente_W_m2"])
|
plt.plot(df["tempo_s"], df["flusso_termico_sorgente_W_m2"])
|
||||||
plt.xlabel("Tempo [s]")
|
plt.xlabel("Tempo [s]")
|
||||||
plt.ylabel("Flusso termico efficace [W/m²]")
|
plt.ylabel("Flusso termico efficace [W/m²]")
|
||||||
plt.title("Flusso termico visto dalla linea del sensore")
|
plt.title("Flusso termico nel punto x osservato dal sensore")
|
||||||
plt.grid(True)
|
plt.grid(True)
|
||||||
plt.tight_layout()
|
plt.tight_layout()
|
||||||
plt.show()
|
plt.show()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
numpy
|
numpy
|
||||||
|
scipy
|
||||||
pandas
|
pandas
|
||||||
matplotlib
|
matplotlib
|
||||||
|
|||||||
+126
-107
@@ -6,8 +6,10 @@ from copy import deepcopy
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import scipy.sparse as sp
|
||||||
|
from scipy.sparse.linalg import splu
|
||||||
|
|
||||||
from config import ARIA, PIASTRA, RANDOMIZZAZIONE, SENSORE, SIMULAZIONE, SORGENTE
|
from config import ARIA, FASCETTA, RANDOMIZZAZIONE, SENSORE, SIMULAZIONE, SORGENTE
|
||||||
from materials import MATERIALI
|
from materials import MATERIALI
|
||||||
|
|
||||||
|
|
||||||
@@ -34,31 +36,28 @@ def _spread_sorgenti_m(sorgente: dict) -> float:
|
|||||||
return (numero_sorgenti - 1) * distanza
|
return (numero_sorgenti - 1) * distanza
|
||||||
|
|
||||||
|
|
||||||
def _x_riferimento_iniziale_m(sorgente: dict) -> float:
|
def _x_riferimento_iniziale_m(sorgente: dict, x_sensore_m: float) -> float:
|
||||||
# Posizione a t=0 della sorgente di indice 0 (quella più arretrata nel
|
# Posizione a t=0 della sorgente di indice 0. Le sorgenti i sono a
|
||||||
# verso di marcia). x_inizio_m è la distanza dal sensore della sorgente
|
# x_i = riferimento + i * distanza, quindi per v >= 0 l'indice 0 è la
|
||||||
# più avanzata (che quindi lo raggiunge per prima).
|
# più arretrata nel verso di marcia, per v < 0 è la più avanzata.
|
||||||
|
# x_inizio_m è la distanza dal sensore della sorgente più avanzata
|
||||||
|
# (quella che lo raggiunge per prima).
|
||||||
spread = _spread_sorgenti_m(sorgente)
|
spread = _spread_sorgenti_m(sorgente)
|
||||||
x_sensore = sorgente["x_sensore_m"]
|
|
||||||
x_inizio = sorgente["x_inizio_m"]
|
x_inizio = sorgente["x_inizio_m"]
|
||||||
if sorgente["velocita_m_s"] >= 0:
|
if sorgente["velocita_m_s"] >= 0:
|
||||||
return (x_sensore - x_inizio) - spread
|
return (x_sensore_m - x_inizio) - spread
|
||||||
return (x_sensore + x_inizio) + spread
|
return x_sensore_m + x_inizio
|
||||||
|
|
||||||
|
|
||||||
def _x_riferimento_finale_m(sorgente: dict) -> float:
|
def _x_riferimento_finale_m(sorgente: dict, x_sensore_m: float) -> float:
|
||||||
# Posizione di fine corsa della sorgente di indice 0. x_fine_m è la
|
# Posizione di fine corsa della sorgente di indice 0. x_fine_m è la
|
||||||
# distanza dal sensore della sorgente più arretrata (che quindi lo
|
# distanza dal sensore della sorgente più arretrata nel verso di marcia
|
||||||
# supera per ultima).
|
# (quella che lo supera per ultima).
|
||||||
x_sensore = sorgente["x_sensore_m"]
|
spread = _spread_sorgenti_m(sorgente)
|
||||||
x_fine = sorgente["x_fine_m"]
|
x_fine = sorgente["x_fine_m"]
|
||||||
if sorgente["velocita_m_s"] >= 0:
|
if sorgente["velocita_m_s"] >= 0:
|
||||||
return x_sensore + x_fine
|
return x_sensore_m + x_fine
|
||||||
return x_sensore - x_fine
|
return (x_sensore_m - x_fine) - spread
|
||||||
|
|
||||||
|
|
||||||
def x_sorgente_al_tempo(sorgente: dict, t_s: float) -> float:
|
|
||||||
return _x_riferimento_iniziale_m(sorgente) + sorgente["velocita_m_s"] * t_s
|
|
||||||
|
|
||||||
|
|
||||||
def _intervallo_attivo(inizio: float, fine: float, v: float, x_m: float) -> bool:
|
def _intervallo_attivo(inizio: float, fine: float, v: float, x_m: float) -> bool:
|
||||||
@@ -67,18 +66,21 @@ def _intervallo_attivo(inizio: float, fine: float, v: float, x_m: float) -> bool
|
|||||||
return fine <= x_m <= inizio
|
return fine <= x_m <= inizio
|
||||||
|
|
||||||
|
|
||||||
def flusso_termico_incidente_W_m2(sorgente: dict, t_s: float) -> tuple[float, float]:
|
def profilo_flusso_incidente_W_m2(
|
||||||
|
sorgente: dict,
|
||||||
|
x_sensore_m: float,
|
||||||
|
t_s: float,
|
||||||
|
x_centri_m: np.ndarray,
|
||||||
|
) -> tuple[float, np.ndarray]:
|
||||||
# Restituisce x_sorgente_m (posizione della sorgente di riferimento) e
|
# Restituisce x_sorgente_m (posizione della sorgente di riferimento) e
|
||||||
# flusso_termico_efficace_W_m2 (somma dei contributi di tutte le sorgenti).
|
# il profilo di flusso termico efficace q(x) [W/m²] sul lato esterno,
|
||||||
|
# somma dei contributi di tutte le sorgenti attive.
|
||||||
#
|
#
|
||||||
# Il movimento è rappresentato con un'impronta gaussiana centrata su
|
# Ogni sorgente in moto ha un'impronta gaussiana lungo x, valutata sui
|
||||||
# ciascuna sorgente in moto. Più sorgenti equidistanti si muovono insieme
|
# centri cella della sezione. L'offset circonferenziale y non è risolto
|
||||||
# come un gruppo rigido: condividono velocità, sigma e flusso di picco, e
|
# spazialmente: entra come attenuazione gaussiana del flusso.
|
||||||
# sono sfalsate lungo x di un multiplo di "distanza_sorgenti_m". Il
|
x_rif_iniziale = _x_riferimento_iniziale_m(sorgente, x_sensore_m)
|
||||||
# modello 1D vede solo il flusso lungo la linea che passa per il sensore
|
x_rif_finale = _x_riferimento_finale_m(sorgente, x_sensore_m)
|
||||||
# fisso, sommando il contributo di tutte le sorgenti attive.
|
|
||||||
x_rif_iniziale = _x_riferimento_iniziale_m(sorgente)
|
|
||||||
x_rif_finale = _x_riferimento_finale_m(sorgente)
|
|
||||||
x_riferimento = x_rif_iniziale + sorgente["velocita_m_s"] * t_s
|
x_riferimento = x_rif_iniziale + sorgente["velocita_m_s"] * t_s
|
||||||
|
|
||||||
numero_sorgenti = sorgente.get("numero_sorgenti", 1)
|
numero_sorgenti = sorgente.get("numero_sorgenti", 1)
|
||||||
@@ -88,7 +90,10 @@ def flusso_termico_incidente_W_m2(sorgente: dict, t_s: float) -> tuple[float, fl
|
|||||||
dy = sorgente["offset_y_percorso_m"]
|
dy = sorgente["offset_y_percorso_m"]
|
||||||
sigma = sorgente["sigma_punto_m"]
|
sigma = sorgente["sigma_punto_m"]
|
||||||
|
|
||||||
q_totale = 0.0
|
q_picco = sorgente["flusso_termico_picco_W_m2"] * sorgente["efficienza_riscaldamento"]
|
||||||
|
attenuazione_y = math.exp(-0.5 * (dy * dy) / (sigma * sigma))
|
||||||
|
|
||||||
|
q_x = np.zeros_like(x_centri_m)
|
||||||
for i in range(numero_sorgenti):
|
for i in range(numero_sorgenti):
|
||||||
x_i = x_riferimento + i * distanza
|
x_i = x_riferimento + i * distanza
|
||||||
|
|
||||||
@@ -98,74 +103,76 @@ def flusso_termico_incidente_W_m2(sorgente: dict, t_s: float) -> tuple[float, fl
|
|||||||
if not _intervallo_attivo(inizio_i, fine_i, v, x_i):
|
if not _intervallo_attivo(inizio_i, fine_i, v, x_i):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
dx = x_i - sorgente["x_sensore_m"]
|
dx = x_centri_m - x_i
|
||||||
gaussiana = math.exp(-0.5 * (dx * dx + dy * dy) / (sigma * sigma))
|
q_x += q_picco * attenuazione_y * np.exp(-0.5 * (dx * dx) / (sigma * sigma))
|
||||||
q_totale += (
|
|
||||||
sorgente["flusso_termico_picco_W_m2"]
|
|
||||||
* sorgente["efficienza_riscaldamento"]
|
|
||||||
* gaussiana
|
|
||||||
)
|
|
||||||
|
|
||||||
return x_riferimento, q_totale
|
return x_riferimento, q_x
|
||||||
|
|
||||||
|
|
||||||
def riscaldamento_volumetrico_W_m3(
|
def profilo_deposizione_z_1_m(
|
||||||
q_superficie_W_m2: float,
|
|
||||||
z_centri_m: np.ndarray,
|
z_centri_m: np.ndarray,
|
||||||
spessore_m: float,
|
spessore_m: float,
|
||||||
skin_depth_m: float,
|
skin_depth_m: float,
|
||||||
) -> np.ndarray:
|
) -> np.ndarray:
|
||||||
# Converte il flusso superficiale equivalente in riscaldamento volumetrico q_vol(z).
|
# Profilo di deposizione volumetrica del flusso superficiale [1/m]:
|
||||||
#
|
#
|
||||||
# q_vol(z) = A * exp(-z / delta)
|
# p(z) = exp(-z / delta) / (delta * (1 - exp(-spessore / delta)))
|
||||||
#
|
#
|
||||||
# A è scelto in modo che integrale_0^L q_vol(z) dz = q_superficie_W_m2.
|
# Normalizzato in modo che integrale_0^spessore p(z) dz = 1, così che
|
||||||
if q_superficie_W_m2 <= 0.0:
|
# q_vol(x, z) = q(x) * p(z) conservi il flusso superficiale.
|
||||||
return np.zeros_like(z_centri_m)
|
|
||||||
|
|
||||||
delta = max(skin_depth_m, 1e-9)
|
delta = max(skin_depth_m, 1e-9)
|
||||||
normalizzazione = delta * (1.0 - math.exp(-spessore_m / delta))
|
normalizzazione = delta * (1.0 - math.exp(-spessore_m / delta))
|
||||||
return (q_superficie_W_m2 / normalizzazione) * np.exp(-z_centri_m / delta)
|
return np.exp(-z_centri_m / delta) / normalizzazione
|
||||||
|
|
||||||
|
|
||||||
def costruisci_matrice_implicita(
|
def _laplaciano_1d(n: int) -> sp.spmatrix:
|
||||||
n: int,
|
# Operatore alle differenze -T'' su n celle con bordi adiabatici (Neumann).
|
||||||
|
diagonale = np.full(n, 2.0)
|
||||||
|
diagonale[0] = 1.0
|
||||||
|
diagonale[-1] = 1.0
|
||||||
|
fuori = -np.ones(n - 1)
|
||||||
|
return sp.diags([fuori, diagonale, fuori], [-1, 0, 1])
|
||||||
|
|
||||||
|
|
||||||
|
def costruisci_solutore_implicito_2d(
|
||||||
|
n_x: int,
|
||||||
|
n_z: int,
|
||||||
dt_s: float,
|
dt_s: float,
|
||||||
|
dx_m: float,
|
||||||
dz_m: float,
|
dz_m: float,
|
||||||
materiale: dict,
|
materiale: dict,
|
||||||
h_caldo_W_m2K: float,
|
h_esterno_W_m2K: float,
|
||||||
h_freddo_W_m2K: float,
|
h_interno_W_m2K: float,
|
||||||
) -> np.ndarray:
|
):
|
||||||
# Costruisce la matrice A per Eulero implicito:
|
# Costruisce e fattorizza (LU sparsa) la matrice A per Eulero implicito 2D:
|
||||||
# A * T_next = rhs
|
# A * T_next = rhs
|
||||||
#
|
#
|
||||||
# Le celle di bordo includono la convezione verso l'ambiente.
|
# Le incognite sono i centri cella T[i, j] con i lungo x e j lungo z,
|
||||||
|
# appiattiti in ordine C (indice = i * n_z + j). I bordi x = 0 e
|
||||||
|
# x = lunghezza sono adiabatici; i bordi z = 0 (esterno) e z = spessore
|
||||||
|
# (interno) includono la convezione verso l'ambiente.
|
||||||
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"]
|
||||||
alpha = k / (rho * cp)
|
alpha = k / (rho * cp)
|
||||||
|
|
||||||
r = alpha * dt_s / (dz_m * dz_m)
|
r_x = alpha * dt_s / (dx_m * dx_m)
|
||||||
b_caldo = h_caldo_W_m2K * dt_s / (rho * cp * dz_m)
|
r_z = alpha * dt_s / (dz_m * dz_m)
|
||||||
b_freddo = h_freddo_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)
|
||||||
|
|
||||||
A = np.zeros((n, n), dtype=float)
|
n = n_x * n_z
|
||||||
|
convezione = np.zeros(n)
|
||||||
|
convezione[0::n_z] = b_esterno
|
||||||
|
convezione[n_z - 1::n_z] = b_interno
|
||||||
|
|
||||||
# Bordo caldo, z = 0.
|
A = (
|
||||||
A[0, 0] = 1.0 + r + b_caldo
|
sp.identity(n)
|
||||||
A[0, 1] = -r
|
+ r_x * sp.kron(_laplaciano_1d(n_x), sp.identity(n_z))
|
||||||
|
+ r_z * sp.kron(sp.identity(n_x), _laplaciano_1d(n_z))
|
||||||
# Celle interne.
|
+ sp.diags(convezione)
|
||||||
for i in range(1, n - 1):
|
)
|
||||||
A[i, i - 1] = -r
|
return splu(sp.csc_matrix(A))
|
||||||
A[i, i] = 1.0 + 2.0 * r
|
|
||||||
A[i, i + 1] = -r
|
|
||||||
|
|
||||||
# Bordo freddo, z = spessore.
|
|
||||||
A[n - 1, n - 2] = -r
|
|
||||||
A[n - 1, n - 1] = 1.0 + r + b_freddo
|
|
||||||
|
|
||||||
return A
|
|
||||||
|
|
||||||
|
|
||||||
def quantizza(valore: float, passo: float) -> float:
|
def quantizza(valore: float, passo: float) -> float:
|
||||||
@@ -175,7 +182,7 @@ def quantizza(valore: float, passo: float) -> float:
|
|||||||
|
|
||||||
|
|
||||||
def configurazione_randomizzata(indice_run: int, rng: random.Random) -> dict:
|
def configurazione_randomizzata(indice_run: int, rng: random.Random) -> dict:
|
||||||
piastra = deepcopy(PIASTRA)
|
fascetta = deepcopy(FASCETTA)
|
||||||
aria = deepcopy(ARIA)
|
aria = deepcopy(ARIA)
|
||||||
sorgente = deepcopy(SORGENTE)
|
sorgente = deepcopy(SORGENTE)
|
||||||
sensore = deepcopy(SENSORE)
|
sensore = deepcopy(SENSORE)
|
||||||
@@ -214,7 +221,7 @@ def configurazione_randomizzata(indice_run: int, rng: random.Random) -> dict:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"id_run": f"run_{indice_run:04d}",
|
"id_run": f"run_{indice_run:04d}",
|
||||||
"piastra": piastra,
|
"fascetta": fascetta,
|
||||||
"aria": aria,
|
"aria": aria,
|
||||||
"sorgente": sorgente,
|
"sorgente": sorgente,
|
||||||
"sensore": sensore,
|
"sensore": sensore,
|
||||||
@@ -222,18 +229,25 @@ def configurazione_randomizzata(indice_run: int, rng: random.Random) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
||||||
piastra = cfg_run["piastra"]
|
fascetta = cfg_run["fascetta"]
|
||||||
aria = cfg_run["aria"]
|
aria = cfg_run["aria"]
|
||||||
sorgente = cfg_run["sorgente"]
|
sorgente = cfg_run["sorgente"]
|
||||||
sensore = cfg_run["sensore"]
|
sensore = cfg_run["sensore"]
|
||||||
|
|
||||||
nome_materiale = piastra["materiale"]
|
nome_materiale = fascetta["materiale"]
|
||||||
materiale = MATERIALI[nome_materiale]
|
materiale = MATERIALI[nome_materiale]
|
||||||
|
|
||||||
spessore = piastra["spessore_mm"] / 1000.0
|
lunghezza = fascetta["lunghezza_mm"] / 1000.0
|
||||||
n = piastra["n_nodi"]
|
spessore = fascetta["spessore_mm"] / 1000.0
|
||||||
dz = spessore / n
|
n_x = fascetta["n_nodi_x"]
|
||||||
z_centri = (np.arange(n) + 0.5) * dz
|
n_z = fascetta["n_nodi_z"]
|
||||||
|
dx = lunghezza / n_x
|
||||||
|
dz = spessore / n_z
|
||||||
|
x_centri = (np.arange(n_x) + 0.5) * dx
|
||||||
|
z_centri = (np.arange(n_z) + 0.5) * dz
|
||||||
|
|
||||||
|
x_sensore = sensore["x_mm"] / 1000.0
|
||||||
|
i_sensore = min(n_x - 1, max(0, int(x_sensore / dx)))
|
||||||
|
|
||||||
dt = SIMULAZIONE["dt_interno_s"]
|
dt = SIMULAZIONE["dt_interno_s"]
|
||||||
durata = SIMULAZIONE["durata_s"]
|
durata = SIMULAZIONE["durata_s"]
|
||||||
@@ -244,26 +258,30 @@ def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
|||||||
else:
|
else:
|
||||||
skin_depth = float(sorgente["skin_depth_fissa_m"])
|
skin_depth = float(sorgente["skin_depth_fissa_m"])
|
||||||
|
|
||||||
A = costruisci_matrice_implicita(
|
solutore = costruisci_solutore_implicito_2d(
|
||||||
n=n,
|
n_x=n_x,
|
||||||
|
n_z=n_z,
|
||||||
dt_s=dt,
|
dt_s=dt,
|
||||||
|
dx_m=dx,
|
||||||
dz_m=dz,
|
dz_m=dz,
|
||||||
materiale=materiale,
|
materiale=materiale,
|
||||||
h_caldo_W_m2K=aria["h_caldo_W_m2K"],
|
h_esterno_W_m2K=aria["h_esterno_W_m2K"],
|
||||||
h_freddo_W_m2K=aria["h_freddo_W_m2K"],
|
h_interno_W_m2K=aria["h_interno_W_m2K"],
|
||||||
)
|
)
|
||||||
|
|
||||||
rho = materiale["densita_kg_m3"]
|
rho = materiale["densita_kg_m3"]
|
||||||
cp = materiale["calore_specifico_J_kgK"]
|
cp = materiale["calore_specifico_J_kgK"]
|
||||||
|
|
||||||
b_caldo = aria["h_caldo_W_m2K"] * dt / (rho * cp * dz)
|
b_esterno = aria["h_esterno_W_m2K"] * dt / (rho * cp * dz)
|
||||||
b_freddo = aria["h_freddo_W_m2K"] * dt / (rho * cp * dz)
|
b_interno = aria["h_interno_W_m2K"] * dt / (rho * cp * dz)
|
||||||
|
|
||||||
T = np.full(n, piastra["temperatura_iniziale_C"], dtype=float)
|
profilo_z = profilo_deposizione_z_1_m(z_centri, spessore, skin_depth)
|
||||||
T_sensore = T[-1]
|
|
||||||
|
T = np.full((n_x, n_z), fascetta["temperatura_iniziale_C"], dtype=float)
|
||||||
|
T_sensore = T[i_sensore, -1]
|
||||||
|
|
||||||
prossimo_campione_t = 0.0
|
prossimo_campione_t = 0.0
|
||||||
T_vera_max = T[-1]
|
T_vera_max = T[i_sensore, -1]
|
||||||
T_misurata_max = T_sensore
|
T_misurata_max = T_sensore
|
||||||
|
|
||||||
output_csv.parent.mkdir(parents=True, exist_ok=True)
|
output_csv.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -289,22 +307,19 @@ def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
|||||||
|
|
||||||
t = 0.0
|
t = 0.0
|
||||||
while t <= durata + 1e-12:
|
while t <= durata + 1e-12:
|
||||||
x_sorgente, q_superficie = flusso_termico_incidente_W_m2(sorgente, t)
|
x_sorgente, q_x = profilo_flusso_incidente_W_m2(
|
||||||
q_vol = riscaldamento_volumetrico_W_m3(
|
sorgente, x_sensore, t, x_centri
|
||||||
q_superficie_W_m2=q_superficie,
|
|
||||||
z_centri_m=z_centri,
|
|
||||||
spessore_m=spessore,
|
|
||||||
skin_depth_m=skin_depth,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
rhs = T + dt * q_vol / (rho * cp)
|
rhs = T + (dt / (rho * cp)) * q_x[:, None] * profilo_z[None, :]
|
||||||
rhs[0] += b_caldo * aria["temperatura_ambiente_C"]
|
rhs[:, 0] += b_esterno * aria["temperatura_ambiente_C"]
|
||||||
rhs[-1] += b_freddo * aria["temperatura_ambiente_C"]
|
rhs[:, -1] += b_interno * aria["temperatura_ambiente_C"]
|
||||||
|
|
||||||
T = np.linalg.solve(A, rhs)
|
T = solutore.solve(rhs.ravel()).reshape(n_x, n_z)
|
||||||
|
|
||||||
# Temperatura vera sul lato freddo, dove si trova il sensore fisso.
|
# Temperatura vera della superficie interna nel punto osservato
|
||||||
T_vera_lato_sensore = T[-1]
|
# dal sensore infrarosso.
|
||||||
|
T_vera_lato_sensore = T[i_sensore, -1]
|
||||||
|
|
||||||
# Inerzia del sensore del primo ordine.
|
# Inerzia del sensore del primo ordine.
|
||||||
tau_sensore = max(sensore["costante_tempo_s"], 1e-9)
|
tau_sensore = max(sensore["costante_tempo_s"], 1e-9)
|
||||||
@@ -323,11 +338,11 @@ def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
|||||||
f"{t:.6f}",
|
f"{t:.6f}",
|
||||||
f"{x_sorgente:.9f}",
|
f"{x_sorgente:.9f}",
|
||||||
f"{sorgente['offset_y_percorso_m']:.9f}",
|
f"{sorgente['offset_y_percorso_m']:.9f}",
|
||||||
f"{q_superficie:.6f}",
|
f"{q_x[i_sensore]:.6f}",
|
||||||
f"{skin_depth:.9e}",
|
f"{skin_depth:.9e}",
|
||||||
f"{T_vera_lato_sensore:.6f}",
|
f"{T_vera_lato_sensore:.6f}",
|
||||||
f"{misurata:.6f}",
|
f"{misurata:.6f}",
|
||||||
f"{T[0]:.6f}",
|
f"{T[i_sensore, 0]:.6f}",
|
||||||
f"{aria['temperatura_ambiente_C']:.6f}",
|
f"{aria['temperatura_ambiente_C']:.6f}",
|
||||||
f"{sorgente['velocita_m_s']:.9f}",
|
f"{sorgente['velocita_m_s']:.9f}",
|
||||||
f"{sorgente['sigma_punto_m']:.9f}",
|
f"{sorgente['sigma_punto_m']:.9f}",
|
||||||
@@ -342,17 +357,21 @@ def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
|||||||
"id_run": cfg_run["id_run"],
|
"id_run": cfg_run["id_run"],
|
||||||
"file_csv": str(output_csv.name),
|
"file_csv": str(output_csv.name),
|
||||||
"materiale": nome_materiale,
|
"materiale": nome_materiale,
|
||||||
|
"diametro_m": fascetta["diametro_mm"] / 1000.0,
|
||||||
|
"lunghezza_m": lunghezza,
|
||||||
"spessore_m": spessore,
|
"spessore_m": spessore,
|
||||||
"n_nodi": n,
|
"n_nodi_x": n_x,
|
||||||
|
"n_nodi_z": n_z,
|
||||||
"durata_s": durata,
|
"durata_s": durata,
|
||||||
"frequenza_campionamento_hz": SIMULAZIONE["frequenza_campionamento_hz"],
|
"frequenza_campionamento_hz": SIMULAZIONE["frequenza_campionamento_hz"],
|
||||||
"dt_interno_s": dt,
|
"dt_interno_s": dt,
|
||||||
"temperatura_ambiente_C": aria["temperatura_ambiente_C"],
|
"temperatura_ambiente_C": aria["temperatura_ambiente_C"],
|
||||||
"h_caldo_W_m2K": aria["h_caldo_W_m2K"],
|
"h_esterno_W_m2K": aria["h_esterno_W_m2K"],
|
||||||
"h_freddo_W_m2K": aria["h_freddo_W_m2K"],
|
"h_interno_W_m2K": aria["h_interno_W_m2K"],
|
||||||
"x_inizio_m": sorgente["x_inizio_m"],
|
"x_inizio_m": sorgente["x_inizio_m"],
|
||||||
"x_fine_m": sorgente["x_fine_m"],
|
"x_fine_m": sorgente["x_fine_m"],
|
||||||
"x_sensore_m": sorgente["x_sensore_m"],
|
"x_sensore_m": x_sensore,
|
||||||
|
"distanza_sensore_parete_m": sensore["distanza_parete_mm"] / 1000.0,
|
||||||
"offset_y_percorso_m": sorgente["offset_y_percorso_m"],
|
"offset_y_percorso_m": sorgente["offset_y_percorso_m"],
|
||||||
"velocita_m_s": sorgente["velocita_m_s"],
|
"velocita_m_s": sorgente["velocita_m_s"],
|
||||||
"numero_sorgenti": sorgente.get("numero_sorgenti", 1),
|
"numero_sorgenti": sorgente.get("numero_sorgenti", 1),
|
||||||
|
|||||||
Reference in New Issue
Block a user