From 97ebab844ed78e763f3e58f348ce982d24d1cec7 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Mon, 3 Aug 2026 11:44:58 +0200 Subject: [PATCH] Esporta in CSV la temperatura simulata e la lettura del sensore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nuova azione `python main.py csv`: esporta_csv.py integra il campo FEM e scrive output/csv/run_XXXX.csv con la temperatura vera della parete nel punto osservato, la lettura del sensore reale e il massimo nodale istante per istante, più un metadata.csv con una riga per analisi. La cartella è ricreata a ogni esecuzione. Le righe sono campionate a FEM["frequenza_campionamento_hz"], indipendente dal passo di integrazione, e FEM["num_run"] analisi vengono esportate di seguito: con la randomizzazione abilitata ognuna ha parametri diversi ma riproducibili dallo stesso seed. simula_campo_fem accetta salva_campi=False, perché all'export servono solo le serie scalari e non i campi nodali di ogni campione, e restituisce anche la temperatura nodale massima per campione. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 9 ++- README.md | 38 +++++++++++- config.py | 8 +++ esporta_csv.py | 153 +++++++++++++++++++++++++++++++++++++++++++++++++ fem.py | 14 ++++- main.py | 8 +++ 6 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 esporta_csv.py diff --git a/CLAUDE.md b/CLAUDE.md index 31c8ba4..7a21464 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,9 +21,10 @@ pip install -r requirements.txt # Punto di ingresso unico: `python main.py` elenca le azioni disponibili python main.py mesh # disegna la sola mesh a elementi shell python main.py fem # animazione 3D del campo FEM sulla mesh shell +python main.py csv # esporta output/csv/run_XXXX.csv + metadata.csv ``` -Ogni modulo resta eseguibile anche direttamente (`python plot_mesh.py`, `python plot_animazione_fem.py`). +Ogni modulo resta eseguibile anche direttamente (`python plot_mesh.py`, `python plot_animazione_fem.py`, `python esporta_csv.py`). Attivare sempre il venv (`source .venv/bin/activate`) prima di eseguire qualsiasi comando Python. @@ -37,11 +38,11 @@ Analisi termica transitoria a elementi finiti di una fascetta (anello cilindrico **Flusso dei dati:** -1. `main.py` — dispatcher da riga di comando che seleziona l'azione (mesh, fem) +1. `main.py` — dispatcher da riga di comando che seleziona l'azione (mesh, fem, csv) 2. `config.py` — tutti i parametri configurabili (dizionari FASCETTA, MESH, FEM, ARIA, SORGENTE, SENSORE, RANDOMIZZAZIONE, USCITA) 3. `materials.py` — dizionario MATERIALI con proprietà termofisiche ed elettriche per materiale 4. `mesh.py` — mesh a elementi shell quadrangolari della superficie media del cilindro, visualizzata da `plot_mesh.py` -5. `fem.py` — solutore termico transitorio a elementi finiti sulla mesh shell, animato da `plot_animazione_fem.py` +5. `fem.py` — solutore termico transitorio a elementi finiti sulla mesh shell, animato da `plot_animazione_fem.py` ed esportato in CSV da `esporta_csv.py` **Mesh shell (`mesh.py`):** griglia strutturata `n_elementi_x × n_elementi_circonferenza` di quadrilateri a 4 nodi sulla superficie media (raggio = (diametro − spessore)/2); lo spessore non è discretizzato, è un attributo della shell. La mesh è chiusa lungo la circonferenza (nessun nodo duplicato sulla cucitura) e usa coordinate globali x = asse, y = R·sin(theta), z = R·cos(theta), con theta = 0 sul piano del sensore. @@ -49,6 +50,8 @@ Analisi termica transitoria a elementi finiti di una fascetta (anello cilindrico **Sensore e randomizzazione:** il campo nodale è la temperatura vera della parete; `simula_campo_fem` restituisce anche `T_lette`, la lettura del sensore degradata da inerzia del primo ordine (a ogni passo dt), rumore gaussiano e quantizzazione (a ogni campionamento di frame). `configurazione_randomizzata` perturba velocità, flusso di picco, sigma dello spot, offset y, temperatura ambiente e rumore del sensore con un RNG con seed `FEM["seed"]`, così due analisi restano riproducibili ma diverse; con `RANDOMIZZAZIONE["abilitata"] = False` restituisce i valori nominali di `config.py`. +**Export CSV (`esporta_csv.py`):** `FEM["num_run"]` analisi (una per file `output/csv/run_XXXX.csv`), righe campionate a `FEM["frequenza_campionamento_hz"]` indipendentemente da `FEM["dt_s"]`. Colonne: `id_run, tempo_s, x_sorgente_m, T_vera_sensore_C, T_misurata_sensore_C, T_massima_fascetta_C, T_ambiente_C, offset_y_sorgente_m, velocita_m_s, sigma_punto_m, flusso_picco_W_m2, skin_depth_m, materiale`. `metadata.csv` ha una riga per analisi con tutti i parametri e i picchi. L'export chiama `simula_campo_fem(salva_campi=False)`: usa solo le serie scalari, i campi nodali non vengono accumulati. La cartella `output/csv` è cancellata e ricreata a ogni esecuzione (`shutil.rmtree`). + ## Convenzioni su `config.py` Ogni parametro in [config.py](config.py) ha un commento che spiega solo cos'è (il suo significato fisico/funzionale), mai perché è impostato a un valore specifico. Quando si modifica un parametro per ottenere un certo comportamento (es. una velocità diversa, un range di temperatura target), va cambiato solo il valore: non aggiungere commenti che giustificano o motivano quel valore, perché diventano obsoleti/fuorvianti alla prossima modifica. diff --git a/README.md b/README.md index 79f6940..e4b4907 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ Questo progetto calcola il transitorio termico di una fascetta (anello cilindric sottile) riscaldata da un gruppo di sorgenti a induzione in movimento, osservata da un sensore a infrarossi fisso. La fascetta è discretizzata con una mesh a elementi shell quadrangolari e il campo di temperatura è risolto a elementi finiti, poi visualizzato in -un'animazione 3D isometrica. +un'animazione 3D isometrica o esportato in CSV come serie temporale (temperatura vera +della parete e lettura del sensore). ## Geometria @@ -126,7 +127,9 @@ mesh.py generazione della mesh a elementi shell fem.py solutore termico transitorio a elementi finiti plot_mesh.py disegno della sola mesh plot_animazione_fem.py animazione 3D del campo di temperatura +esporta_csv.py export CSV della serie temporale output/ immagini e GIF salvate quando il backend non è interattivo +output/csv/ CSV esportati (ricreata a ogni esecuzione) ``` ## Installazione @@ -143,10 +146,11 @@ pip install -r requirements.txt python main.py # elenco delle azioni disponibili python main.py mesh # disegna la sola mesh a elementi shell python main.py fem # integra il campo FEM e lo anima in 3D +python main.py csv # esporta la serie temporale in output/csv/ ``` Ogni modulo resta eseguibile anche direttamente (`python plot_mesh.py`, -`python plot_animazione_fem.py`). +`python plot_animazione_fem.py`, `python esporta_csv.py`). Gli script aprono finestre interattive (backend Qt); se il backend non è interattivo (es. sessione senza display) salvano automaticamente PNG/GIF in `output/`. @@ -158,6 +162,34 @@ del sensore reale). Finestra temporale iniziale e cadenza dei fotogrammi si regolano con le costanti in testa a `plot_animazione_fem.py`; la durata simulata è `FEM["durata_s"]`. +## Output CSV + +`python main.py csv` cancella e ricrea `output/csv/`, poi esporta `FEM["num_run"]` analisi +(una per file `run_XXXX.csv`) più un `metadata.csv` con una riga per analisi. Le righe +sono campionate a `FEM["frequenza_campionamento_hz"]`, indipendente dal passo di +integrazione. + +### `output/csv/run_XXXX.csv` — serie temporale + +| Colonna | Significato | +|-------------------------|-----------------------------------------------------------------| +| `id_run` | identificativo dell'analisi | +| `tempo_s` | tempo simulato | +| `x_sorgente_m` | posizione della sorgente di riferimento del gruppo | +| `T_vera_sensore_C` | temperatura vera della parete nel punto osservato (valore nodale FEM) | +| `T_misurata_sensore_C` | lettura del sensore (inerzia + rumore + quantizzazione) | +| `T_massima_fascetta_C` | temperatura nodale massima su tutta la fascetta in quell'istante | +| `T_ambiente_C` | temperatura ambiente dell'analisi | +| `offset_y_sorgente_m`, `velocita_m_s`, `sigma_punto_m`, `flusso_picco_W_m2` | parametri effettivi dell'analisi (variano se la randomizzazione è attiva) | +| `skin_depth_m` | skin depth diagnostica | +| `materiale` | chiave del materiale | + +### `output/csv/metadata.csv` — una riga per analisi + +Tutti i parametri effettivi (geometria, mesh, coefficienti di scambio, sorgenti, sensore) +e i valori di picco: `T_vera_max_sensore_C`, `T_misurata_max_sensore_C`, +`T_massima_fascetta_C`. + ## Configurazione Tutto si modifica in `config.py`. I dizionari principali: @@ -166,7 +198,7 @@ Tutto si modifica in `config.py`. I dizionari principali: |-------------|---------------------------------------------------------------------------| | `FASCETTA` | diametro, lunghezza, spessore, materiale | | `MESH` | numero di elementi shell lungo x e lungo la circonferenza | -| `FEM` | passo temporale, durata dell'analisi transitoria, seed | +| `FEM` | passo temporale, durata, seed, campionamento CSV, numero di analisi | | `ARIA` | temperatura ambiente e coefficienti di convezione (facce e bordi) | | `SORGENTE` | corsa, velocità (il segno dà il verso), gruppo di sorgenti, gaussiana, flusso, frequenza | | `SENSORE` | posizione (x e distanza dalla parete), inerzia, rumore, quantizzazione | diff --git a/config.py b/config.py index 01ccaba..a0f88e2 100644 --- a/config.py +++ b/config.py @@ -56,6 +56,14 @@ FEM = { # Seed per la riproducibilità di randomizzazione e rumore del sensore. "seed": 42, + + # Frequenza di campionamento delle righe del CSV esportato. + # Esempio: 10 Hz significa una riga ogni 0.1 s. + "frequenza_campionamento_hz": 10.0, + + # Numero di analisi da esportare in CSV, una per file. + # Con la randomizzazione abilitata ogni analisi ha parametri diversi. + "num_run": 1, } ARIA = { diff --git a/esporta_csv.py b/esporta_csv.py new file mode 100644 index 0000000..b74c877 --- /dev/null +++ b/esporta_csv.py @@ -0,0 +1,153 @@ +# Export CSV della serie temporale calcolata dal solutore FEM (fem.py). +# +# Per ogni analisi viene scritto un file run_XXXX.csv con la temperatura vera +# della parete nel punto osservato dal sensore e la lettura del sensore reale +# (inerzia, rumore, quantizzazione), più i parametri effettivi dell'analisi. +# `metadata.csv` riassume un'analisi per riga. +# +# Le righe sono campionate a FEM["frequenza_campionamento_hz"], indipendente +# dal passo di integrazione FEM["dt_s"]. I campi nodali non vengono accumulati: +# servono solo le serie scalari. + +import csv +import random +import shutil +from pathlib import Path + +from config import FEM, USCITA +from fem import configurazione_randomizzata, simula_campo_fem +from mesh import genera_mesh, riepilogo_mesh + +INTESTAZIONE = [ + "id_run", + "tempo_s", + "x_sorgente_m", + "T_vera_sensore_C", + "T_misurata_sensore_C", + "T_massima_fascetta_C", + "T_ambiente_C", + "offset_y_sorgente_m", + "velocita_m_s", + "sigma_punto_m", + "flusso_picco_W_m2", + "skin_depth_m", + "materiale", +] + + +def esporta_run( + id_run: str, + percorso_csv: Path, + mesh_dati: dict, + rng: random.Random, +) -> dict: + """Integra un'analisi FEM, ne scrive il CSV e restituisce i suoi metadati.""" + cfg = configurazione_randomizzata(rng) + dati = simula_campo_fem( + dt_frame_s=1.0 / FEM["frequenza_campionamento_hz"], + cfg=cfg, + mesh_dati=mesh_dati, + rng=rng, + salva_campi=False, + ) + + fascetta = cfg["fascetta"] + aria = cfg["aria"] + sorgente = cfg["sorgente"] + sensore = cfg["sensore"] + skin_depth = dati["skin_depth_m"] + + percorso_csv.parent.mkdir(parents=True, exist_ok=True) + with percorso_csv.open("w", newline="") as f: + writer = csv.writer(f) + writer.writerow(INTESTAZIONE) + for k, t in enumerate(dati["tempi"]): + writer.writerow([ + id_run, + f"{t:.6f}", + f"{dati['x_riferimenti'][k]:.9f}", + f"{dati['T_vere'][k]:.6f}", + f"{dati['T_lette'][k]:.6f}", + f"{dati['T_massime'][k]:.6f}", + f"{aria['temperatura_ambiente_C']:.6f}", + f"{sorgente['offset_y_percorso_m']:.9f}", + f"{sorgente['velocita_m_s']:.9f}", + f"{sorgente['sigma_punto_m']:.9f}", + f"{sorgente['flusso_termico_picco_W_m2']:.6f}", + f"{skin_depth:.9e}", + fascetta["materiale"], + ]) + + return { + "id_run": id_run, + "file_csv": percorso_csv.name, + "materiale": fascetta["materiale"], + "diametro_m": fascetta["diametro_mm"] / 1000.0, + "lunghezza_m": fascetta["lunghezza_mm"] / 1000.0, + "spessore_m": fascetta["spessore_mm"] / 1000.0, + "n_elementi_x": mesh_dati["n_elementi_x"], + "n_elementi_circonferenza": mesh_dati["n_elementi_circonferenza"], + "durata_s": FEM["durata_s"], + "dt_s": FEM["dt_s"], + "frequenza_campionamento_hz": FEM["frequenza_campionamento_hz"], + "temperatura_ambiente_C": aria["temperatura_ambiente_C"], + "h_esterno_W_m2K": aria["h_esterno_W_m2K"], + "h_interno_W_m2K": aria["h_interno_W_m2K"], + "h_bordi_W_m2K": aria["h_bordi_W_m2K"], + "x_inizio_m": sorgente["x_inizio_m"], + "x_fine_m": sorgente["x_fine_m"], + "x_sensore_m": dati["stato"]["x_sensore_m"], + "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), + "distanza_sorgenti_m": sorgente.get("distanza_sorgenti_m", 0.0), + "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_sensore_C": float(dati["T_vere"].max()), + "T_misurata_max_sensore_C": float(dati["T_lette"].max()), + "T_massima_fascetta_C": float(dati["T_massime"].max()), + } + + +def main() -> None: + mesh_dati = genera_mesh() + print(riepilogo_mesh(mesh_dati)) + + # La cartella dei CSV è ricreata da zero a ogni esecuzione, così non + # restano run di esecuzioni precedenti con parametri diversi. + cartella = Path(USCITA["cartella"]) / "csv" + if cartella.exists(): + shutil.rmtree(cartella) + cartella.mkdir(parents=True, exist_ok=True) + + rng = random.Random(FEM["seed"]) + righe_metadata = [] + for indice in range(1, FEM["num_run"] + 1): + id_run = f"run_{indice:04d}" + print(f"Integrazione FEM di {id_run} in corso...") + riga = esporta_run(id_run, cartella / f"{id_run}.csv", mesh_dati, rng) + print( + f" T vera max al sensore: {riga['T_vera_max_sensore_C']:.1f} °C — " + f"T misurata max: {riga['T_misurata_max_sensore_C']:.1f} °C" + ) + righe_metadata.append(riga) + + percorso_metadata = cartella / "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"Esportate {len(righe_metadata)} analisi in: {cartella.resolve()}") + print(f"Metadata: {percorso_metadata.resolve()}") + + +if __name__ == "__main__": + main() diff --git a/fem.py b/fem.py index d651e26..8e668f1 100644 --- a/fem.py +++ b/fem.py @@ -387,12 +387,16 @@ def simula_campo_fem( durata_s: float | None = None, mesh_dati: dict | None = None, rng: random.Random | None = None, + salva_campi: bool = True, ) -> dict: - """Integra il campo FEM fino a durata_s, salvando un campo ogni dt_frame_s. + """Integra il campo FEM fino a durata_s, campionando ogni dt_frame_s. Il campo nodale è la temperatura vera della parete; la lettura del sensore ne è la versione degradata: inerzia del primo ordine, rumore gaussiano e quantizzazione, questi ultimi due applicati a ogni campionamento. + + Con `salva_campi = False` i campi nodali non vengono accumulati (utile per + l'export CSV, che usa solo le serie scalari): `campi` resta una lista vuota. """ if rng is None: rng = random.Random(FEM["seed"]) @@ -415,7 +419,7 @@ def simula_campo_fem( tau_sensore = max(sensore["costante_tempo_s"], 1e-9) tempi, campi, x_riferimenti = [], [], [] - T_vere, T_lette = [], [] + T_vere, T_lette, T_massime = [], [], [] prossimo_frame_t = 0.0 t = 0.0 @@ -430,10 +434,12 @@ def simula_campo_fem( letta = quantizza(letta, sensore["quantizzazione_C"]) tempi.append(t) - campi.append(T.copy()) + if salva_campi: + campi.append(T.copy()) x_riferimenti.append(x_rif) T_vere.append(float(T[indice_sensore])) T_lette.append(letta) + T_massime.append(float(T.max())) prossimo_frame_t += dt_frame_s t += dt @@ -446,6 +452,8 @@ def simula_campo_fem( "x_riferimenti": np.array(x_riferimenti), "T_vere": np.array(T_vere), "T_lette": np.array(T_lette), + "T_massime": np.array(T_massime), + "cfg": cfg, "sorgente": sorgente, "T_ambiente_C": stato["T_ambiente_C"], "skin_depth_m": stato["skin_depth_m"], diff --git a/main.py b/main.py index 3dc1f0d..bbbd5e8 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,7 @@ # # python main.py mesh # disegna la sola mesh a elementi shell # python main.py fem # animazione 3D del campo FEM sulla mesh shell +# python main.py csv # esporta in CSV la serie temporale del FEM # # Senza argomenti stampa l'elenco delle azioni disponibili. @@ -22,10 +23,17 @@ def _azione_fem() -> None: plot_animazione_fem.main() +def _azione_csv() -> None: + import esporta_csv + + esporta_csv.main() + + # Chiave da riga di comando -> (funzione, descrizione mostrata nell'help). AZIONI = { "mesh": (_azione_mesh, "Disegna la mesh a elementi shell della fascetta"), "fem": (_azione_fem, "Animazione 3D del campo FEM sulla mesh a elementi shell"), + "csv": (_azione_csv, "Esporta in CSV la temperatura simulata e la lettura del sensore"), }