Commit iniziale

This commit is contained in:
2026-07-05 11:58:52 +02:00
commit 92b7197330
8 changed files with 788 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
.venv/
__pycache__/
dataset/
+61
View File
@@ -0,0 +1,61 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Lingua
L'utente comunica in italiano. Tutto il codice, i commenti, la documentazione e i messaggi di commit devono essere scritti in italiano.
## Comandi
Usare sempre il virtual environment:
```bash
# Creare e attivare il venv (prima volta)
python -m venv .venv
source .venv/bin/activate
# Installare le dipendenze
pip install -r requirements.txt
# Generare il dataset (scrive dataset/run_XXXX.csv + dataset/metadata.csv)
python simulate.py
# Visualizzare il primo run
python plot_csv.py
```
Attivare sempre il venv (`source .venv/bin/activate`) prima di eseguire qualsiasi comando Python.
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.
**Flusso dei dati:**
1. `config.py` — tutti i parametri configurabili (dizionari SIMULAZIONE, PIASTRA, 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
**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
- 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à.
**Schema di output** (`dataset/run_XXXX.csv`): serie temporale con colonne `id_run, tempo_s, x_sorgente_m, offset_y_sorgente_m, flusso_termico_sorgente_W_m2, skin_depth_m, T_vera_lato_sensore_C, T_misurata_sensore_C, T_lato_caldo_C, T_ambiente_C, velocita_m_s, sigma_punto_m, flusso_picco_W_m2, materiale`. `metadata.csv` ha una riga per run con tutti i parametri e le temperature di picco.
## 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.
- `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).
+149
View File
@@ -0,0 +1,149 @@
# Simulatore Termico 1D — Induttore Mobile, Sensore Fisso
Questo progetto genera misurazioni CSV pseudo-realistiche per una piastra riscaldata da un induttore in movimento.
## Idea fisica
La geometria è semplificata a 1D attraverso lo spessore della piastra:
```text
lato caldo, z = 0
[sorgente / induttore]
--------------------- piastra
|
| spessore
|
---------------------
[sensore]
lato freddo, z = L
```
L'induttore si muove lungo `x`, mentre il sensore è fisso. Poiché il modello è 1D, il movimento della sorgente è convertito in un flusso termico dipendente dal tempo:
```text
q(t) è massimo quando x_sorgente(t) è allineata con x_sensore
```
Il calore viene depositato attraverso lo spessore con un decadimento esponenziale basato sulla skin depth.
## File
```text
config.py tutti i parametri di simulazione
materials.py proprietà dei materiali
simulate.py genera i file CSV
plot_csv.py utilità di visualizzazione rapida
dataset/ file CSV generati dopo aver eseguito simulate.py
```
## Installazione
```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
## Generare i file CSV
```bash
python simulate.py
```
La cartella di output conterrà:
```text
dataset/run_0001.csv
dataset/run_0002.csv
...
dataset/metadata.csv
```
## Visualizzare un run
```bash
python plot_csv.py
```
## Principali punti di configurazione
Modifica `config.py`.
### Piastra
```python
PIASTRA = {
"spessore_m": 0.005,
"n_nodi": 61,
"temperatura_iniziale_C": 25.0,
"materiale": "acciaio_basso_carbonio",
}
```
### Sorgente mobile
```python
SORGENTE = {
"x_inizio_m": -0.08,
"x_fine_m": 0.08,
"velocita_m_s": 0.004,
"sigma_punto_m": 0.012,
"flusso_termico_picco_W_m2": 70000.0,
"efficienza_riscaldamento": 0.35,
"frequenza_hz": 20000.0,
}
```
### Aria
```python
ARIA = {
"temperatura_ambiente_C": 25.0,
"h_caldo_W_m2K": 12.0,
"h_freddo_W_m2K": 8.0,
}
```
### Sensore
```python
SENSORE = {
"costante_tempo_s": 1.5,
"rumore_std_C": 0.15,
"quantizzazione_C": 0.25,
}
```
## Colonne CSV
Ogni `run_XXXX.csv` contiene:
```text
id_run
tempo_s
x_sorgente_m
offset_y_sorgente_m
flusso_termico_sorgente_W_m2
skin_depth_m
T_vera_lato_sensore_C
T_misurata_sensore_C
T_lato_caldo_C
T_ambiente_C
velocita_m_s
sigma_punto_m
flusso_picco_W_m2
materiale
```
## Limitazione importante
Questo non è una simulazione FEM elettromagnetica + termica completa. È un generatore pratico di dataset.
La sorgente a induzione è approssimata da:
1. un'impronta termica gaussiana in movimento;
2. un flusso termico efficace;
3. deposizione esponenziale del calore attraverso lo spessore tramite skin depth;
4. diffusione del calore 1D attraverso la piastra;
5. perdite per convezione su entrambi i lati;
6. inerzia, rumore e quantizzazione del sensore.
+139
View File
@@ -0,0 +1,139 @@
# Configurazione per il simulatore termico 1D attraverso lo spessore.
#
# 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.
# - Il riscaldamento a induzione è approssimato come riscaldamento volumetrico
# che decade esponenzialmente con la profondità secondo un parametro skin depth.
#
# Unità di misura:
# - lunghezza: m
# - tempo: s
# - temperatura: °C
# - flusso termico: W/m²
# - coefficiente di convezione: W/(m² K)
SIMULAZIONE = {
# Numero di file CSV da generare.
"num_run": 8,
# Tempo simulato totale.
"durata_s": 80.0,
# Frequenza di campionamento CSV.
# Esempio: 2 Hz significa una riga ogni 0.5 s.
"frequenza_campionamento_hz": 2.0,
# Passo di integrazione numerica interna.
# Può essere inferiore al periodo di campionamento CSV.
"dt_interno_s": 0.02,
# Seed per la riproducibilità.
"seed": 42,
# Cartella di output.
"cartella_output": "dataset",
}
PIASTRA = {
# Spessore della piastra.
"spessore_m": 0.005,
# Numero di celle del volume finito attraverso lo spessore.
# Più nodi = maggiore risoluzione spaziale, simulazione più lenta.
"n_nodi": 61,
# Temperatura iniziale uniforme.
"temperatura_iniziale_C": 25.0,
# Deve corrispondere a una chiave in materials.py.
"materiale": "acciaio_basso_carbonio",
}
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 freddo (sensore).
"h_freddo_W_m2K": 8.0,
}
SORGENTE = {
# La sorgente parte da x = x_inizio_m e si muove verso x_fine_m.
"x_inizio_m": -0.08,
"x_fine_m": 0.08,
# 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_y_percorso_m": 0.0,
# Velocità della sorgente lungo x.
"velocita_m_s": 0.004,
# Raggio del punto gaussiano. Valore più alto = impronta termica più larga.
"sigma_punto_m": 0.012,
# Flusso termico incidente massimo prima della correzione per efficienza.
# Parametro di taratura per dati pseudo-realistici.
"flusso_termico_picco_W_m2": 70000.0,
# Frazione del flusso incidente che diventa effettivamente calore nella piastra.
"efficienza_riscaldamento": 0.35,
# Frequenza di induzione usata per stimare la skin depth se skin_depth_fissa_m è None.
"frequenza_hz": 20000.0,
# Override della skin depth. Usare None per calcolarla dalle proprietà elettriche del materiale.
# Esempio: 0.0002 significa 0.2 mm.
"skin_depth_fissa_m": None,
# Annulla il flusso termico una volta che la sorgente ha superato x_fine_m.
"zero_dopo_fine": True,
}
SENSORE = {
# Il sensore si trova sul lato freddo della piastra.
"posizione": "lato_freddo",
# Inerzia del sensore del primo ordine.
# Valore più alto = risposta del sensore più lenta.
"costante_tempo_s": 1.5,
# Deviazione standard del rumore gaussiano.
"rumore_std_C": 0.15,
# Passo di quantizzazione.
# Esempio: 0.25 significa che i valori sono arrotondati a 0.25 °C.
# Usare 0.0 per disabilitare.
"quantizzazione_C": 0.25,
}
RANDOMIZZAZIONE = {
# Se abilitata, ogni run varia leggermente alcuni parametri.
"abilitata": True,
# Deviazioni standard relative.
"velocita_std_rel": 0.08,
"flusso_picco_std_rel": 0.15,
"sigma_punto_std_rel": 0.10,
# Deviazioni standard assolute.
"temperatura_ambiente_std_C": 1.0,
"rumore_sensore_std_rel": 0.25,
# Offset casuale uniforme del percorso della sorgente in y.
# Esempio: +/- 0.012 m significa che l'induttore può passare fino a 12 mm
# dalla linea ideale allineata con il sensore.
"offset_y_max_assoluto_m": 0.012,
}
+39
View File
@@ -0,0 +1,39 @@
# Database dei materiali per il simulatore termico 1D.
#
# Tutte le unità sono SI:
# - conducibilita_termica_W_mK
# - densita_kg_m3
# - calore_specifico_J_kgK
# - resistivita_elettrica_ohm_m
# - permeabilita_relativa
MATERIALI = {
"ferro": {
"conducibilita_termica_W_mK": 80.0,
"densita_kg_m3": 7870.0,
"calore_specifico_J_kgK": 449.0,
"resistivita_elettrica_ohm_m": 9.7e-8,
"permeabilita_relativa": 200.0,
},
"acciaio_basso_carbonio": {
"conducibilita_termica_W_mK": 54.0,
"densita_kg_m3": 7850.0,
"calore_specifico_J_kgK": 470.0,
"resistivita_elettrica_ohm_m": 1.6e-7,
"permeabilita_relativa": 100.0,
},
"acciaio_inossidabile_304": {
"conducibilita_termica_W_mK": 16.2,
"densita_kg_m3": 8000.0,
"calore_specifico_J_kgK": 500.0,
"resistivita_elettrica_ohm_m": 7.2e-7,
"permeabilita_relativa": 1.05,
},
"alluminio": {
"conducibilita_termica_W_mK": 205.0,
"densita_kg_m3": 2700.0,
"calore_specifico_J_kgK": 900.0,
"resistivita_elettrica_ohm_m": 2.82e-8,
"permeabilita_relativa": 1.0,
},
}
+38
View File
@@ -0,0 +1,38 @@
from pathlib import Path
import matplotlib.pyplot as plt
import pandas as pd
def main() -> None:
percorso_csv = Path("dataset/run_0001.csv")
if not percorso_csv.exists():
raise FileNotFoundError(
"File CSV non trovato. Esegui prima `python simulate.py`."
)
df = pd.read_csv(percorso_csv)
plt.figure()
plt.plot(df["tempo_s"], df["T_vera_lato_sensore_C"], label="Temperatura vera lato freddo")
plt.plot(df["tempo_s"], df["T_misurata_sensore_C"], label="Temperatura misurata dal sensore")
plt.xlabel("Tempo [s]")
plt.ylabel("Temperatura [°C]")
plt.title("Sensore fisso, induttore in moto")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
plt.figure()
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.grid(True)
plt.tight_layout()
plt.show()
if __name__ == "__main__":
main()
+3
View File
@@ -0,0 +1,3 @@
numpy
pandas
matplotlib
+356
View File
@@ -0,0 +1,356 @@
import csv
import math
import random
import shutil
from copy import deepcopy
from pathlib import Path
import numpy as np
from config import ARIA, PIASTRA, RANDOMIZZAZIONE, SENSORE, SIMULAZIONE, SORGENTE
from materials import MATERIALI
MU0 = 4.0 * math.pi * 1e-7
def calcola_skin_depth_m(materiale: dict, frequenza_hz: float) -> float:
# Skin depth elettromagnetica approssimata:
# delta = sqrt(2 * rho_e / (omega * mu))
#
# Semplificata. Per acciai ferromagnetici il comportamento reale
# è fortemente non lineare con temperatura e campo magnetico.
rho_e = materiale["resistivita_elettrica_ohm_m"]
mu_r = materiale["permeabilita_relativa"]
omega = 2.0 * math.pi * frequenza_hz
mu = MU0 * mu_r
return math.sqrt(2.0 * rho_e / (omega * mu))
def x_sorgente_al_tempo(sorgente: dict, t_s: float) -> float:
return sorgente["x_inizio_m"] + sorgente["velocita_m_s"] * t_s
def sorgente_attiva(sorgente: dict, x_m: float) -> bool:
if not sorgente.get("zero_dopo_fine", True):
return True
inizio = sorgente["x_inizio_m"]
fine = sorgente["x_fine_m"]
v = sorgente["velocita_m_s"]
if v >= 0:
return inizio <= x_m <= fine
return fine <= x_m <= inizio
def flusso_termico_incidente_W_m2(sorgente: dict, t_s: float) -> tuple[float, float]:
# Restituisce x_sorgente_m e flusso_termico_efficace_W_m2.
#
# Il movimento è rappresentato con un'impronta gaussiana centrata sulla
# sorgente in moto. Il modello 1D vede solo il flusso lungo la linea
# che passa per il sensore fisso.
x = x_sorgente_al_tempo(sorgente, t_s)
if not sorgente_attiva(sorgente, x):
return x, 0.0
dx = x - sorgente["x_sensore_m"]
dy = sorgente["offset_y_percorso_m"]
sigma = sorgente["sigma_punto_m"]
gaussiana = math.exp(-0.5 * (dx * dx + dy * dy) / (sigma * sigma))
q = (
sorgente["flusso_termico_picco_W_m2"]
* sorgente["efficienza_riscaldamento"]
* gaussiana
)
return x, q
def riscaldamento_volumetrico_W_m3(
q_superficie_W_m2: float,
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).
#
# q_vol(z) = A * exp(-z / 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)
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)
def costruisci_matrice_implicita(
n: int,
dt_s: 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:
# A * T_next = rhs
#
# Le celle di bordo 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)
A = np.zeros((n, n), dtype=float)
# 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
def quantizza(valore: float, passo: float) -> float:
if passo <= 0.0:
return valore
return round(valore / passo) * passo
def configurazione_randomizzata(indice_run: int, rng: random.Random) -> dict:
piastra = deepcopy(PIASTRA)
aria = deepcopy(ARIA)
sorgente = deepcopy(SORGENTE)
sensore = deepcopy(SENSORE)
if RANDOMIZZAZIONE.get("abilitata", False):
def perturba_rel(valore: float, std_rel: float, fattore_min: float = 0.1) -> float:
fattore = rng.gauss(1.0, std_rel)
fattore = max(fattore_min, fattore)
return valore * fattore
sorgente["velocita_m_s"] = perturba_rel(
sorgente["velocita_m_s"],
RANDOMIZZAZIONE["velocita_std_rel"],
)
sorgente["flusso_termico_picco_W_m2"] = perturba_rel(
sorgente["flusso_termico_picco_W_m2"],
RANDOMIZZAZIONE["flusso_picco_std_rel"],
)
sorgente["sigma_punto_m"] = perturba_rel(
sorgente["sigma_punto_m"],
RANDOMIZZAZIONE["sigma_punto_std_rel"],
)
sorgente["offset_y_percorso_m"] = rng.uniform(
-RANDOMIZZAZIONE["offset_y_max_assoluto_m"],
RANDOMIZZAZIONE["offset_y_max_assoluto_m"],
)
aria["temperatura_ambiente_C"] += rng.gauss(
0.0,
RANDOMIZZAZIONE["temperatura_ambiente_std_C"],
)
sensore["rumore_std_C"] = perturba_rel(
sensore["rumore_std_C"],
RANDOMIZZAZIONE["rumore_sensore_std_rel"],
fattore_min=0.0,
)
return {
"id_run": f"run_{indice_run:04d}",
"piastra": piastra,
"aria": aria,
"sorgente": sorgente,
"sensore": sensore,
}
def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
piastra = cfg_run["piastra"]
aria = cfg_run["aria"]
sorgente = cfg_run["sorgente"]
sensore = cfg_run["sensore"]
nome_materiale = piastra["materiale"]
materiale = MATERIALI[nome_materiale]
spessore = piastra["spessore_m"]
n = piastra["n_nodi"]
dz = spessore / n
z_centri = (np.arange(n) + 0.5) * dz
dt = SIMULAZIONE["dt_interno_s"]
durata = SIMULAZIONE["durata_s"]
periodo_campionamento = 1.0 / SIMULAZIONE["frequenza_campionamento_hz"]
if sorgente["skin_depth_fissa_m"] is None:
skin_depth = calcola_skin_depth_m(materiale, sorgente["frequenza_hz"])
else:
skin_depth = float(sorgente["skin_depth_fissa_m"])
A = costruisci_matrice_implicita(
n=n,
dt_s=dt,
dz_m=dz,
materiale=materiale,
h_caldo_W_m2K=aria["h_caldo_W_m2K"],
h_freddo_W_m2K=aria["h_freddo_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)
T = np.full(n, piastra["temperatura_iniziale_C"], dtype=float)
T_sensore = T[-1]
prossimo_campione_t = 0.0
T_vera_max = T[-1]
T_misurata_max = T_sensore
output_csv.parent.mkdir(parents=True, exist_ok=True)
with output_csv.open("w", newline="") as f:
writer = csv.writer(f)
writer.writerow([
"id_run",
"tempo_s",
"x_sorgente_m",
"offset_y_sorgente_m",
"flusso_termico_sorgente_W_m2",
"skin_depth_m",
"T_vera_lato_sensore_C",
"T_misurata_sensore_C",
"T_lato_caldo_C",
"T_ambiente_C",
"velocita_m_s",
"sigma_punto_m",
"flusso_picco_W_m2",
"materiale",
])
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,
)
rhs = T + dt * q_vol / (rho * cp)
rhs[0] += b_caldo * aria["temperatura_ambiente_C"]
rhs[-1] += b_freddo * aria["temperatura_ambiente_C"]
T = np.linalg.solve(A, rhs)
# Temperatura vera sul lato freddo, dove si trova il sensore fisso.
T_vera_lato_sensore = T[-1]
# Inerzia del sensore del primo ordine.
tau_sensore = max(sensore["costante_tempo_s"], 1e-9)
T_sensore += (T_vera_lato_sensore - T_sensore) * dt / tau_sensore
# Campionamento CSV.
if t + 1e-12 >= prossimo_campione_t:
misurata = T_sensore + rng.gauss(0.0, sensore["rumore_std_C"])
misurata = quantizza(misurata, sensore["quantizzazione_C"])
T_vera_max = max(T_vera_max, T_vera_lato_sensore)
T_misurata_max = max(T_misurata_max, misurata)
writer.writerow([
cfg_run["id_run"],
f"{t:.6f}",
f"{x_sorgente:.9f}",
f"{sorgente['offset_y_percorso_m']:.9f}",
f"{q_superficie:.6f}",
f"{skin_depth:.9e}",
f"{T_vera_lato_sensore:.6f}",
f"{misurata:.6f}",
f"{T[0]:.6f}",
f"{aria['temperatura_ambiente_C']:.6f}",
f"{sorgente['velocita_m_s']:.9f}",
f"{sorgente['sigma_punto_m']:.9f}",
f"{sorgente['flusso_termico_picco_W_m2']:.6f}",
nome_materiale,
])
prossimo_campione_t += periodo_campionamento
t += dt
return {
"id_run": cfg_run["id_run"],
"file_csv": str(output_csv.name),
"materiale": nome_materiale,
"spessore_m": spessore,
"n_nodi": n,
"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"],
"x_inizio_m": sorgente["x_inizio_m"],
"x_fine_m": sorgente["x_fine_m"],
"x_sensore_m": sorgente["x_sensore_m"],
"offset_y_percorso_m": sorgente["offset_y_percorso_m"],
"velocita_m_s": sorgente["velocita_m_s"],
"sigma_punto_m": sorgente["sigma_punto_m"],
"flusso_termico_picco_W_m2": sorgente["flusso_termico_picco_W_m2"],
"efficienza_riscaldamento": sorgente["efficienza_riscaldamento"],
"frequenza_hz": sorgente["frequenza_hz"],
"skin_depth_m": skin_depth,
"costante_tempo_sensore_s": sensore["costante_tempo_s"],
"rumore_std_sensore_C": sensore["rumore_std_C"],
"quantizzazione_sensore_C": sensore["quantizzazione_C"],
"T_vera_max_lato_sensore_C": T_vera_max,
"T_misurata_max_sensore_C": T_misurata_max,
}
def main() -> None:
cartella_output = Path(SIMULAZIONE["cartella_output"])
if cartella_output.exists():
shutil.rmtree(cartella_output)
cartella_output.mkdir(parents=True, exist_ok=True)
rng = random.Random(SIMULAZIONE["seed"])
righe_metadata = []
for i in range(1, SIMULAZIONE["num_run"] + 1):
cfg_run = configurazione_randomizzata(i, rng)
percorso_csv = cartella_output / f"{cfg_run['id_run']}.csv"
righe_metadata.append(simula_singolo(cfg_run, percorso_csv, rng))
percorso_metadata = cartella_output / "metadata.csv"
with percorso_metadata.open("w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(righe_metadata[0].keys()))
writer.writeheader()
writer.writerows(righe_metadata)
print(f"Generati {len(righe_metadata)} run in: {cartella_output.resolve()}")
print(f"Metadata: {percorso_metadata.resolve()}")
if __name__ == "__main__":
main()