Compare commits
10 Commits
b40ee49528
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 560c6a6a62 | |||
| 288ae81b3e | |||
| 4d83ac4e70 | |||
| a04465a674 | |||
| 4369d2799f | |||
| 7f540ed108 | |||
| c67f6f8da6 | |||
| c92fd26864 | |||
| c4e71a2676 | |||
| bb492c37a6 |
@@ -31,11 +31,13 @@ Non sono configurati test o linter.
|
|||||||
|
|
||||||
## Architettura
|
## Architettura
|
||||||
|
|
||||||
Generatore di dataset per misurazioni termiche pseudo-realistiche di una piastra riscaldata da una sorgente a induzione in movimento.
|
Generatore di dataset per misurazioni termiche pseudo-realistiche di una fascetta (anello cilindrico sottile) riscaldata da sorgenti a induzione in movimento.
|
||||||
|
|
||||||
|
**Geometria:** la fascetta ha diametro, spessore e lunghezza configurabili. Il dominio simulato è la sezione rettangolare lunghezza × spessore, con origine (0, 0) nel vertice in alto a sinistra: x = lunghezza (le sorgenti si muovono in direzione -x sul lato esterno), z = spessore (0 = lato esterno, spessore = lato interno). La coordinata circonferenziale y non è risolta: l'offset y delle sorgenti è collassato in un'attenuazione gaussiana del flusso. Il sensore è un pirometro a infrarossi dentro la fascetta che misura la superficie interna in un punto x fisso.
|
||||||
|
|
||||||
**Flusso dei dati:**
|
**Flusso dei dati:**
|
||||||
|
|
||||||
1. `config.py` — tutti i parametri configurabili (dizionari SIMULAZIONE, PIASTRA, ARIA, SORGENTE, SENSORE, RANDOMIZZAZIONE)
|
1. `config.py` — tutti i parametri configurabili (dizionari SIMULAZIONE, FASCETTA, ARIA, SORGENTE, SENSORE, RANDOMIZZAZIONE)
|
||||||
2. `materials.py` — dizionario MATERIALI con proprietà termofisiche ed elettriche per materiale
|
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`
|
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
|
4. `plot_csv.py` — visualizzazione autonoma per un singolo run
|
||||||
@@ -43,12 +45,15 @@ Generatore di dataset per misurazioni termiche pseudo-realistiche di una piastra
|
|||||||
**Pipeline fisica dentro `simula_singolo()` in [simulate.py](simulate.py):**
|
**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 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`)
|
- Le sorgenti gaussiane in movimento producono un profilo di flusso termico superficiale q(x) sul lato esterno, variabile nel tempo (`profilo_flusso_incidente_W_m2`)
|
||||||
- Quel flusso è ridistribuito volumetricamente attraverso lo spessore con decadimento esponenziale (`riscaldamento_volumetrico_W_m3`)
|
- Quel flusso è ridistribuito volumetricamente attraverso lo spessore con decadimento esponenziale in z (`profilo_deposizione_z_1_m`): q_vol(x, z) = q(x) · p(z)
|
||||||
- Uno schema 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)
|
- Uno schema 2D a volumi finiti con Eulero implicito integra l'equazione del calore su `n_nodi_x × n_nodi_z` celle: `prepara_stato_termico` costruisce griglia, coefficienti e matrice sparsa fattorizzata LU una volta per run (`costruisci_solutore_implicito_2d`, che restituisce l'oggetto `splu`), poi `passo_implicito` avanza il campo risolvendo solo il sistema triangolare
|
||||||
- Le condizioni al contorno di convezione sono incorporate nella matrice
|
- Le condizioni al contorno sono incorporate nella matrice: convezione su tutti e quattro i lati della sezione, più un termine di conduzione circonferenziale (y) verso il resto della fascetta assunto a temperatura ambiente. Il termine è un'equazione di aletta ricavata sull'intero volume del cilindro: il calore conduce lungo y attraverso l'intero spessore mentre le superfici esterna e interna dell'intero cilindro scambiano per convezione, dando q_y = -(h_esterno + h_interno)/spessore · (T - T_amb), senza parametri di conduzione y configurabili a parte
|
||||||
|
- La temperatura iniziale del campo è la temperatura ambiente (randomizzata per run)
|
||||||
- L'output del sensore aggiunge inerzia del primo ordine, rumore gaussiano e quantizzazione
|
- L'output del sensore aggiunge inerzia del primo ordine, rumore gaussiano e quantizzazione
|
||||||
|
|
||||||
|
`prepara_stato_termico` e `passo_implicito` sono condivisi con `plot_animazione.py`, che riproduce la fisica di run_0001 per animare la sezione: ogni modifica alla fisica va fatta lì, non duplicata.
|
||||||
|
|
||||||
**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à.
|
**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.
|
**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.
|
||||||
@@ -59,7 +64,8 @@ Ogni parametro in [config.py](config.py) ha un commento che spiega solo cos'è (
|
|||||||
|
|
||||||
## Vincoli progettuali chiave
|
## 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.
|
- Il modello è 2D nella sezione (x = lunghezza, z = spessore). La coordinata circonferenziale y non è risolta spazialmente — l'offset y del percorso delle sorgenti è collassato in un'attenuazione gaussiana del flusso, e la conduzione lungo y è un termine di scambio lineare verso la temperatura ambiente; il diametro è registrato solo come geometria del setup.
|
||||||
- `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.
|
- Le posizioni di inizio/fine corsa delle sorgenti (`x_inizio_m`, `x_fine_m`) sono distanze dal punto x del sensore lungo il verso di marcia; il segno di `velocita_m_s` determina il verso (negativo = -x).
|
||||||
|
- La matrice implicita è costruita e fattorizzata una volta per run (proprietà del materiale costanti, nessun coefficiente dipendente dalla temperatura). Se si aggiungono proprietà dipendenti dalla temperatura, la matrice deve essere ricostruita e rifattorizzata ad ogni passo temporale.
|
||||||
- `simulate.py` cancella e ricrea l'intera cartella di output ad ogni esecuzione (`shutil.rmtree`).
|
- `simulate.py` cancella e ricrea l'intera cartella di output ad ogni esecuzione (`shutil.rmtree`).
|
||||||
- Aggiungere un nuovo materiale richiede solo una nuova voce nel dizionario `MATERIALI` in [materials.py](materials.py); la chiave del materiale va poi impostata in `PIASTRA["materiale"]` in [config.py](config.py).
|
- Aggiungere un nuovo materiale richiede solo una nuova voce nel dizionario `MATERIALI` in [materials.py](materials.py); la chiave del materiale va poi impostata in `FASCETTA["materiale"]` in [config.py](config.py).
|
||||||
|
|||||||
@@ -1,39 +1,113 @@
|
|||||||
# Simulatore Termico 1D — Induttore Mobile, Sensore Fisso
|
# Simulatore Termico 2D — Fascetta, Sorgenti a Induzione Mobili, Sensore IR Fisso
|
||||||
|
|
||||||
Questo progetto genera misurazioni CSV pseudo-realistiche per una piastra riscaldata da un induttore in movimento.
|
Questo progetto genera misurazioni CSV pseudo-realistiche della temperatura di una
|
||||||
|
fascetta (anello cilindrico sottile) riscaldata da un gruppo di sorgenti a induzione
|
||||||
|
in movimento, osservata da un sensore a infrarossi fisso. Lo scopo è produrre dataset
|
||||||
|
per l'addestramento e la validazione di modelli di stima/regressione termica.
|
||||||
|
|
||||||
## Idea fisica
|
## Geometria
|
||||||
|
|
||||||
La geometria è semplificata a 1D attraverso lo spessore della piastra:
|
La fascetta è un anello cilindrico definito da tre dimensioni:
|
||||||
|
|
||||||
|
- **diametro** (default 70 mm) — il diametro del cilindro;
|
||||||
|
- **spessore** (default 0.12 mm) — lo spessore della parete;
|
||||||
|
- **lunghezza** (default 100 mm) — l'estensione assiale lungo `x`.
|
||||||
|
|
||||||
|
Il dominio simulato è la **sezione rettangolare lunghezza × spessore**. Il sistema di
|
||||||
|
coordinate ha l'origine `(0, 0)` nel vertice in alto a sinistra della sezione:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
lato caldo, z = 0
|
sorgenti (induttori), in moto verso -x
|
||||||
[sorgente / induttore]
|
▼ ▼ ▼
|
||||||
--------------------- piastra
|
(0,0) ─────────────────────────────────────► x
|
||||||
|
|
│ ┌───────────────────────────────────┐ z = 0 lato ESTERNO
|
||||||
| spessore
|
│ │ sezione della fascetta │ (flusso termico)
|
||||||
|
|
│ └───────────────────────────────────┘ z = spessore lato INTERNO
|
||||||
---------------------
|
▼ ┆
|
||||||
[sensore]
|
z ┆ linea di vista
|
||||||
lato freddo, z = L
|
▲
|
||||||
|
sensore IR (fisso, x = 50 mm,
|
||||||
|
a 10 mm dalla parete interna)
|
||||||
```
|
```
|
||||||
|
|
||||||
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:
|
- **x** = direzione della lunghezza, da `0` a `lunghezza`. Le sorgenti viaggiano in
|
||||||
|
direzione `-x` sul lato esterno.
|
||||||
|
- **z** = direzione dello spessore, da `0` (lato esterno, dove arriva il flusso
|
||||||
|
termico) a `spessore` (lato interno, osservato dal sensore).
|
||||||
|
- **y** = coordinata circonferenziale (lungo la circonferenza π·diametro). Non è
|
||||||
|
risolta spazialmente: vedi sotto come viene trattata.
|
||||||
|
|
||||||
```text
|
Il **sensore** è un pirometro a infrarossi posto all'interno della fascetta, a una
|
||||||
q(t) è massimo quando x_sorgente(t) è allineata con x_sensore
|
distanza configurabile dalla parete interna (default 10 mm). Essendo senza contatto,
|
||||||
```
|
la distanza non influenza la misura: il sensore legge la temperatura della superficie
|
||||||
|
interna nel punto `x` configurato (default 50 mm, al centro della lunghezza).
|
||||||
|
|
||||||
Il calore viene depositato attraverso lo spessore con un decadimento esponenziale basato sulla skin depth.
|
## Modello fisico
|
||||||
|
|
||||||
|
Non è una simulazione FEM elettromagnetica + termica completa: è un generatore
|
||||||
|
pratico di dataset. La catena di approssimazioni è la seguente.
|
||||||
|
|
||||||
|
1. **Sorgenti gaussiane in moto** — ogni sorgente ha un'impronta gaussiana di raggio
|
||||||
|
`sigma_punto_m`. Un gruppo di `numero_sorgenti` sorgenti equidistanti
|
||||||
|
(`distanza_sorgenti_m`) si muove rigidamente a velocità costante. Il profilo di
|
||||||
|
flusso sul lato esterno è la somma dei contributi:
|
||||||
|
`q(x, t) = Σᵢ q_picco · efficienza · exp(-((x - xᵢ(t))² + Δy²) / (2σ²))`.
|
||||||
|
L'offset circonferenziale `Δy` tra il percorso delle sorgenti e il punto osservato
|
||||||
|
dal sensore non è risolto spazialmente: entra come attenuazione gaussiana del flusso.
|
||||||
|
|
||||||
|
2. **Skin depth** — il riscaldamento a induzione è approssimato come riscaldamento
|
||||||
|
volumetrico che decade esponenzialmente con la profondità `z`:
|
||||||
|
`q_vol(x, z) = q(x) · exp(-z/δ) / (δ·(1 - exp(-spessore/δ)))`, normalizzato in modo
|
||||||
|
da conservare il flusso superficiale. La skin depth `δ = √(2ρₑ/(ωμ))` è calcolata
|
||||||
|
dalla resistività elettrica e dalla permeabilità del materiale alla frequenza di
|
||||||
|
induzione, oppure può essere imposta con `skin_depth_fissa_m`. Per la banda
|
||||||
|
stagnata a 20 kHz risulta ≈ 0.1 mm, confrontabile con lo spessore: la parete è
|
||||||
|
quasi isoterma attraverso lo spessore.
|
||||||
|
|
||||||
|
3. **Diffusione 2D del calore** — l'equazione del calore è integrata nella sezione
|
||||||
|
`(x, z)` con volumi finiti ed Eulero implicito (incondizionatamente stabile).
|
||||||
|
|
||||||
|
4. **Scambi con l'esterno** — la sezione scambia calore con l'ambiente su tutto il
|
||||||
|
contorno:
|
||||||
|
- convezione sul lato esterno (`h_esterno_W_m2K`), sul lato interno
|
||||||
|
(`h_interno_W_m2K`) e sui due bordi in x (`h_bordi_W_m2K`);
|
||||||
|
- **conduzione circonferenziale**: la sezione cede calore per conduzione lungo `y`
|
||||||
|
al resto della fascetta, assunto a temperatura ambiente. Il termine è
|
||||||
|
un'equazione di aletta ricavata sull'intero volume del cilindro: il calore
|
||||||
|
conduce lungo `y` attraverso l'intero spessore mentre le superfici esterna e
|
||||||
|
interna dell'intero cilindro perdono calore per convezione, dando
|
||||||
|
`q_y = -(h_esterno + h_interno)/spessore · (T - T_amb)` — nessun parametro di
|
||||||
|
conduzione `y` aggiuntivo da configurare.
|
||||||
|
|
||||||
|
5. **Temperatura iniziale** — il campo parte uniformemente alla temperatura ambiente
|
||||||
|
del run (che è randomizzata, quindi varia run per run).
|
||||||
|
|
||||||
|
6. **Sensore realistico** — la lettura aggiunge alla temperatura vera della superficie
|
||||||
|
interna: inerzia del primo ordine (`costante_tempo_s`), rumore gaussiano
|
||||||
|
(`rumore_std_C`) e quantizzazione (`quantizzazione_C`).
|
||||||
|
|
||||||
|
## Metodo numerico
|
||||||
|
|
||||||
|
- Griglia a volumi finiti `n_nodi_x × n_nodi_z` (default 100 × 15); le incognite sono
|
||||||
|
i centri cella.
|
||||||
|
- Eulero implicito con passo `dt_interno_s` (default 0.2 ms), più fine del periodo di
|
||||||
|
campionamento CSV.
|
||||||
|
- Tutti i termini (diffusione, convezione, conduzione circonferenziale) sono lineari e
|
||||||
|
costanti nel run: la matrice sparsa viene costruita e **fattorizzata LU una sola
|
||||||
|
volta per run** (`scipy.sparse.linalg.splu`); ogni passo temporale risolve solo i
|
||||||
|
sistemi triangolari. Un run da 30 s simulati richiede ~20 s di calcolo.
|
||||||
|
- Se in futuro si introducessero proprietà dipendenti dalla temperatura, la matrice
|
||||||
|
andrebbe ricostruita e rifattorizzata a ogni passo.
|
||||||
|
|
||||||
## File
|
## File
|
||||||
|
|
||||||
```text
|
```text
|
||||||
config.py tutti i parametri di simulazione
|
config.py tutti i parametri di simulazione
|
||||||
materials.py proprietà dei materiali
|
materials.py proprietà termofisiche ed elettriche dei materiali
|
||||||
simulate.py genera i file CSV
|
simulate.py motore fisico + generazione dei CSV
|
||||||
plot_csv.py utilità di visualizzazione rapida
|
plot_csv.py grafici rapidi (temperature e flusso) del primo run
|
||||||
dataset/ file CSV generati dopo aver eseguito simulate.py
|
plot_animazione.py animazione della sezione: campo T(x,z), sorgenti, sensore
|
||||||
|
dataset/ output generato da simulate.py (ricreato a ogni esecuzione)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installazione
|
## Installazione
|
||||||
@@ -44,106 +118,99 @@ source .venv/bin/activate
|
|||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
## Generare i file CSV
|
## Uso
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# genera il dataset (ATTENZIONE: cancella e ricrea la cartella dataset/)
|
||||||
python simulate.py
|
python simulate.py
|
||||||
```
|
|
||||||
|
|
||||||
La cartella di output conterrà:
|
# grafici statici del primo run (temperatura e flusso nel tempo)
|
||||||
|
|
||||||
```text
|
|
||||||
dataset/run_0001.csv
|
|
||||||
dataset/run_0002.csv
|
|
||||||
...
|
|
||||||
dataset/metadata.csv
|
|
||||||
```
|
|
||||||
|
|
||||||
## Visualizzare un run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python plot_csv.py
|
python plot_csv.py
|
||||||
|
|
||||||
|
# animazione della sezione durante il passaggio delle sorgenti
|
||||||
|
python plot_animazione.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## Principali punti di configurazione
|
Gli script di visualizzazione aprono finestre interattive (backend Qt); se il backend
|
||||||
|
non è interattivo (es. sessione senza display) salvano automaticamente PNG/GIF in
|
||||||
|
`dataset/`.
|
||||||
|
|
||||||
Modifica `config.py`.
|
L'animazione riproduce esattamente la fisica di `run_0001` (stesso seed) e mostra tre
|
||||||
|
pannelli allineati: il profilo di flusso `q(x)` con le sorgenti in transito, il campo
|
||||||
|
di temperatura nella sezione con il sensore IR, e la temperatura nel punto osservato
|
||||||
|
(vera e con inerzia del sensore). Finestra temporale e cadenza dei fotogrammi si
|
||||||
|
regolano con le costanti in testa a `plot_animazione.py`.
|
||||||
|
|
||||||
### Piastra
|
## Configurazione
|
||||||
|
|
||||||
```python
|
Tutto si modifica in `config.py`. I dizionari principali:
|
||||||
PIASTRA = {
|
|
||||||
"spessore_m": 0.005,
|
|
||||||
"n_nodi": 61,
|
|
||||||
"temperatura_iniziale_C": 25.0,
|
|
||||||
"materiale": "acciaio_basso_carbonio",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sorgente mobile
|
| Dizionario | Contenuto |
|
||||||
|
|------------------|---------------------------------------------------------------------------|
|
||||||
|
| `SIMULAZIONE` | numero di run, durata, campionamento CSV, passo interno, seed, cartella |
|
||||||
|
| `FASCETTA` | diametro, lunghezza, spessore, griglia, conduzione circonferenziale, materiale |
|
||||||
|
| `ARIA` | temperatura ambiente e coefficienti di convezione dei quattro lati |
|
||||||
|
| `SORGENTE` | corsa, velocità (il segno dà il verso), gruppo di sorgenti, gaussiana, flusso, frequenza |
|
||||||
|
| `SENSORE` | posizione (x e distanza dalla parete), inerzia, rumore, quantizzazione |
|
||||||
|
| `RANDOMIZZAZIONE`| entità delle perturbazioni per run |
|
||||||
|
|
||||||
```python
|
Punti da conoscere:
|
||||||
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
|
- **Corsa delle sorgenti**: `x_inizio_m` è la distanza dal sensore, all'inizio della
|
||||||
|
corsa, della sorgente più avanzata (quella che lo raggiunge per prima);
|
||||||
|
`x_fine_m` è la distanza dal sensore, a fine corsa, della sorgente più arretrata
|
||||||
|
(quella che lo supera per ultima). Con `zero_dopo_fine` ogni sorgente si spegne
|
||||||
|
alla fine della propria corsa.
|
||||||
|
- **Materiale**: `FASCETTA["materiale"]` deve essere una chiave di `MATERIALI` in
|
||||||
|
`materials.py`. Per aggiungere un materiale basta una nuova voce nel dizionario
|
||||||
|
(conducibilità, densità, calore specifico, resistività elettrica, permeabilità).
|
||||||
|
- **Proprietà di `banda_stagnata`**: la banda stagnata è un nastro di acciaio a basso
|
||||||
|
tenore di carbonio (0,15–0,25% C) rivestito su entrambe le facce da un sottile
|
||||||
|
strato di stagno elettrolitico, dello spessore di pochi micrometri — trascurabile
|
||||||
|
rispetto allo spessore tipico della parete (es. 0,18 mm) e quindi ininfluente sulle
|
||||||
|
proprietà termiche, elettriche e magnetiche in massa. I valori in `materials.py`
|
||||||
|
sono quindi quelli dell'acciaio dolce sottostante:
|
||||||
|
[Banda stagnata: tutto quello che c'è da sapere (MUNDOLATAS)](https://mundolatas.com/it/banda-stagnata-tutto-quello-che-ce-da-sapere/),
|
||||||
|
[Bande stagnate elettrolitiche (EUROPERF)](https://www.europerf.it/it/banda-stagnata-elettrolitica.php),
|
||||||
|
[Differenza tra banda stagnata e acciaio inossidabile (Wuxi Bright Packing)](https://it.brightmetalcan.com/info/difference-between-tinplate-and-stainless-stee-48700260.html).
|
||||||
|
- **Randomizzazione**: ogni run perturba velocità, flusso di picco, sigma, offset y,
|
||||||
|
temperatura ambiente e rumore del sensore con estrazioni da un RNG a seed fisso
|
||||||
|
(`SIMULAZIONE["seed"]`): il dataset è riproducibile.
|
||||||
|
|
||||||
```python
|
## Output
|
||||||
ARIA = {
|
|
||||||
"temperatura_ambiente_C": 25.0,
|
|
||||||
"h_caldo_W_m2K": 12.0,
|
|
||||||
"h_freddo_W_m2K": 8.0,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sensore
|
### `dataset/run_XXXX.csv` — serie temporale del run
|
||||||
|
|
||||||
```python
|
| Colonna | Significato |
|
||||||
SENSORE = {
|
|--------------------------------|--------------------------------------------------------------------|
|
||||||
"costante_tempo_s": 1.5,
|
| `id_run` | identificativo del run |
|
||||||
"rumore_std_C": 0.15,
|
| `tempo_s` | tempo simulato |
|
||||||
"quantizzazione_C": 0.25,
|
| `x_sorgente_m` | posizione della sorgente di riferimento del gruppo |
|
||||||
}
|
| `offset_y_sorgente_m` | offset circonferenziale del percorso (costante nel run) |
|
||||||
```
|
| `flusso_termico_sorgente_W_m2` | flusso efficace nel punto x del sensore |
|
||||||
|
| `skin_depth_m` | skin depth usata (costante nel run) |
|
||||||
|
| `T_vera_lato_sensore_C` | temperatura vera della superficie interna nel punto del sensore |
|
||||||
|
| `T_misurata_sensore_C` | lettura del sensore (inerzia + rumore + quantizzazione) |
|
||||||
|
| `T_lato_caldo_C` | temperatura della superficie esterna nello stesso punto x |
|
||||||
|
| `T_ambiente_C` | temperatura ambiente del run |
|
||||||
|
| `velocita_m_s`, `sigma_punto_m`, `flusso_picco_W_m2` | parametri randomizzati del run |
|
||||||
|
| `materiale` | chiave del materiale |
|
||||||
|
|
||||||
## Colonne CSV
|
### `dataset/metadata.csv` — una riga per run
|
||||||
|
|
||||||
Ogni `run_XXXX.csv` contiene:
|
Contiene tutti i parametri effettivi del run (geometria, griglia, coefficienti di
|
||||||
|
scambio, parametri delle sorgenti e del sensore, valori randomizzati) e le temperature
|
||||||
|
di picco vera e misurata: utile come ground truth e per filtrare i run.
|
||||||
|
|
||||||
```text
|
## Limitazioni
|
||||||
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
|
1. Il campo elettromagnetico non è simulato: l'accoppiamento induttivo è ridotto a
|
||||||
|
impronta gaussiana × efficienza × decadimento esponenziale in z.
|
||||||
Questo non è una simulazione FEM elettromagnetica + termica completa. È un generatore pratico di dataset.
|
2. La coordinata circonferenziale y non è risolta: offset del percorso e conduzione
|
||||||
|
verso il resto della fascetta sono modelli collassati (attenuazione gaussiana e
|
||||||
La sorgente a induzione è approssimata da:
|
scambio lineare verso T ambiente).
|
||||||
|
3. Le proprietà dei materiali sono costanti con la temperatura; per gli acciai
|
||||||
1. un'impronta termica gaussiana in movimento;
|
ferromagnetici la skin depth reale varia fortemente con temperatura e campo
|
||||||
2. un flusso termico efficace;
|
(punto di Curie non modellato).
|
||||||
3. deposizione esponenziale del calore attraverso lo spessore tramite skin depth;
|
4. Irraggiamento non modellato: a ~220 °C le perdite radiative non sono del tutto
|
||||||
4. diffusione del calore 1D attraverso la piastra;
|
trascurabili rispetto alla convezione.
|
||||||
5. perdite per convezione su entrambi i lati;
|
|
||||||
6. inerzia, rumore e quantizzazione del sensore.
|
|
||||||
|
|||||||
@@ -1,17 +1,33 @@
|
|||||||
# Configurazione per il simulatore termico 1D attraverso lo spessore.
|
# Configurazione per il simulatore termico 2D della sezione di una fascetta.
|
||||||
#
|
#
|
||||||
# Modello fisico:
|
# Geometria e modello fisico:
|
||||||
# - La piastra è ridotta a una dimensione: z = direzione dello spessore.
|
# - La fascetta è un anello cilindrico con diametro "diametro_mm", spessore
|
||||||
# - La sorgente di calore si trova sul lato caldo, z = 0.
|
# "spessore_mm" e lunghezza "lunghezza_mm".
|
||||||
# - Il sensore si trova sul lato opposto, z = spessore.
|
# - Il dominio simulato è la sezione rettangolare lunghezza × spessore.
|
||||||
# - L'induttore si muove lungo x. Poiché il modello è 1D, il movimento è
|
# - Sistema di coordinate: origine (0, 0) nel vertice in alto a sinistra
|
||||||
# rappresentato come un flusso termico variabile nel tempo nel punto
|
# della sezione. x = direzione della lunghezza (da 0 a lunghezza),
|
||||||
# allineato con il sensore fisso.
|
# z = direzione dello spessore (0 = lato esterno, dove agiscono le
|
||||||
|
# sorgenti; spessore = lato interno, osservato dal sensore).
|
||||||
|
# - y è la coordinata circonferenziale: non è risolta spazialmente, l'offset
|
||||||
|
# y del percorso delle sorgenti è collassato in un'attenuazione gaussiana
|
||||||
|
# del flusso.
|
||||||
|
# - Le sorgenti a induzione si muovono in direzione -x sul lato esterno.
|
||||||
# - Il riscaldamento a induzione è approssimato come riscaldamento volumetrico
|
# - Il riscaldamento a induzione è approssimato come riscaldamento volumetrico
|
||||||
# che decade esponenzialmente con la profondità secondo un parametro skin depth.
|
# che decade esponenzialmente con la profondità z secondo la skin depth.
|
||||||
|
# - La sezione scambia per convezione con l'aria su tutti e quattro i lati
|
||||||
|
# (esterno, interno e i due bordi in x). Scambia inoltre per conduzione
|
||||||
|
# lungo y con il resto della fascetta, assunto a temperatura ambiente:
|
||||||
|
# il calore conduce attraverso l'intero volume dello spessore mentre le
|
||||||
|
# superfici esterna e interna dell'intero cilindro perdono calore per
|
||||||
|
# convezione (equazione dell'aletta), derivato da h_esterno, h_interno e
|
||||||
|
# spessore_mm senza parametri di conduzione y aggiuntivi.
|
||||||
|
# - La temperatura iniziale della fascetta è pari alla temperatura ambiente.
|
||||||
|
# - Il sensore è un pirometro a infrarossi posto all'interno della fascetta,
|
||||||
|
# a distanza "distanza_parete_mm" dalla parete interna: misura senza
|
||||||
|
# contatto la temperatura della superficie interna nel punto x = "x_mm".
|
||||||
#
|
#
|
||||||
# Unità di misura:
|
# Unità di misura:
|
||||||
# - lunghezza: m
|
# - lunghezza: m (mm dove indicato dal suffisso)
|
||||||
# - tempo: s
|
# - tempo: s
|
||||||
# - temperatura: °C
|
# - temperatura: °C
|
||||||
# - flusso termico: W/m²
|
# - flusso termico: W/m²
|
||||||
@@ -19,7 +35,7 @@
|
|||||||
|
|
||||||
SIMULAZIONE = {
|
SIMULAZIONE = {
|
||||||
# Numero di file CSV da generare.
|
# Numero di file CSV da generare.
|
||||||
"num_run": 2,
|
"num_run": 1,
|
||||||
|
|
||||||
# Tempo simulato totale.
|
# Tempo simulato totale.
|
||||||
"durata_s": 30.0,
|
"durata_s": 30.0,
|
||||||
@@ -37,18 +53,28 @@ SIMULAZIONE = {
|
|||||||
|
|
||||||
# Cartella di output.
|
# Cartella di output.
|
||||||
"cartella_output": "dataset",
|
"cartella_output": "dataset",
|
||||||
|
|
||||||
|
# Numero di processi paralleli per la generazione dei run.
|
||||||
|
# None = usa tutti i core disponibili.
|
||||||
|
"num_processi": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
PIASTRA = {
|
FASCETTA = {
|
||||||
# Spessore della piastra [mm].
|
# Diametro della fascetta [mm].
|
||||||
"spessore_mm": 0.12,
|
"diametro_mm": 70.0,
|
||||||
|
|
||||||
# Numero di celle del volume finito attraverso lo spessore.
|
# Lunghezza della fascetta lungo x [mm].
|
||||||
|
"lunghezza_mm": 100.0,
|
||||||
|
|
||||||
|
# Spessore della parete [mm].
|
||||||
|
"spessore_mm": 0.18,
|
||||||
|
|
||||||
|
# Numero di celle del volume finito lungo x (lunghezza).
|
||||||
|
"n_nodi_x": 100,
|
||||||
|
|
||||||
|
# Numero di celle del volume finito lungo z (spessore).
|
||||||
# Più nodi = maggiore risoluzione spaziale, simulazione più lenta.
|
# Più nodi = maggiore risoluzione spaziale, simulazione più lenta.
|
||||||
"n_nodi": 61,
|
"n_nodi_z": 15,
|
||||||
|
|
||||||
# Temperatura iniziale uniforme.
|
|
||||||
"temperatura_iniziale_C": 25.0,
|
|
||||||
|
|
||||||
# Deve corrispondere a una chiave in materials.py.
|
# Deve corrispondere a una chiave in materials.py.
|
||||||
"materiale": "banda_stagnata",
|
"materiale": "banda_stagnata",
|
||||||
@@ -58,47 +84,47 @@ ARIA = {
|
|||||||
# Temperatura dell'aria ambiente.
|
# Temperatura dell'aria ambiente.
|
||||||
"temperatura_ambiente_C": 25.0,
|
"temperatura_ambiente_C": 25.0,
|
||||||
|
|
||||||
# Coefficiente di convezione sul lato caldo (sorgente).
|
# Coefficiente di convezione sul lato esterno (z = 0, lato sorgenti).
|
||||||
"h_caldo_W_m2K": 12.0,
|
"h_esterno_W_m2K": 12.0,
|
||||||
|
|
||||||
# Coefficiente di convezione sul lato freddo (sensore).
|
# Coefficiente di convezione sul lato interno (z = spessore, lato sensore).
|
||||||
"h_freddo_W_m2K": 8.0,
|
"h_interno_W_m2K": 8.0,
|
||||||
|
|
||||||
|
# Coefficiente di convezione sui bordi laterali (x = 0 e x = lunghezza).
|
||||||
|
"h_bordi_W_m2K": 10.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
SORGENTE = {
|
SORGENTE = {
|
||||||
# Distanza dal sensore, all'inizio della corsa, della sorgente del
|
# Distanza dal sensore, all'inizio della corsa, della sorgente del
|
||||||
# gruppo più vicina al sensore (quella che lo raggiunge per prima).
|
# gruppo più vicina al sensore (quella che lo raggiunge per prima).
|
||||||
"x_inizio_m": 1.0,
|
"x_inizio_m": 5.0,
|
||||||
|
|
||||||
# Distanza dal sensore, alla fine della corsa, della sorgente del
|
# Distanza dal sensore, alla fine della corsa, della sorgente del
|
||||||
# gruppo più lontana dal sensore (quella che lo supera per ultima).
|
# gruppo più lontana dal sensore (quella che lo supera per ultima).
|
||||||
"x_fine_m": 5.0,
|
"x_fine_m": 5.0,
|
||||||
|
|
||||||
# Coordinata laterale fissa della proiezione del sensore sul lato caldo.
|
# Offset circonferenziale (y) tra il percorso delle sorgenti e il punto
|
||||||
# L'effetto della sorgente è massimo quando x_sorgente == x_sensore e offset_y_m == 0.
|
# osservato dal sensore. Se diverso da zero, la sorgente passa a lato,
|
||||||
"x_sensore_m": 0.0,
|
# riducendo il picco.
|
||||||
|
|
||||||
# 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,
|
"offset_y_percorso_m": 0.0,
|
||||||
|
|
||||||
# Velocità della sorgente lungo x.
|
# Velocità delle sorgenti lungo x. Il segno indica il verso di marcia.
|
||||||
"velocita_m_s": 2.0,
|
"velocita_m_s": -1.0,
|
||||||
|
|
||||||
# Numero di sorgenti equidistanti che si muovono insieme come un gruppo
|
# Numero di sorgenti equidistanti che si muovono insieme come un gruppo
|
||||||
# rigido (stessa velocità, sigma, flusso ed efficienza).
|
# rigido (stessa velocità, sigma, flusso ed efficienza).
|
||||||
"numero_sorgenti": 3,
|
"numero_sorgenti": 3,
|
||||||
|
|
||||||
# Distanza lungo x tra sorgenti consecutive del gruppo.
|
# Distanza lungo x tra sorgenti consecutive del gruppo.
|
||||||
"distanza_sorgenti_m": 0.5,
|
"distanza_sorgenti_m": 1.0,
|
||||||
|
|
||||||
# Raggio del punto gaussiano. Valore più alto = impronta termica più larga.
|
# Raggio del punto gaussiano. Valore più alto = impronta termica più larga.
|
||||||
"sigma_punto_m": 0.012,
|
"sigma_punto_m": 0.012,
|
||||||
|
|
||||||
# Flusso termico incidente massimo prima della correzione per efficienza.
|
# Flusso termico incidente massimo prima della correzione per efficienza.
|
||||||
"flusso_termico_picco_W_m2": 5500000.0,
|
"flusso_termico_picco_W_m2": 4434000.0,
|
||||||
|
|
||||||
# Frazione del flusso incidente che diventa effettivamente calore nella piastra.
|
# Frazione del flusso incidente che diventa effettivamente calore nella fascetta.
|
||||||
"efficienza_riscaldamento": 0.35,
|
"efficienza_riscaldamento": 0.35,
|
||||||
|
|
||||||
# Frequenza di induzione usata per stimare la skin depth se skin_depth_fissa_m è None.
|
# Frequenza di induzione usata per stimare la skin depth se skin_depth_fissa_m è None.
|
||||||
@@ -113,12 +139,17 @@ SORGENTE = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SENSORE = {
|
SENSORE = {
|
||||||
# Il sensore si trova sul lato freddo della piastra.
|
# Coordinata x del punto della superficie interna osservato dal sensore [mm].
|
||||||
"posizione": "lato_freddo",
|
"x_mm": 50.0,
|
||||||
|
|
||||||
|
# Distanza del sensore dalla parete interna lungo z [mm].
|
||||||
|
# Il sensore è a infrarossi: la distanza non influenza la misura,
|
||||||
|
# è registrata solo come geometria del setup.
|
||||||
|
"distanza_parete_mm": 10.0,
|
||||||
|
|
||||||
# Inerzia del sensore del primo ordine.
|
# Inerzia del sensore del primo ordine.
|
||||||
# Valore più alto = risposta del sensore più lenta.
|
# Valore più alto = risposta del sensore più lenta.
|
||||||
"costante_tempo_s": 0.05,
|
"costante_tempo_s": 0.01,
|
||||||
|
|
||||||
# Deviazione standard del rumore gaussiano.
|
# Deviazione standard del rumore gaussiano.
|
||||||
"rumore_std_C": 0.15,
|
"rumore_std_C": 0.15,
|
||||||
@@ -131,7 +162,7 @@ SENSORE = {
|
|||||||
|
|
||||||
RANDOMIZZAZIONE = {
|
RANDOMIZZAZIONE = {
|
||||||
# Se abilitata, ogni run varia leggermente alcuni parametri.
|
# Se abilitata, ogni run varia leggermente alcuni parametri.
|
||||||
"abilitata": True,
|
"abilitata": False,
|
||||||
|
|
||||||
# Deviazioni standard relative.
|
# Deviazioni standard relative.
|
||||||
"velocita_std_rel": 0.007,
|
"velocita_std_rel": 0.007,
|
||||||
|
|||||||
+6
-6
@@ -1,4 +1,4 @@
|
|||||||
# Database dei materiali per il simulatore termico 1D.
|
# Database dei materiali per il simulatore termico.
|
||||||
#
|
#
|
||||||
# Tutte le unità sono SI:
|
# Tutte le unità sono SI:
|
||||||
# - conducibilita_termica_W_mK
|
# - conducibilita_termica_W_mK
|
||||||
@@ -37,10 +37,10 @@ MATERIALI = {
|
|||||||
"permeabilita_relativa": 1.0,
|
"permeabilita_relativa": 1.0,
|
||||||
},
|
},
|
||||||
"banda_stagnata": {
|
"banda_stagnata": {
|
||||||
"conducibilita_termica_W_mK": 60.0,
|
"conducibilita_termica_W_mK": 54.0,
|
||||||
"densita_kg_m3": 7860.0,
|
"densita_kg_m3": 7850.0,
|
||||||
"calore_specifico_J_kgK": 460.0,
|
"calore_specifico_J_kgK": 470.0,
|
||||||
"resistivita_elettrica_ohm_m": 1.2e-7,
|
"resistivita_elettrica_ohm_m": 1.6e-7,
|
||||||
"permeabilita_relativa": 150.0,
|
"permeabilita_relativa": 100.0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
# Animazione della sezione della fascetta durante il passaggio delle sorgenti.
|
||||||
|
#
|
||||||
|
# Riproduce la fisica di run_0001 (stesso seed di simulate.py) e mostra:
|
||||||
|
# - il profilo di flusso termico q(x) sul lato esterno e le sorgenti in moto;
|
||||||
|
# - il campo di temperatura T(x, z) nella sezione lunghezza × spessore;
|
||||||
|
# - il sensore infrarosso e la temperatura nel punto osservato.
|
||||||
|
|
||||||
|
import random
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
from matplotlib.animation import FuncAnimation, PillowWriter
|
||||||
|
|
||||||
|
from config import SIMULAZIONE
|
||||||
|
from simulate import (
|
||||||
|
configurazione_randomizzata,
|
||||||
|
passo_implicito,
|
||||||
|
prepara_stato_termico,
|
||||||
|
profilo_flusso_incidente_W_m2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Istante di inizio dei fotogrammi mostrati (la simulazione parte comunque da 0).
|
||||||
|
T_INIZIO_ANIMAZIONE_S = 0.40
|
||||||
|
|
||||||
|
# Istante di fine dell'animazione.
|
||||||
|
T_FINE_ANIMAZIONE_S = 30
|
||||||
|
|
||||||
|
# Tempo simulato tra un fotogramma e il successivo.
|
||||||
|
DT_FRAME_S = 0.05
|
||||||
|
|
||||||
|
# Millisecondi tra i fotogrammi in riproduzione.
|
||||||
|
INTERVALLO_RIPRODUZIONE_MS = 30
|
||||||
|
|
||||||
|
|
||||||
|
def simula_campi(cfg_run: dict) -> dict:
|
||||||
|
# Esegue la simulazione fino a T_FINE_ANIMAZIONE_S salvando, a ogni
|
||||||
|
# fotogramma, campo di temperatura, profilo di flusso e stato del sensore.
|
||||||
|
fascetta = cfg_run["fascetta"]
|
||||||
|
aria = cfg_run["aria"]
|
||||||
|
sorgente = cfg_run["sorgente"]
|
||||||
|
sensore = cfg_run["sensore"]
|
||||||
|
|
||||||
|
stato = prepara_stato_termico(fascetta, aria, sorgente)
|
||||||
|
n_x = stato["n_x"]
|
||||||
|
n_z = stato["n_z"]
|
||||||
|
x_centri = stato["x_centri_m"]
|
||||||
|
dt = stato["dt_s"]
|
||||||
|
|
||||||
|
x_sensore = sensore["x_mm"] / 1000.0
|
||||||
|
i_sensore = min(n_x - 1, max(0, int(x_sensore / stato["dx_m"])))
|
||||||
|
|
||||||
|
T = np.full((n_x, n_z), aria["temperatura_ambiente_C"], dtype=float)
|
||||||
|
T_sensore = T[i_sensore, -1]
|
||||||
|
tau_sensore = max(sensore["costante_tempo_s"], 1e-9)
|
||||||
|
|
||||||
|
tempi, campi, flussi, x_riferimenti = [], [], [], []
|
||||||
|
T_vere, T_lette = [], []
|
||||||
|
|
||||||
|
prossimo_frame_t = 0.0
|
||||||
|
t = 0.0
|
||||||
|
while t <= T_FINE_ANIMAZIONE_S + 1e-12:
|
||||||
|
x_rif, q_x = profilo_flusso_incidente_W_m2(sorgente, x_sensore, t, x_centri)
|
||||||
|
T = passo_implicito(stato, T, q_x)
|
||||||
|
|
||||||
|
T_sensore += (T[i_sensore, -1] - T_sensore) * dt / tau_sensore
|
||||||
|
|
||||||
|
if t + 1e-12 >= prossimo_frame_t:
|
||||||
|
tempi.append(t)
|
||||||
|
campi.append(T.copy())
|
||||||
|
flussi.append(q_x.copy())
|
||||||
|
x_riferimenti.append(x_rif)
|
||||||
|
T_vere.append(T[i_sensore, -1])
|
||||||
|
T_lette.append(T_sensore)
|
||||||
|
prossimo_frame_t += DT_FRAME_S
|
||||||
|
|
||||||
|
t += dt
|
||||||
|
|
||||||
|
return {
|
||||||
|
"tempi": np.array(tempi),
|
||||||
|
"campi": campi,
|
||||||
|
"flussi": flussi,
|
||||||
|
"x_riferimenti": np.array(x_riferimenti),
|
||||||
|
"T_vere": np.array(T_vere),
|
||||||
|
"T_lette": np.array(T_lette),
|
||||||
|
"x_centri_mm": x_centri * 1000.0,
|
||||||
|
"spessore_mm": fascetta["spessore_mm"],
|
||||||
|
"lunghezza_mm": fascetta["lunghezza_mm"],
|
||||||
|
"x_sensore_mm": sensore["x_mm"],
|
||||||
|
"sorgente": cfg_run["sorgente"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
rng = random.Random(SIMULAZIONE["seed"])
|
||||||
|
cfg_run = configurazione_randomizzata(1, rng)
|
||||||
|
dati = simula_campi(cfg_run)
|
||||||
|
|
||||||
|
tempi = dati["tempi"]
|
||||||
|
indice_inizio = int(np.searchsorted(tempi, T_INIZIO_ANIMAZIONE_S))
|
||||||
|
n_frame = len(tempi) - indice_inizio
|
||||||
|
|
||||||
|
lunghezza_mm = dati["lunghezza_mm"]
|
||||||
|
spessore_mm = dati["spessore_mm"]
|
||||||
|
x_vista_mm = (-10.0, lunghezza_mm + 10.0)
|
||||||
|
|
||||||
|
q_max_MW = max(q.max() for q in dati["flussi"]) / 1e6
|
||||||
|
T_max = max(c.max() for c in dati["campi"])
|
||||||
|
|
||||||
|
sorgente = dati["sorgente"]
|
||||||
|
numero_sorgenti = sorgente.get("numero_sorgenti", 1)
|
||||||
|
distanza_mm = sorgente.get("distanza_sorgenti_m", 0.0) * 1000.0
|
||||||
|
|
||||||
|
fig, (ax_flusso, ax_sezione, ax_storia) = plt.subplots(
|
||||||
|
3, 1, figsize=(10, 8), height_ratios=[1.0, 1.6, 1.2],
|
||||||
|
gridspec_kw={"hspace": 0.45},
|
||||||
|
)
|
||||||
|
fig.suptitle("Sezione della fascetta: sorgenti in transito e sensore")
|
||||||
|
|
||||||
|
# Pannello 1: profilo di flusso sul lato esterno e posizioni delle sorgenti.
|
||||||
|
linea_flusso, = ax_flusso.plot([], [], color="tab:red")
|
||||||
|
marker_sorgenti, = ax_flusso.plot(
|
||||||
|
[], [], "v", color="tab:red", markersize=10, clip_on=False
|
||||||
|
)
|
||||||
|
ax_flusso.annotate(
|
||||||
|
"verso di marcia",
|
||||||
|
xy=(0.28, 0.85), xytext=(0.55, 0.85), xycoords="axes fraction",
|
||||||
|
arrowprops={"arrowstyle": "->", "color": "gray"},
|
||||||
|
color="gray", va="center",
|
||||||
|
)
|
||||||
|
ax_flusso.set_xlim(*x_vista_mm)
|
||||||
|
ax_flusso.set_ylim(0.0, q_max_MW * 1.25)
|
||||||
|
ax_flusso.set_ylabel("q(x) [MW/m²]")
|
||||||
|
ax_flusso.grid(True, alpha=0.3)
|
||||||
|
ax_flusso.set_xticklabels([])
|
||||||
|
|
||||||
|
# Pannello 2: campo di temperatura nella sezione (z verso il basso,
|
||||||
|
# origine nel vertice in alto a sinistra come nel modello).
|
||||||
|
immagine = ax_sezione.imshow(
|
||||||
|
dati["campi"][indice_inizio].T,
|
||||||
|
extent=(0.0, lunghezza_mm, spessore_mm, 0.0),
|
||||||
|
aspect="auto",
|
||||||
|
cmap="inferno",
|
||||||
|
vmin=cfg_run["aria"]["temperatura_ambiente_C"],
|
||||||
|
vmax=T_max,
|
||||||
|
interpolation="bilinear",
|
||||||
|
)
|
||||||
|
ax_sezione.set_xlim(*x_vista_mm)
|
||||||
|
ax_sezione.set_ylim(3.2 * spessore_mm, -0.6 * spessore_mm)
|
||||||
|
ax_sezione.set_ylabel("z [mm]")
|
||||||
|
ax_sezione.set_xlabel("x [mm]")
|
||||||
|
# Sensore infrarosso sotto la parete interna (posizione schematica,
|
||||||
|
# non in scala) con linea di vista tratteggiata.
|
||||||
|
x_sens = dati["x_sensore_mm"]
|
||||||
|
ax_sezione.plot([x_sens], [2.4 * spessore_mm], "^", color="tab:blue", markersize=12)
|
||||||
|
ax_sezione.plot(
|
||||||
|
[x_sens, x_sens], [1.1 * spessore_mm, 2.1 * spessore_mm],
|
||||||
|
linestyle="--", color="tab:blue", linewidth=1,
|
||||||
|
)
|
||||||
|
ax_sezione.text(
|
||||||
|
x_sens + 3, 2.4 * spessore_mm, "sensore IR",
|
||||||
|
color="tab:blue", va="center",
|
||||||
|
)
|
||||||
|
# La colorbar è agganciata a tutti i pannelli per non restringere solo
|
||||||
|
# quello della sezione, mantenendo allineati gli assi x.
|
||||||
|
barra = fig.colorbar(
|
||||||
|
immagine, ax=(ax_flusso, ax_sezione, ax_storia), pad=0.02, aspect=35
|
||||||
|
)
|
||||||
|
barra.set_label("T [°C]")
|
||||||
|
|
||||||
|
# Pannello 3: temperatura nel punto osservato dal sensore.
|
||||||
|
linea_vera, = ax_storia.plot([], [], label="T vera lato interno")
|
||||||
|
linea_letta, = ax_storia.plot([], [], label="T sensore (con inerzia)")
|
||||||
|
cursore = ax_storia.axvline(tempi[indice_inizio], color="gray", linewidth=0.8)
|
||||||
|
ax_storia.set_xlim(0.0, T_FINE_ANIMAZIONE_S)
|
||||||
|
ax_storia.set_ylim(15.0, max(dati["T_vere"].max(), dati["T_lette"].max()) * 1.08)
|
||||||
|
ax_storia.set_xlabel("Tempo [s]")
|
||||||
|
ax_storia.set_ylabel("T [°C]")
|
||||||
|
ax_storia.legend(loc="upper left")
|
||||||
|
ax_storia.grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
testo_tempo = ax_flusso.set_title(f"t = {tempi[indice_inizio]:.3f} s", loc="right")
|
||||||
|
|
||||||
|
def aggiorna(frame: int):
|
||||||
|
k = indice_inizio + frame
|
||||||
|
t = tempi[k]
|
||||||
|
|
||||||
|
linea_flusso.set_data(dati["x_centri_mm"], dati["flussi"][k] / 1e6)
|
||||||
|
|
||||||
|
x_sorgenti_mm = (
|
||||||
|
dati["x_riferimenti"][k] * 1000.0
|
||||||
|
+ np.arange(numero_sorgenti) * distanza_mm
|
||||||
|
)
|
||||||
|
visibili = (x_sorgenti_mm >= x_vista_mm[0]) & (x_sorgenti_mm <= x_vista_mm[1])
|
||||||
|
marker_sorgenti.set_data(
|
||||||
|
x_sorgenti_mm[visibili],
|
||||||
|
np.full(int(visibili.sum()), q_max_MW * 1.12),
|
||||||
|
)
|
||||||
|
|
||||||
|
immagine.set_data(dati["campi"][k].T)
|
||||||
|
|
||||||
|
linea_vera.set_data(tempi[: k + 1], dati["T_vere"][: k + 1])
|
||||||
|
linea_letta.set_data(tempi[: k + 1], dati["T_lette"][: k + 1])
|
||||||
|
cursore.set_xdata([t, t])
|
||||||
|
testo_tempo.set_text(f"t = {t:.3f} s")
|
||||||
|
|
||||||
|
return (
|
||||||
|
linea_flusso, marker_sorgenti, immagine,
|
||||||
|
linea_vera, linea_letta, cursore, testo_tempo,
|
||||||
|
)
|
||||||
|
|
||||||
|
animazione = FuncAnimation(
|
||||||
|
fig, aggiorna, frames=n_frame, interval=INTERVALLO_RIPRODUZIONE_MS, blit=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Se il backend non è interattivo si salva una GIF invece di mostrare la finestra.
|
||||||
|
if matplotlib.get_backend().lower() == "agg":
|
||||||
|
percorso = Path("dataset") / "animazione_sezione.gif"
|
||||||
|
animazione.save(
|
||||||
|
percorso, writer=PillowWriter(fps=1000 // INTERVALLO_RIPRODUZIONE_MS)
|
||||||
|
)
|
||||||
|
print(f"Backend non interattivo: animazione salvata in {percorso}")
|
||||||
|
return
|
||||||
|
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# Animazione 3D del barattolo in vista isometrica: colormap della temperatura
|
||||||
|
# sulla superficie esterna.
|
||||||
|
#
|
||||||
|
# Il modello risolve solo la sezione x-z (vedi CLAUDE.md): la coordinata
|
||||||
|
# circonferenziale y non è simulata, è collassata in un'attenuazione
|
||||||
|
# gaussiana del flusso. Per estrudere il campo attorno alla circonferenza si
|
||||||
|
# riusa la stessa gaussiana: la sovratemperatura rispetto al piano y=0 (dove
|
||||||
|
# si trova il sensore) viene scalata in funzione della distanza
|
||||||
|
# circonferenziale dal percorso delle sorgenti. È quindi una ricostruzione
|
||||||
|
# visiva, non un calcolo di diffusione in y.
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
from matplotlib import cm
|
||||||
|
from matplotlib.animation import FuncAnimation, PillowWriter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from config import FASCETTA, SIMULAZIONE
|
||||||
|
from plot_animazione import DT_FRAME_S as DT_FRAME_S_BASE
|
||||||
|
from plot_animazione import T_FINE_ANIMAZIONE_S, T_INIZIO_ANIMAZIONE_S, simula_campi
|
||||||
|
from simulate import configurazione_randomizzata
|
||||||
|
|
||||||
|
# Tempo simulato tra un fotogramma e il successivo dell'animazione 3D. Più
|
||||||
|
# largo di DT_FRAME_S_BASE perché ricreare una superficie 3D a ogni
|
||||||
|
# fotogramma è più costoso della semplice imshow 2D.
|
||||||
|
DT_FRAME_S = 0.15
|
||||||
|
|
||||||
|
# Millisecondi tra i fotogrammi in riproduzione.
|
||||||
|
INTERVALLO_RIPRODUZIONE_MS = 60
|
||||||
|
|
||||||
|
# Numero di punti lungo la circonferenza per l'estrusione della superficie.
|
||||||
|
N_THETA = 72
|
||||||
|
|
||||||
|
|
||||||
|
def attenuazione_circonferenziale(
|
||||||
|
y_m: np.ndarray, offset_y_m: float, sigma_m: float
|
||||||
|
) -> np.ndarray:
|
||||||
|
# Fattore che scala la sovratemperatura (T - T_ambiente) del piano y=0
|
||||||
|
# in funzione della distanza circonferenziale y dal piano stesso,
|
||||||
|
# normalizzato in modo da valere 1 in y=0.
|
||||||
|
sigma = max(sigma_m, 1e-9)
|
||||||
|
esponente = -0.5 * ((y_m - offset_y_m) ** 2 - offset_y_m ** 2) / (sigma * sigma)
|
||||||
|
return np.exp(esponente)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
rng = random.Random(SIMULAZIONE["seed"])
|
||||||
|
cfg_run = configurazione_randomizzata(1, rng)
|
||||||
|
dati = simula_campi(cfg_run)
|
||||||
|
|
||||||
|
tempi = dati["tempi"]
|
||||||
|
indice_inizio = int(np.searchsorted(tempi, T_INIZIO_ANIMAZIONE_S))
|
||||||
|
passo = max(1, round(DT_FRAME_S / DT_FRAME_S_BASE))
|
||||||
|
indici_frame = list(range(indice_inizio, len(tempi), passo))
|
||||||
|
|
||||||
|
T_ambiente = cfg_run["aria"]["temperatura_ambiente_C"]
|
||||||
|
sorgente = cfg_run["sorgente"]
|
||||||
|
sigma_m = sorgente["sigma_punto_m"]
|
||||||
|
offset_y_m = sorgente["offset_y_percorso_m"]
|
||||||
|
numero_sorgenti = sorgente.get("numero_sorgenti", 1)
|
||||||
|
distanza_m = sorgente.get("distanza_sorgenti_m", 0.0)
|
||||||
|
|
||||||
|
raggio_m = (FASCETTA["diametro_mm"] / 1000.0) / 2.0
|
||||||
|
x_centri_m = dati["x_centri_mm"] / 1000.0
|
||||||
|
lunghezza_m = dati["lunghezza_mm"] / 1000.0
|
||||||
|
|
||||||
|
theta = np.linspace(-np.pi, np.pi, N_THETA)
|
||||||
|
y_circ_m = theta * raggio_m
|
||||||
|
attenuazione = attenuazione_circonferenziale(y_circ_m, offset_y_m, sigma_m)
|
||||||
|
|
||||||
|
Xm, Thetam = np.meshgrid(x_centri_m, theta)
|
||||||
|
Ym = raggio_m * np.sin(Thetam)
|
||||||
|
Zm = raggio_m * np.cos(Thetam)
|
||||||
|
|
||||||
|
T_max = max(c[:, 0].max() for c in dati["campi"])
|
||||||
|
# vmin più basso della temperatura ambiente reale: altrimenti le zone
|
||||||
|
# fredde cadrebbero sul nero puro di "inferno" e, essendo lo shading
|
||||||
|
# moltiplicativo, nessuna illuminazione basterebbe a renderle visibili.
|
||||||
|
norm = matplotlib.colors.Normalize(
|
||||||
|
vmin=T_ambiente - 0.4 * (T_max - T_ambiente), vmax=T_max
|
||||||
|
)
|
||||||
|
cmap = matplotlib.colormaps["inferno"]
|
||||||
|
lightsource = matplotlib.colors.LightSource(azdeg=315, altdeg=45)
|
||||||
|
|
||||||
|
fig = plt.figure(figsize=(9, 7))
|
||||||
|
ax = fig.add_subplot(projection="3d")
|
||||||
|
ax.view_init(elev=35.264, azim=45)
|
||||||
|
ax.set_box_aspect((lunghezza_m, 2 * raggio_m, 2 * raggio_m))
|
||||||
|
ax.set_xlabel("x [m]")
|
||||||
|
ax.set_axis_off()
|
||||||
|
|
||||||
|
# La colorbar mostra il range reale delle temperature: il norm esteso
|
||||||
|
# verso il basso serve solo a schiarire il colore di base della
|
||||||
|
# superficie fredda, non deve comparire nella scala mostrata all'utente.
|
||||||
|
norm_colorbar = matplotlib.colors.Normalize(vmin=T_ambiente, vmax=T_max)
|
||||||
|
mappabile = cm.ScalarMappable(cmap=cmap, norm=norm_colorbar)
|
||||||
|
mappabile.set_array([])
|
||||||
|
barra = fig.colorbar(mappabile, ax=ax, shrink=0.6, pad=0.05)
|
||||||
|
barra.set_label("T [°C]")
|
||||||
|
|
||||||
|
def disegna_frame(k: int):
|
||||||
|
ax.cla()
|
||||||
|
ax.view_init(elev=35.264, azim=45)
|
||||||
|
ax.set_box_aspect((lunghezza_m, 2 * raggio_m, 2 * raggio_m))
|
||||||
|
ax.set_axis_off()
|
||||||
|
|
||||||
|
T_lato_esterno = dati["campi"][k][:, 0]
|
||||||
|
T_superficie = T_ambiente + (T_lato_esterno[None, :] - T_ambiente) * attenuazione[:, None]
|
||||||
|
colori = cmap(norm(T_superficie))
|
||||||
|
|
||||||
|
ax.plot_surface(
|
||||||
|
Xm, Ym, Zm, facecolors=colori, rstride=1, cstride=1,
|
||||||
|
antialiased=False, shade=True, lightsource=lightsource, linewidth=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
x_sorgenti_m = dati["x_riferimenti"][k] + np.arange(numero_sorgenti) * distanza_m
|
||||||
|
visibili = (x_sorgenti_m >= 0.0) & (x_sorgenti_m <= lunghezza_m)
|
||||||
|
if visibili.any():
|
||||||
|
theta_sorgente = offset_y_m / raggio_m
|
||||||
|
ax.scatter(
|
||||||
|
x_sorgenti_m[visibili],
|
||||||
|
np.full(int(visibili.sum()), raggio_m * np.sin(theta_sorgente) * 1.05),
|
||||||
|
np.full(int(visibili.sum()), raggio_m * np.cos(theta_sorgente) * 1.05),
|
||||||
|
color="cyan", s=25, depthshade=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
ax.set_title(f"t = {tempi[k]:.3f} s")
|
||||||
|
return ()
|
||||||
|
|
||||||
|
animazione = FuncAnimation(
|
||||||
|
fig, disegna_frame, frames=indici_frame,
|
||||||
|
interval=INTERVALLO_RIPRODUZIONE_MS, blit=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if matplotlib.get_backend().lower() == "agg":
|
||||||
|
percorso = Path("dataset") / "animazione_3d.gif"
|
||||||
|
animazione.save(
|
||||||
|
percorso, writer=PillowWriter(fps=1000 // INTERVALLO_RIPRODUZIONE_MS)
|
||||||
|
)
|
||||||
|
print(f"Backend non interattivo: animazione salvata in {percorso}")
|
||||||
|
return
|
||||||
|
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+16
-6
@@ -1,5 +1,6 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
@@ -13,24 +14,33 @@ def main() -> None:
|
|||||||
|
|
||||||
df = pd.read_csv(percorso_csv)
|
df = pd.read_csv(percorso_csv)
|
||||||
|
|
||||||
plt.figure()
|
fig_temperature = 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_vera_lato_sensore_C"], label="Temperatura vera lato interno")
|
||||||
plt.plot(df["tempo_s"], df["T_misurata_sensore_C"], label="Temperatura misurata dal sensore")
|
plt.plot(df["tempo_s"], df["T_misurata_sensore_C"], label="Temperatura misurata dal sensore")
|
||||||
plt.xlabel("Tempo [s]")
|
plt.xlabel("Tempo [s]")
|
||||||
plt.ylabel("Temperatura [°C]")
|
plt.ylabel("Temperatura [°C]")
|
||||||
plt.title("Sensore fisso, induttore in moto")
|
plt.title("Sensore fisso, sorgenti in moto")
|
||||||
plt.legend()
|
plt.legend()
|
||||||
plt.grid(True)
|
plt.grid(True)
|
||||||
plt.tight_layout()
|
plt.tight_layout()
|
||||||
plt.show()
|
|
||||||
|
|
||||||
plt.figure()
|
fig_flusso = plt.figure()
|
||||||
plt.plot(df["tempo_s"], df["flusso_termico_sorgente_W_m2"])
|
plt.plot(df["tempo_s"], df["flusso_termico_sorgente_W_m2"])
|
||||||
plt.xlabel("Tempo [s]")
|
plt.xlabel("Tempo [s]")
|
||||||
plt.ylabel("Flusso termico efficace [W/m²]")
|
plt.ylabel("Flusso termico efficace [W/m²]")
|
||||||
plt.title("Flusso termico visto dalla linea del sensore")
|
plt.title("Flusso termico nel punto x osservato dal sensore")
|
||||||
plt.grid(True)
|
plt.grid(True)
|
||||||
plt.tight_layout()
|
plt.tight_layout()
|
||||||
|
|
||||||
|
# Se il backend non è interattivo (nessun display o backend GUI
|
||||||
|
# disponibile), plt.show() non aprirebbe nulla: si salvano i PNG.
|
||||||
|
if matplotlib.get_backend().lower() == "agg":
|
||||||
|
for fig, nome in [(fig_temperature, "temperature"), (fig_flusso, "flusso")]:
|
||||||
|
percorso = Path("dataset") / f"grafico_{nome}.png"
|
||||||
|
fig.savefig(percorso, dpi=150)
|
||||||
|
print(f"Backend non interattivo: grafico salvato in {percorso}")
|
||||||
|
return
|
||||||
|
|
||||||
plt.show()
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
numpy
|
numpy
|
||||||
|
scipy
|
||||||
pandas
|
pandas
|
||||||
matplotlib
|
matplotlib
|
||||||
|
PySide6
|
||||||
|
|||||||
+217
-129
@@ -1,13 +1,17 @@
|
|||||||
import csv
|
import csv
|
||||||
import math
|
import math
|
||||||
|
import os
|
||||||
import random
|
import random
|
||||||
import shutil
|
import shutil
|
||||||
|
from concurrent.futures import ProcessPoolExecutor
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import scipy.sparse as sp
|
||||||
|
from scipy.sparse.linalg import splu
|
||||||
|
|
||||||
from config import ARIA, PIASTRA, RANDOMIZZAZIONE, SENSORE, SIMULAZIONE, SORGENTE
|
from config import ARIA, FASCETTA, RANDOMIZZAZIONE, SENSORE, SIMULAZIONE, SORGENTE
|
||||||
from materials import MATERIALI
|
from materials import MATERIALI
|
||||||
|
|
||||||
|
|
||||||
@@ -34,31 +38,28 @@ def _spread_sorgenti_m(sorgente: dict) -> float:
|
|||||||
return (numero_sorgenti - 1) * distanza
|
return (numero_sorgenti - 1) * distanza
|
||||||
|
|
||||||
|
|
||||||
def _x_riferimento_iniziale_m(sorgente: dict) -> float:
|
def _x_riferimento_iniziale_m(sorgente: dict, x_sensore_m: float) -> float:
|
||||||
# Posizione a t=0 della sorgente di indice 0 (quella più arretrata nel
|
# Posizione a t=0 della sorgente di indice 0. Le sorgenti i sono a
|
||||||
# verso di marcia). x_inizio_m è la distanza dal sensore della sorgente
|
# x_i = riferimento + i * distanza, quindi per v >= 0 l'indice 0 è la
|
||||||
# più avanzata (che quindi lo raggiunge per prima).
|
# più arretrata nel verso di marcia, per v < 0 è la più avanzata.
|
||||||
|
# x_inizio_m è la distanza dal sensore della sorgente più avanzata
|
||||||
|
# (quella che lo raggiunge per prima).
|
||||||
spread = _spread_sorgenti_m(sorgente)
|
spread = _spread_sorgenti_m(sorgente)
|
||||||
x_sensore = sorgente["x_sensore_m"]
|
|
||||||
x_inizio = sorgente["x_inizio_m"]
|
x_inizio = sorgente["x_inizio_m"]
|
||||||
if sorgente["velocita_m_s"] >= 0:
|
if sorgente["velocita_m_s"] >= 0:
|
||||||
return (x_sensore - x_inizio) - spread
|
return (x_sensore_m - x_inizio) - spread
|
||||||
return (x_sensore + x_inizio) + spread
|
return x_sensore_m + x_inizio
|
||||||
|
|
||||||
|
|
||||||
def _x_riferimento_finale_m(sorgente: dict) -> float:
|
def _x_riferimento_finale_m(sorgente: dict, x_sensore_m: float) -> float:
|
||||||
# Posizione di fine corsa della sorgente di indice 0. x_fine_m è la
|
# Posizione di fine corsa della sorgente di indice 0. x_fine_m è la
|
||||||
# distanza dal sensore della sorgente più arretrata (che quindi lo
|
# distanza dal sensore della sorgente più arretrata nel verso di marcia
|
||||||
# supera per ultima).
|
# (quella che lo supera per ultima).
|
||||||
x_sensore = sorgente["x_sensore_m"]
|
spread = _spread_sorgenti_m(sorgente)
|
||||||
x_fine = sorgente["x_fine_m"]
|
x_fine = sorgente["x_fine_m"]
|
||||||
if sorgente["velocita_m_s"] >= 0:
|
if sorgente["velocita_m_s"] >= 0:
|
||||||
return x_sensore + x_fine
|
return x_sensore_m + x_fine
|
||||||
return x_sensore - x_fine
|
return (x_sensore_m - x_fine) - spread
|
||||||
|
|
||||||
|
|
||||||
def x_sorgente_al_tempo(sorgente: dict, t_s: float) -> float:
|
|
||||||
return _x_riferimento_iniziale_m(sorgente) + sorgente["velocita_m_s"] * t_s
|
|
||||||
|
|
||||||
|
|
||||||
def _intervallo_attivo(inizio: float, fine: float, v: float, x_m: float) -> bool:
|
def _intervallo_attivo(inizio: float, fine: float, v: float, x_m: float) -> bool:
|
||||||
@@ -67,18 +68,21 @@ def _intervallo_attivo(inizio: float, fine: float, v: float, x_m: float) -> bool
|
|||||||
return fine <= x_m <= inizio
|
return fine <= x_m <= inizio
|
||||||
|
|
||||||
|
|
||||||
def flusso_termico_incidente_W_m2(sorgente: dict, t_s: float) -> tuple[float, float]:
|
def profilo_flusso_incidente_W_m2(
|
||||||
|
sorgente: dict,
|
||||||
|
x_sensore_m: float,
|
||||||
|
t_s: float,
|
||||||
|
x_centri_m: np.ndarray,
|
||||||
|
) -> tuple[float, np.ndarray]:
|
||||||
# Restituisce x_sorgente_m (posizione della sorgente di riferimento) e
|
# Restituisce x_sorgente_m (posizione della sorgente di riferimento) e
|
||||||
# flusso_termico_efficace_W_m2 (somma dei contributi di tutte le sorgenti).
|
# il profilo di flusso termico efficace q(x) [W/m²] sul lato esterno,
|
||||||
|
# somma dei contributi di tutte le sorgenti attive.
|
||||||
#
|
#
|
||||||
# Il movimento è rappresentato con un'impronta gaussiana centrata su
|
# Ogni sorgente in moto ha un'impronta gaussiana lungo x, valutata sui
|
||||||
# ciascuna sorgente in moto. Più sorgenti equidistanti si muovono insieme
|
# centri cella della sezione. L'offset circonferenziale y non è risolto
|
||||||
# come un gruppo rigido: condividono velocità, sigma e flusso di picco, e
|
# spazialmente: entra come attenuazione gaussiana del flusso.
|
||||||
# sono sfalsate lungo x di un multiplo di "distanza_sorgenti_m". Il
|
x_rif_iniziale = _x_riferimento_iniziale_m(sorgente, x_sensore_m)
|
||||||
# modello 1D vede solo il flusso lungo la linea che passa per il sensore
|
x_rif_finale = _x_riferimento_finale_m(sorgente, x_sensore_m)
|
||||||
# fisso, sommando il contributo di tutte le sorgenti attive.
|
|
||||||
x_rif_iniziale = _x_riferimento_iniziale_m(sorgente)
|
|
||||||
x_rif_finale = _x_riferimento_finale_m(sorgente)
|
|
||||||
x_riferimento = x_rif_iniziale + sorgente["velocita_m_s"] * t_s
|
x_riferimento = x_rif_iniziale + sorgente["velocita_m_s"] * t_s
|
||||||
|
|
||||||
numero_sorgenti = sorgente.get("numero_sorgenti", 1)
|
numero_sorgenti = sorgente.get("numero_sorgenti", 1)
|
||||||
@@ -88,7 +92,10 @@ def flusso_termico_incidente_W_m2(sorgente: dict, t_s: float) -> tuple[float, fl
|
|||||||
dy = sorgente["offset_y_percorso_m"]
|
dy = sorgente["offset_y_percorso_m"]
|
||||||
sigma = sorgente["sigma_punto_m"]
|
sigma = sorgente["sigma_punto_m"]
|
||||||
|
|
||||||
q_totale = 0.0
|
q_picco = sorgente["flusso_termico_picco_W_m2"] * sorgente["efficienza_riscaldamento"]
|
||||||
|
attenuazione_y = math.exp(-0.5 * (dy * dy) / (sigma * sigma))
|
||||||
|
|
||||||
|
q_x = np.zeros_like(x_centri_m)
|
||||||
for i in range(numero_sorgenti):
|
for i in range(numero_sorgenti):
|
||||||
x_i = x_riferimento + i * distanza
|
x_i = x_riferimento + i * distanza
|
||||||
|
|
||||||
@@ -98,74 +105,161 @@ def flusso_termico_incidente_W_m2(sorgente: dict, t_s: float) -> tuple[float, fl
|
|||||||
if not _intervallo_attivo(inizio_i, fine_i, v, x_i):
|
if not _intervallo_attivo(inizio_i, fine_i, v, x_i):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
dx = x_i - sorgente["x_sensore_m"]
|
dx = x_centri_m - x_i
|
||||||
gaussiana = math.exp(-0.5 * (dx * dx + dy * dy) / (sigma * sigma))
|
q_x += q_picco * attenuazione_y * np.exp(-0.5 * (dx * dx) / (sigma * sigma))
|
||||||
q_totale += (
|
|
||||||
sorgente["flusso_termico_picco_W_m2"]
|
|
||||||
* sorgente["efficienza_riscaldamento"]
|
|
||||||
* gaussiana
|
|
||||||
)
|
|
||||||
|
|
||||||
return x_riferimento, q_totale
|
return x_riferimento, q_x
|
||||||
|
|
||||||
|
|
||||||
def riscaldamento_volumetrico_W_m3(
|
def profilo_deposizione_z_1_m(
|
||||||
q_superficie_W_m2: float,
|
|
||||||
z_centri_m: np.ndarray,
|
z_centri_m: np.ndarray,
|
||||||
spessore_m: float,
|
spessore_m: float,
|
||||||
skin_depth_m: float,
|
skin_depth_m: float,
|
||||||
) -> np.ndarray:
|
) -> np.ndarray:
|
||||||
# Converte il flusso superficiale equivalente in riscaldamento volumetrico q_vol(z).
|
# Profilo di deposizione volumetrica del flusso superficiale [1/m]:
|
||||||
#
|
#
|
||||||
# q_vol(z) = A * exp(-z / delta)
|
# p(z) = exp(-z / delta) / (delta * (1 - exp(-spessore / delta)))
|
||||||
#
|
#
|
||||||
# A è scelto in modo che integrale_0^L q_vol(z) dz = q_superficie_W_m2.
|
# Normalizzato in modo che integrale_0^spessore p(z) dz = 1, così che
|
||||||
if q_superficie_W_m2 <= 0.0:
|
# q_vol(x, z) = q(x) * p(z) conservi il flusso superficiale.
|
||||||
return np.zeros_like(z_centri_m)
|
|
||||||
|
|
||||||
delta = max(skin_depth_m, 1e-9)
|
delta = max(skin_depth_m, 1e-9)
|
||||||
normalizzazione = delta * (1.0 - math.exp(-spessore_m / delta))
|
normalizzazione = delta * (1.0 - math.exp(-spessore_m / delta))
|
||||||
return (q_superficie_W_m2 / normalizzazione) * np.exp(-z_centri_m / delta)
|
return np.exp(-z_centri_m / delta) / normalizzazione
|
||||||
|
|
||||||
|
|
||||||
def costruisci_matrice_implicita(
|
def _laplaciano_1d(n: int) -> sp.spmatrix:
|
||||||
n: int,
|
# Operatore alle differenze -T'' su n celle con bordi adiabatici (Neumann).
|
||||||
|
diagonale = np.full(n, 2.0)
|
||||||
|
diagonale[0] = 1.0
|
||||||
|
diagonale[-1] = 1.0
|
||||||
|
fuori = -np.ones(n - 1)
|
||||||
|
return sp.diags([fuori, diagonale, fuori], [-1, 0, 1])
|
||||||
|
|
||||||
|
|
||||||
|
def costruisci_solutore_implicito_2d(
|
||||||
|
n_x: int,
|
||||||
|
n_z: int,
|
||||||
dt_s: float,
|
dt_s: float,
|
||||||
|
dx_m: float,
|
||||||
dz_m: float,
|
dz_m: float,
|
||||||
|
spessore_m: float,
|
||||||
materiale: dict,
|
materiale: dict,
|
||||||
h_caldo_W_m2K: float,
|
h_esterno_W_m2K: float,
|
||||||
h_freddo_W_m2K: float,
|
h_interno_W_m2K: float,
|
||||||
) -> np.ndarray:
|
h_bordi_W_m2K: float,
|
||||||
# Costruisce la matrice A per Eulero implicito:
|
):
|
||||||
|
# Costruisce e fattorizza (LU sparsa) la matrice A per Eulero implicito 2D:
|
||||||
# A * T_next = rhs
|
# A * T_next = rhs
|
||||||
#
|
#
|
||||||
# Le celle di bordo includono la convezione verso l'ambiente.
|
# Le incognite sono i centri cella T[i, j] con i lungo x e j lungo z,
|
||||||
|
# appiattiti in ordine C (indice = i * n_z + j). Tutti e quattro i lati
|
||||||
|
# della sezione includono la convezione verso l'ambiente; su ogni cella
|
||||||
|
# agisce inoltre la conduzione circonferenziale (y) verso il resto della
|
||||||
|
# fascetta, assunto a temperatura ambiente. Il termine è ricavato
|
||||||
|
# considerando l'intero volume della fascia cilindrica (equazione
|
||||||
|
# dell'aletta): il calore conduce lungo y attraverso l'intero spessore
|
||||||
|
# mentre le superfici esterna e interna dell'intero cilindro scambiano
|
||||||
|
# per convezione, dando un sink distribuito uniformemente su ogni cella
|
||||||
|
# q_y = -(h_esterno + h_interno) / spessore * (T - T_amb).
|
||||||
k = materiale["conducibilita_termica_W_mK"]
|
k = materiale["conducibilita_termica_W_mK"]
|
||||||
rho = materiale["densita_kg_m3"]
|
rho = materiale["densita_kg_m3"]
|
||||||
cp = materiale["calore_specifico_J_kgK"]
|
cp = materiale["calore_specifico_J_kgK"]
|
||||||
alpha = k / (rho * cp)
|
alpha = k / (rho * cp)
|
||||||
|
|
||||||
r = alpha * dt_s / (dz_m * dz_m)
|
r_x = alpha * dt_s / (dx_m * dx_m)
|
||||||
b_caldo = h_caldo_W_m2K * dt_s / (rho * cp * dz_m)
|
r_z = alpha * dt_s / (dz_m * dz_m)
|
||||||
b_freddo = h_freddo_W_m2K * dt_s / (rho * cp * dz_m)
|
b_esterno = h_esterno_W_m2K * dt_s / (rho * cp * dz_m)
|
||||||
|
b_interno = h_interno_W_m2K * dt_s / (rho * cp * dz_m)
|
||||||
|
b_bordo = h_bordi_W_m2K * dt_s / (rho * cp * dx_m)
|
||||||
|
c_y = (h_esterno_W_m2K + h_interno_W_m2K) * dt_s / (rho * cp * spessore_m)
|
||||||
|
|
||||||
A = np.zeros((n, n), dtype=float)
|
n = n_x * n_z
|
||||||
|
scambio = np.full(n, c_y)
|
||||||
|
scambio[0::n_z] += b_esterno
|
||||||
|
scambio[n_z - 1::n_z] += b_interno
|
||||||
|
scambio[:n_z] += b_bordo
|
||||||
|
scambio[n - n_z:] += b_bordo
|
||||||
|
|
||||||
# Bordo caldo, z = 0.
|
A = (
|
||||||
A[0, 0] = 1.0 + r + b_caldo
|
sp.identity(n)
|
||||||
A[0, 1] = -r
|
+ r_x * sp.kron(_laplaciano_1d(n_x), sp.identity(n_z))
|
||||||
|
+ r_z * sp.kron(sp.identity(n_x), _laplaciano_1d(n_z))
|
||||||
|
+ sp.diags(scambio)
|
||||||
|
)
|
||||||
|
return splu(sp.csc_matrix(A))
|
||||||
|
|
||||||
# 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.
|
def prepara_stato_termico(fascetta: dict, aria: dict, sorgente: dict) -> dict:
|
||||||
A[n - 1, n - 2] = -r
|
# Prepara griglia, coefficienti e solutore fattorizzato per un run:
|
||||||
A[n - 1, n - 1] = 1.0 + r + b_freddo
|
# tutto ciò che resta costante durante l'integrazione temporale.
|
||||||
|
materiale = MATERIALI[fascetta["materiale"]]
|
||||||
|
|
||||||
return A
|
lunghezza = fascetta["lunghezza_mm"] / 1000.0
|
||||||
|
spessore = fascetta["spessore_mm"] / 1000.0
|
||||||
|
n_x = fascetta["n_nodi_x"]
|
||||||
|
n_z = fascetta["n_nodi_z"]
|
||||||
|
dx = lunghezza / n_x
|
||||||
|
dz = spessore / n_z
|
||||||
|
|
||||||
|
dt = SIMULAZIONE["dt_interno_s"]
|
||||||
|
|
||||||
|
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"])
|
||||||
|
|
||||||
|
rho = materiale["densita_kg_m3"]
|
||||||
|
cp = materiale["calore_specifico_J_kgK"]
|
||||||
|
alpha = materiale["conducibilita_termica_W_mK"] / (rho * cp)
|
||||||
|
|
||||||
|
z_centri = (np.arange(n_z) + 0.5) * dz
|
||||||
|
|
||||||
|
return {
|
||||||
|
"n_x": n_x,
|
||||||
|
"n_z": n_z,
|
||||||
|
"dx_m": dx,
|
||||||
|
"dz_m": dz,
|
||||||
|
"dt_s": dt,
|
||||||
|
"x_centri_m": (np.arange(n_x) + 0.5) * dx,
|
||||||
|
"z_centri_m": z_centri,
|
||||||
|
"skin_depth_m": skin_depth,
|
||||||
|
"rho": rho,
|
||||||
|
"cp": cp,
|
||||||
|
"profilo_z": profilo_deposizione_z_1_m(z_centri, spessore, skin_depth),
|
||||||
|
"b_esterno": aria["h_esterno_W_m2K"] * dt / (rho * cp * dz),
|
||||||
|
"b_interno": aria["h_interno_W_m2K"] * dt / (rho * cp * dz),
|
||||||
|
"b_bordo": aria["h_bordi_W_m2K"] * dt / (rho * cp * dx),
|
||||||
|
"c_y": (aria["h_esterno_W_m2K"] + aria["h_interno_W_m2K"]) * dt / (rho * cp * spessore),
|
||||||
|
"T_ambiente_C": aria["temperatura_ambiente_C"],
|
||||||
|
"solutore": costruisci_solutore_implicito_2d(
|
||||||
|
n_x=n_x,
|
||||||
|
n_z=n_z,
|
||||||
|
dt_s=dt,
|
||||||
|
dx_m=dx,
|
||||||
|
dz_m=dz,
|
||||||
|
spessore_m=spessore,
|
||||||
|
materiale=materiale,
|
||||||
|
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"],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def passo_implicito(stato: dict, T: np.ndarray, q_x: np.ndarray) -> np.ndarray:
|
||||||
|
# Avanza il campo di temperatura di un passo dt: assembla il termine noto
|
||||||
|
# (sorgente volumetrica, convezione sui quattro lati, conduzione
|
||||||
|
# circonferenziale verso l'ambiente) e risolve il sistema implicito.
|
||||||
|
T_amb = stato["T_ambiente_C"]
|
||||||
|
rhs = T + (stato["dt_s"] / (stato["rho"] * stato["cp"])) * (
|
||||||
|
q_x[:, None] * stato["profilo_z"][None, :]
|
||||||
|
)
|
||||||
|
rhs += stato["c_y"] * T_amb
|
||||||
|
rhs[:, 0] += stato["b_esterno"] * T_amb
|
||||||
|
rhs[:, -1] += stato["b_interno"] * T_amb
|
||||||
|
rhs[0, :] += stato["b_bordo"] * T_amb
|
||||||
|
rhs[-1, :] += stato["b_bordo"] * T_amb
|
||||||
|
return stato["solutore"].solve(rhs.ravel()).reshape(T.shape)
|
||||||
|
|
||||||
|
|
||||||
def quantizza(valore: float, passo: float) -> float:
|
def quantizza(valore: float, passo: float) -> float:
|
||||||
@@ -175,7 +269,7 @@ def quantizza(valore: float, passo: float) -> float:
|
|||||||
|
|
||||||
|
|
||||||
def configurazione_randomizzata(indice_run: int, rng: random.Random) -> dict:
|
def configurazione_randomizzata(indice_run: int, rng: random.Random) -> dict:
|
||||||
piastra = deepcopy(PIASTRA)
|
fascetta = deepcopy(FASCETTA)
|
||||||
aria = deepcopy(ARIA)
|
aria = deepcopy(ARIA)
|
||||||
sorgente = deepcopy(SORGENTE)
|
sorgente = deepcopy(SORGENTE)
|
||||||
sensore = deepcopy(SENSORE)
|
sensore = deepcopy(SENSORE)
|
||||||
@@ -214,7 +308,7 @@ def configurazione_randomizzata(indice_run: int, rng: random.Random) -> dict:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"id_run": f"run_{indice_run:04d}",
|
"id_run": f"run_{indice_run:04d}",
|
||||||
"piastra": piastra,
|
"fascetta": fascetta,
|
||||||
"aria": aria,
|
"aria": aria,
|
||||||
"sorgente": sorgente,
|
"sorgente": sorgente,
|
||||||
"sensore": sensore,
|
"sensore": sensore,
|
||||||
@@ -222,48 +316,31 @@ def configurazione_randomizzata(indice_run: int, rng: random.Random) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
||||||
piastra = cfg_run["piastra"]
|
fascetta = cfg_run["fascetta"]
|
||||||
aria = cfg_run["aria"]
|
aria = cfg_run["aria"]
|
||||||
sorgente = cfg_run["sorgente"]
|
sorgente = cfg_run["sorgente"]
|
||||||
sensore = cfg_run["sensore"]
|
sensore = cfg_run["sensore"]
|
||||||
|
|
||||||
nome_materiale = piastra["materiale"]
|
nome_materiale = fascetta["materiale"]
|
||||||
materiale = MATERIALI[nome_materiale]
|
|
||||||
|
|
||||||
spessore = piastra["spessore_mm"] / 1000.0
|
stato = prepara_stato_termico(fascetta, aria, sorgente)
|
||||||
n = piastra["n_nodi"]
|
n_x = stato["n_x"]
|
||||||
dz = spessore / n
|
n_z = stato["n_z"]
|
||||||
z_centri = (np.arange(n) + 0.5) * dz
|
x_centri = stato["x_centri_m"]
|
||||||
|
skin_depth = stato["skin_depth_m"]
|
||||||
|
|
||||||
dt = SIMULAZIONE["dt_interno_s"]
|
x_sensore = sensore["x_mm"] / 1000.0
|
||||||
|
i_sensore = min(n_x - 1, max(0, int(x_sensore / stato["dx_m"])))
|
||||||
|
|
||||||
|
dt = stato["dt_s"]
|
||||||
durata = SIMULAZIONE["durata_s"]
|
durata = SIMULAZIONE["durata_s"]
|
||||||
periodo_campionamento = 1.0 / SIMULAZIONE["frequenza_campionamento_hz"]
|
periodo_campionamento = 1.0 / SIMULAZIONE["frequenza_campionamento_hz"]
|
||||||
|
|
||||||
if sorgente["skin_depth_fissa_m"] is None:
|
T = np.full((n_x, n_z), aria["temperatura_ambiente_C"], dtype=float)
|
||||||
skin_depth = calcola_skin_depth_m(materiale, sorgente["frequenza_hz"])
|
T_sensore = T[i_sensore, -1]
|
||||||
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
|
prossimo_campione_t = 0.0
|
||||||
T_vera_max = T[-1]
|
T_vera_max = T[i_sensore, -1]
|
||||||
T_misurata_max = T_sensore
|
T_misurata_max = T_sensore
|
||||||
|
|
||||||
output_csv.parent.mkdir(parents=True, exist_ok=True)
|
output_csv.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -289,22 +366,14 @@ def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
|||||||
|
|
||||||
t = 0.0
|
t = 0.0
|
||||||
while t <= durata + 1e-12:
|
while t <= durata + 1e-12:
|
||||||
x_sorgente, q_superficie = flusso_termico_incidente_W_m2(sorgente, t)
|
x_sorgente, q_x = profilo_flusso_incidente_W_m2(
|
||||||
q_vol = riscaldamento_volumetrico_W_m3(
|
sorgente, x_sensore, t, x_centri
|
||||||
q_superficie_W_m2=q_superficie,
|
|
||||||
z_centri_m=z_centri,
|
|
||||||
spessore_m=spessore,
|
|
||||||
skin_depth_m=skin_depth,
|
|
||||||
)
|
)
|
||||||
|
T = passo_implicito(stato, T, q_x)
|
||||||
|
|
||||||
rhs = T + dt * q_vol / (rho * cp)
|
# Temperatura vera della superficie interna nel punto osservato
|
||||||
rhs[0] += b_caldo * aria["temperatura_ambiente_C"]
|
# dal sensore infrarosso.
|
||||||
rhs[-1] += b_freddo * aria["temperatura_ambiente_C"]
|
T_vera_lato_sensore = T[i_sensore, -1]
|
||||||
|
|
||||||
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.
|
# Inerzia del sensore del primo ordine.
|
||||||
tau_sensore = max(sensore["costante_tempo_s"], 1e-9)
|
tau_sensore = max(sensore["costante_tempo_s"], 1e-9)
|
||||||
@@ -323,11 +392,11 @@ def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
|||||||
f"{t:.6f}",
|
f"{t:.6f}",
|
||||||
f"{x_sorgente:.9f}",
|
f"{x_sorgente:.9f}",
|
||||||
f"{sorgente['offset_y_percorso_m']:.9f}",
|
f"{sorgente['offset_y_percorso_m']:.9f}",
|
||||||
f"{q_superficie:.6f}",
|
f"{q_x[i_sensore]:.6f}",
|
||||||
f"{skin_depth:.9e}",
|
f"{skin_depth:.9e}",
|
||||||
f"{T_vera_lato_sensore:.6f}",
|
f"{T_vera_lato_sensore:.6f}",
|
||||||
f"{misurata:.6f}",
|
f"{misurata:.6f}",
|
||||||
f"{T[0]:.6f}",
|
f"{T[i_sensore, 0]:.6f}",
|
||||||
f"{aria['temperatura_ambiente_C']:.6f}",
|
f"{aria['temperatura_ambiente_C']:.6f}",
|
||||||
f"{sorgente['velocita_m_s']:.9f}",
|
f"{sorgente['velocita_m_s']:.9f}",
|
||||||
f"{sorgente['sigma_punto_m']:.9f}",
|
f"{sorgente['sigma_punto_m']:.9f}",
|
||||||
@@ -342,17 +411,22 @@ def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
|||||||
"id_run": cfg_run["id_run"],
|
"id_run": cfg_run["id_run"],
|
||||||
"file_csv": str(output_csv.name),
|
"file_csv": str(output_csv.name),
|
||||||
"materiale": nome_materiale,
|
"materiale": nome_materiale,
|
||||||
"spessore_m": spessore,
|
"diametro_m": fascetta["diametro_mm"] / 1000.0,
|
||||||
"n_nodi": n,
|
"lunghezza_m": fascetta["lunghezza_mm"] / 1000.0,
|
||||||
|
"spessore_m": fascetta["spessore_mm"] / 1000.0,
|
||||||
|
"n_nodi_x": n_x,
|
||||||
|
"n_nodi_z": n_z,
|
||||||
"durata_s": durata,
|
"durata_s": durata,
|
||||||
"frequenza_campionamento_hz": SIMULAZIONE["frequenza_campionamento_hz"],
|
"frequenza_campionamento_hz": SIMULAZIONE["frequenza_campionamento_hz"],
|
||||||
"dt_interno_s": dt,
|
"dt_interno_s": dt,
|
||||||
"temperatura_ambiente_C": aria["temperatura_ambiente_C"],
|
"temperatura_ambiente_C": aria["temperatura_ambiente_C"],
|
||||||
"h_caldo_W_m2K": aria["h_caldo_W_m2K"],
|
"h_esterno_W_m2K": aria["h_esterno_W_m2K"],
|
||||||
"h_freddo_W_m2K": aria["h_freddo_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_inizio_m": sorgente["x_inizio_m"],
|
||||||
"x_fine_m": sorgente["x_fine_m"],
|
"x_fine_m": sorgente["x_fine_m"],
|
||||||
"x_sensore_m": sorgente["x_sensore_m"],
|
"x_sensore_m": x_sensore,
|
||||||
|
"distanza_sensore_parete_m": sensore["distanza_parete_mm"] / 1000.0,
|
||||||
"offset_y_percorso_m": sorgente["offset_y_percorso_m"],
|
"offset_y_percorso_m": sorgente["offset_y_percorso_m"],
|
||||||
"velocita_m_s": sorgente["velocita_m_s"],
|
"velocita_m_s": sorgente["velocita_m_s"],
|
||||||
"numero_sorgenti": sorgente.get("numero_sorgenti", 1),
|
"numero_sorgenti": sorgente.get("numero_sorgenti", 1),
|
||||||
@@ -370,19 +444,33 @@ def simula_singolo(cfg_run: dict, output_csv: Path, rng: random.Random) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _esegui_run(indice_e_seme: tuple[int, int]) -> dict:
|
||||||
|
# Ogni run riceve un seme indipendente derivato dal seed globale, così
|
||||||
|
# l'esecuzione in parallelo resta riproducibile indipendentemente
|
||||||
|
# dall'ordine in cui i processi la completano.
|
||||||
|
indice, seme = indice_e_seme
|
||||||
|
rng = random.Random(seme)
|
||||||
|
cfg_run = configurazione_randomizzata(indice, rng)
|
||||||
|
cartella_output = Path(SIMULAZIONE["cartella_output"])
|
||||||
|
percorso_csv = cartella_output / f"{cfg_run['id_run']}.csv"
|
||||||
|
return simula_singolo(cfg_run, percorso_csv, rng)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
cartella_output = Path(SIMULAZIONE["cartella_output"])
|
cartella_output = Path(SIMULAZIONE["cartella_output"])
|
||||||
if cartella_output.exists():
|
if cartella_output.exists():
|
||||||
shutil.rmtree(cartella_output)
|
shutil.rmtree(cartella_output)
|
||||||
cartella_output.mkdir(parents=True, exist_ok=True)
|
cartella_output.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
rng = random.Random(SIMULAZIONE["seed"])
|
rng_semi = random.Random(SIMULAZIONE["seed"])
|
||||||
|
num_run = SIMULAZIONE["num_run"]
|
||||||
|
semi = [rng_semi.randrange(2**63) for _ in range(num_run)]
|
||||||
|
|
||||||
righe_metadata = []
|
num_processi = SIMULAZIONE["num_processi"] or os.cpu_count() or 1
|
||||||
for i in range(1, SIMULAZIONE["num_run"] + 1):
|
with ProcessPoolExecutor(max_workers=num_processi) as executor:
|
||||||
cfg_run = configurazione_randomizzata(i, rng)
|
righe_metadata = list(
|
||||||
percorso_csv = cartella_output / f"{cfg_run['id_run']}.csv"
|
executor.map(_esegui_run, enumerate(semi, start=1))
|
||||||
righe_metadata.append(simula_singolo(cfg_run, percorso_csv, rng))
|
)
|
||||||
|
|
||||||
percorso_metadata = cartella_output / "metadata.csv"
|
percorso_metadata = cartella_output / "metadata.csv"
|
||||||
with percorso_metadata.open("w", newline="") as f:
|
with percorso_metadata.open("w", newline="") as f:
|
||||||
|
|||||||
Reference in New Issue
Block a user