From 92b7197330af5d677d511ac721477f8d03aa93ba Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Sun, 5 Jul 2026 11:58:52 +0200 Subject: [PATCH] Commit iniziale --- .gitignore | 3 + CLAUDE.md | 61 ++++++++ README.md | 149 ++++++++++++++++++++ config.py | 139 ++++++++++++++++++ materials.py | 39 ++++++ plot_csv.py | 38 +++++ requirements.txt | 3 + simulate.py | 356 +++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 788 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 config.py create mode 100644 materials.py create mode 100644 plot_csv.py create mode 100644 requirements.txt create mode 100644 simulate.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc5ba3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.venv/ +__pycache__/ +dataset/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3fc7467 --- /dev/null +++ b/CLAUDE.md @@ -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). diff --git a/README.md b/README.md new file mode 100644 index 0000000..15fc4a6 --- /dev/null +++ b/README.md @@ -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. diff --git a/config.py b/config.py new file mode 100644 index 0000000..fdcc0d8 --- /dev/null +++ b/config.py @@ -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, +} diff --git a/materials.py b/materials.py new file mode 100644 index 0000000..5d6e3ed --- /dev/null +++ b/materials.py @@ -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, + }, +} diff --git a/plot_csv.py b/plot_csv.py new file mode 100644 index 0000000..cb112b4 --- /dev/null +++ b/plot_csv.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4021870 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +numpy +pandas +matplotlib diff --git a/simulate.py b/simulate.py new file mode 100644 index 0000000..0699c60 --- /dev/null +++ b/simulate.py @@ -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()