Commit iniziale
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
dataset/
|
||||||
@@ -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).
|
||||||
@@ -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.
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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
@@ -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()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
numpy
|
||||||
|
pandas
|
||||||
|
matplotlib
|
||||||
+356
@@ -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()
|
||||||
Reference in New Issue
Block a user