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:
2026-07-05 19:20:47 +02:00
parent b40ee49528
commit bb492c37a6
6 changed files with 189 additions and 147 deletions
+12 -9
View File
@@ -31,11 +31,13 @@ Non sono configurati test o linter.
## 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:**
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
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
@@ -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):**
- 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`)
- Quel flusso è ridistribuito volumetricamente attraverso lo spessore con decadimento esponenziale (`riscaldamento_volumetrico_W_m3`)
- 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)
- Le condizioni al contorno di convezione sono incorporate nella matrice
- 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)
- 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 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
**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
- 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.
- `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.
- 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.
- 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`).
- 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).
+48 -29
View File
@@ -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:
# - La piastra è ridotta a una dimensione: z = direzione dello spessore.
# - La sorgente di calore si trova sul lato caldo, z = 0.
# - Il sensore si trova sul lato opposto, z = spessore.
# - L'induttore si muove lungo x. Poiché il modello è 1D, il movimento è
# rappresentato come un flusso termico variabile nel tempo nel punto
# allineato con il sensore fisso.
# Geometria e modello fisico:
# - La fascetta è un anello cilindrico con diametro "diametro_mm", spessore
# "spessore_mm" e lunghezza "lunghezza_mm".
# - Il dominio simulato è la sezione rettangolare lunghezza × spessore.
# - Sistema di coordinate: origine (0, 0) nel vertice in alto a sinistra
# della sezione. x = direzione della lunghezza (da 0 a lunghezza),
# 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
# 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:
# - lunghezza: m
# - lunghezza: m (mm dove indicato dal suffisso)
# - tempo: s
# - temperatura: °C
# - flusso termico: W/m²
@@ -39,13 +47,22 @@ SIMULAZIONE = {
"cartella_output": "dataset",
}
PIASTRA = {
# Spessore della piastra [mm].
FASCETTA = {
# 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,
# 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.
"n_nodi": 61,
"n_nodi_z": 15,
# Temperatura iniziale uniforme.
"temperatura_iniziale_C": 25.0,
@@ -58,11 +75,11 @@ ARIA = {
# Temperatura dell'aria ambiente.
"temperatura_ambiente_C": 25.0,
# Coefficiente di convezione sul lato caldo (sorgente).
"h_caldo_W_m2K": 12.0,
# Coefficiente di convezione sul lato esterno (z = 0, lato sorgenti).
"h_esterno_W_m2K": 12.0,
# Coefficiente di convezione sul lato freddo (sensore).
"h_freddo_W_m2K": 8.0,
# Coefficiente di convezione sul lato interno (z = spessore, lato sensore).
"h_interno_W_m2K": 8.0,
}
SORGENTE = {
@@ -74,16 +91,13 @@ SORGENTE = {
# gruppo più lontana dal sensore (quella che lo supera per ultima).
"x_fine_m": 5.0,
# Coordinata laterale fissa della proiezione del sensore sul lato caldo.
# L'effetto della sorgente è massimo quando x_sorgente == x_sensore e offset_y_m == 0.
"x_sensore_m": 0.0,
# 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 circonferenziale (y) tra il percorso delle sorgenti e il punto
# osservato dal sensore. Se diverso da zero, la sorgente passa a lato,
# riducendo il picco.
"offset_y_percorso_m": 0.0,
# Velocità della sorgente lungo x.
"velocita_m_s": 2.0,
# Velocità delle sorgenti lungo x. Il segno indica il verso di marcia.
"velocita_m_s": -2.0,
# Numero di sorgenti equidistanti che si muovono insieme come un gruppo
# rigido (stessa velocità, sigma, flusso ed efficienza).
@@ -98,7 +112,7 @@ SORGENTE = {
# Flusso termico incidente massimo prima della correzione per efficienza.
"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,
# Frequenza di induzione usata per stimare la skin depth se skin_depth_fissa_m è None.
@@ -113,8 +127,13 @@ SORGENTE = {
}
SENSORE = {
# Il sensore si trova sul lato freddo della piastra.
"posizione": "lato_freddo",
# Coordinata x del punto della superficie interna osservato dal sensore [mm].
"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.
# Valore più alto = risposta del sensore più lenta.
+1 -1
View File
@@ -1,4 +1,4 @@
# Database dei materiali per il simulatore termico 1D.
# Database dei materiali per il simulatore termico.
#
# Tutte le unità sono SI:
# - conducibilita_termica_W_mK
+1 -1
View File
@@ -28,7 +28,7 @@ def main() -> None:
plt.plot(df["tempo_s"], df["flusso_termico_sorgente_W_m2"])
plt.xlabel("Tempo [s]")
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.tight_layout()
plt.show()
+1
View File
@@ -1,3 +1,4 @@
numpy
scipy
pandas
matplotlib
+126 -107
View File
@@ -6,8 +6,10 @@ from copy import deepcopy
from pathlib import Path
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
@@ -34,31 +36,28 @@ def _spread_sorgenti_m(sorgente: dict) -> float:
return (numero_sorgenti - 1) * distanza
def _x_riferimento_iniziale_m(sorgente: dict) -> float:
# Posizione a t=0 della sorgente di indice 0 (quella più arretrata nel
# verso di marcia). x_inizio_m è la distanza dal sensore della sorgente
# più avanzata (che quindi lo raggiunge per prima).
def _x_riferimento_iniziale_m(sorgente: dict, x_sensore_m: float) -> float:
# Posizione a t=0 della sorgente di indice 0. Le sorgenti i sono a
# x_i = riferimento + i * distanza, quindi per v >= 0 l'indice 0 è la
# 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)
x_sensore = sorgente["x_sensore_m"]
x_inizio = sorgente["x_inizio_m"]
if sorgente["velocita_m_s"] >= 0:
return (x_sensore - x_inizio) - spread
return (x_sensore + x_inizio) + spread
return (x_sensore_m - 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
# distanza dal sensore della sorgente più arretrata (che quindi lo
# supera per ultima).
x_sensore = sorgente["x_sensore_m"]
# distanza dal sensore della sorgente più arretrata nel verso di marcia
# (quella che lo supera per ultima).
spread = _spread_sorgenti_m(sorgente)
x_fine = sorgente["x_fine_m"]
if sorgente["velocita_m_s"] >= 0:
return x_sensore + x_fine
return x_sensore - x_fine
def x_sorgente_al_tempo(sorgente: dict, t_s: float) -> float:
return _x_riferimento_iniziale_m(sorgente) + sorgente["velocita_m_s"] * t_s
return x_sensore_m + x_fine
return (x_sensore_m - x_fine) - spread
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
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
# 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
# ciascuna sorgente in moto. Più sorgenti equidistanti si muovono insieme
# come un gruppo rigido: condividono velocità, sigma e flusso di picco, e
# sono sfalsate lungo x di un multiplo di "distanza_sorgenti_m". Il
# modello 1D vede solo il flusso lungo la linea che passa per il sensore
# 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)
# Ogni sorgente in moto ha un'impronta gaussiana lungo x, valutata sui
# centri cella della sezione. L'offset circonferenziale y non è risolto
# spazialmente: entra come attenuazione gaussiana del flusso.
x_rif_iniziale = _x_riferimento_iniziale_m(sorgente, x_sensore_m)
x_rif_finale = _x_riferimento_finale_m(sorgente, x_sensore_m)
x_riferimento = x_rif_iniziale + sorgente["velocita_m_s"] * t_s
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"]
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):
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):
continue
dx = x_i - sorgente["x_sensore_m"]
gaussiana = math.exp(-0.5 * (dx * dx + dy * dy) / (sigma * sigma))
q_totale += (
sorgente["flusso_termico_picco_W_m2"]
* sorgente["efficienza_riscaldamento"]
* gaussiana
)
dx = x_centri_m - x_i
q_x += q_picco * attenuazione_y * np.exp(-0.5 * (dx * dx) / (sigma * sigma))
return x_riferimento, q_totale
return x_riferimento, q_x
def riscaldamento_volumetrico_W_m3(
q_superficie_W_m2: float,
def profilo_deposizione_z_1_m(
z_centri_m: np.ndarray,
spessore_m: float,
skin_depth_m: float,
) -> 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.
if q_superficie_W_m2 <= 0.0:
return np.zeros_like(z_centri_m)
# Normalizzato in modo che integrale_0^spessore p(z) dz = 1, così che
# q_vol(x, z) = q(x) * p(z) conservi il flusso superficiale.
delta = max(skin_depth_m, 1e-9)
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(
n: int,
def _laplaciano_1d(n: int) -> sp.spmatrix:
# 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,
dx_m: float,
dz_m: float,
materiale: dict,
h_caldo_W_m2K: float,
h_freddo_W_m2K: float,
) -> np.ndarray:
# Costruisce la matrice A per Eulero implicito:
h_esterno_W_m2K: float,
h_interno_W_m2K: float,
):
# Costruisce e fattorizza (LU sparsa) la matrice A per Eulero implicito 2D:
# 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"]
rho = materiale["densita_kg_m3"]
cp = materiale["calore_specifico_J_kgK"]
alpha = k / (rho * cp)
r = alpha * dt_s / (dz_m * dz_m)
b_caldo = h_caldo_W_m2K * dt_s / (rho * cp * dz_m)
b_freddo = h_freddo_W_m2K * dt_s / (rho * cp * dz_m)
r_x = alpha * dt_s / (dx_m * dx_m)
r_z = alpha * dt_s / (dz_m * 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[0, 0] = 1.0 + r + b_caldo
A[0, 1] = -r
# Celle interne.
for i in range(1, n - 1):
A[i, i - 1] = -r
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
A = (
sp.identity(n)
+ r_x * sp.kron(_laplaciano_1d(n_x), sp.identity(n_z))
+ r_z * sp.kron(sp.identity(n_x), _laplaciano_1d(n_z))
+ sp.diags(convezione)
)
return splu(sp.csc_matrix(A))
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:
piastra = deepcopy(PIASTRA)
fascetta = deepcopy(FASCETTA)
aria = deepcopy(ARIA)
sorgente = deepcopy(SORGENTE)
sensore = deepcopy(SENSORE)
@@ -214,7 +221,7 @@ def configurazione_randomizzata(indice_run: int, rng: random.Random) -> dict:
return {
"id_run": f"run_{indice_run:04d}",
"piastra": piastra,
"fascetta": fascetta,
"aria": aria,
"sorgente": sorgente,
"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:
piastra = cfg_run["piastra"]
fascetta = cfg_run["fascetta"]
aria = cfg_run["aria"]
sorgente = cfg_run["sorgente"]
sensore = cfg_run["sensore"]
nome_materiale = piastra["materiale"]
nome_materiale = fascetta["materiale"]
materiale = MATERIALI[nome_materiale]
spessore = piastra["spessore_mm"] / 1000.0
n = piastra["n_nodi"]
dz = spessore / n
z_centri = (np.arange(n) + 0.5) * dz
lunghezza = fascetta["lunghezza_mm"] / 1000.0
spessore = fascetta["spessore_mm"] / 1000.0
n_x = fascetta["n_nodi_x"]
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"]
durata = SIMULAZIONE["durata_s"]
@@ -244,26 +258,30 @@ def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
else:
skin_depth = float(sorgente["skin_depth_fissa_m"])
A = costruisci_matrice_implicita(
n=n,
solutore = costruisci_solutore_implicito_2d(
n_x=n_x,
n_z=n_z,
dt_s=dt,
dx_m=dx,
dz_m=dz,
materiale=materiale,
h_caldo_W_m2K=aria["h_caldo_W_m2K"],
h_freddo_W_m2K=aria["h_freddo_W_m2K"],
h_esterno_W_m2K=aria["h_esterno_W_m2K"],
h_interno_W_m2K=aria["h_interno_W_m2K"],
)
rho = materiale["densita_kg_m3"]
cp = materiale["calore_specifico_J_kgK"]
b_caldo = aria["h_caldo_W_m2K"] * dt / (rho * cp * dz)
b_freddo = aria["h_freddo_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)
T = np.full(n, piastra["temperatura_iniziale_C"], dtype=float)
T_sensore = T[-1]
profilo_z = profilo_deposizione_z_1_m(z_centri, spessore, skin_depth)
T = np.full((n_x, n_z), fascetta["temperatura_iniziale_C"], dtype=float)
T_sensore = T[i_sensore, -1]
prossimo_campione_t = 0.0
T_vera_max = T[-1]
T_vera_max = T[i_sensore, -1]
T_misurata_max = T_sensore
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
while t <= durata + 1e-12:
x_sorgente, q_superficie = flusso_termico_incidente_W_m2(sorgente, t)
q_vol = riscaldamento_volumetrico_W_m3(
q_superficie_W_m2=q_superficie,
z_centri_m=z_centri,
spessore_m=spessore,
skin_depth_m=skin_depth,
x_sorgente, q_x = profilo_flusso_incidente_W_m2(
sorgente, x_sensore, t, x_centri
)
rhs = T + dt * q_vol / (rho * cp)
rhs[0] += b_caldo * aria["temperatura_ambiente_C"]
rhs[-1] += b_freddo * aria["temperatura_ambiente_C"]
rhs = T + (dt / (rho * cp)) * q_x[:, None] * profilo_z[None, :]
rhs[:, 0] += b_esterno * 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.
T_vera_lato_sensore = T[-1]
# Temperatura vera della superficie interna nel punto osservato
# dal sensore infrarosso.
T_vera_lato_sensore = T[i_sensore, -1]
# Inerzia del sensore del primo ordine.
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"{x_sorgente:.9f}",
f"{sorgente['offset_y_percorso_m']:.9f}",
f"{q_superficie:.6f}",
f"{q_x[i_sensore]:.6f}",
f"{skin_depth:.9e}",
f"{T_vera_lato_sensore:.6f}",
f"{misurata:.6f}",
f"{T[0]:.6f}",
f"{T[i_sensore, 0]:.6f}",
f"{aria['temperatura_ambiente_C']:.6f}",
f"{sorgente['velocita_m_s']:.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"],
"file_csv": str(output_csv.name),
"materiale": nome_materiale,
"diametro_m": fascetta["diametro_mm"] / 1000.0,
"lunghezza_m": lunghezza,
"spessore_m": spessore,
"n_nodi": n,
"n_nodi_x": n_x,
"n_nodi_z": n_z,
"durata_s": durata,
"frequenza_campionamento_hz": SIMULAZIONE["frequenza_campionamento_hz"],
"dt_interno_s": dt,
"temperatura_ambiente_C": aria["temperatura_ambiente_C"],
"h_caldo_W_m2K": aria["h_caldo_W_m2K"],
"h_freddo_W_m2K": aria["h_freddo_W_m2K"],
"h_esterno_W_m2K": aria["h_esterno_W_m2K"],
"h_interno_W_m2K": aria["h_interno_W_m2K"],
"x_inizio_m": sorgente["x_inizio_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"],
"velocita_m_s": sorgente["velocita_m_s"],
"numero_sorgenti": sorgente.get("numero_sorgenti", 1),