Files
simulatore-induttori/simulate.py
T

399 lines
14 KiB
Python
Raw Normal View History

2026-07-05 11:58:52 +02:00
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 _spread_sorgenti_m(sorgente: dict) -> float:
# Distanza lungo x tra la prima e l'ultima sorgente del gruppo.
numero_sorgenti = sorgente.get("numero_sorgenti", 1)
distanza = sorgente.get("distanza_sorgenti_m", 0.0)
return (numero_sorgenti - 1) * distanza
def _x_riferimento_iniziale_m(sorgente: dict) -> float:
# Posizione a t=0 della sorgente di indice 0 (quella più arretrata nel
# verso di marcia). x_inizio_m è la distanza dal sensore della sorgente
# più avanzata (che quindi lo raggiunge per prima).
spread = _spread_sorgenti_m(sorgente)
x_sensore = sorgente["x_sensore_m"]
x_inizio = sorgente["x_inizio_m"]
if sorgente["velocita_m_s"] >= 0:
return (x_sensore - x_inizio) - spread
return (x_sensore + x_inizio) + spread
def _x_riferimento_finale_m(sorgente: dict) -> float:
# Posizione di fine corsa della sorgente di indice 0. x_fine_m è la
# distanza dal sensore della sorgente più arretrata (che quindi lo
# supera per ultima).
x_sensore = sorgente["x_sensore_m"]
x_fine = sorgente["x_fine_m"]
if sorgente["velocita_m_s"] >= 0:
return x_sensore + x_fine
return x_sensore - x_fine
2026-07-05 11:58:52 +02:00
def x_sorgente_al_tempo(sorgente: dict, t_s: float) -> float:
return _x_riferimento_iniziale_m(sorgente) + sorgente["velocita_m_s"] * t_s
2026-07-05 11:58:52 +02:00
def _intervallo_attivo(inizio: float, fine: float, v: float, x_m: float) -> bool:
2026-07-05 11:58:52 +02:00
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 (posizione della sorgente di riferimento) e
# flusso_termico_efficace_W_m2 (somma dei contributi di tutte le sorgenti).
2026-07-05 11:58:52 +02:00
#
# Il movimento è rappresentato con un'impronta gaussiana centrata su
# ciascuna sorgente in moto. Più sorgenti equidistanti si muovono insieme
# come un gruppo rigido: condividono velocità, sigma e flusso di picco, e
# sono sfalsate lungo x di un multiplo di "distanza_sorgenti_m". Il
# modello 1D vede solo il flusso lungo la linea che passa per il sensore
# 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
numero_sorgenti = sorgente.get("numero_sorgenti", 1)
distanza = sorgente.get("distanza_sorgenti_m", 0.0)
v = sorgente["velocita_m_s"]
zero_dopo_fine = sorgente.get("zero_dopo_fine", True)
2026-07-05 11:58:52 +02:00
dy = sorgente["offset_y_percorso_m"]
sigma = sorgente["sigma_punto_m"]
q_totale = 0.0
for i in range(numero_sorgenti):
x_i = x_riferimento + i * distanza
if zero_dopo_fine:
inizio_i = x_rif_iniziale + i * distanza
fine_i = x_rif_finale + i * distanza
if not _intervallo_attivo(inizio_i, fine_i, v, x_i):
continue
dx = x_i - sorgente["x_sensore_m"]
gaussiana = math.exp(-0.5 * (dx * dx + dy * dy) / (sigma * sigma))
q_totale += (
sorgente["flusso_termico_picco_W_m2"]
* sorgente["efficienza_riscaldamento"]
* gaussiana
)
2026-07-05 11:58:52 +02:00
return x_riferimento, q_totale
2026-07-05 11:58:52 +02:00
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_mm"] / 1000.0
2026-07-05 11:58:52 +02:00
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"],
"numero_sorgenti": sorgente.get("numero_sorgenti", 1),
"distanza_sorgenti_m": sorgente.get("distanza_sorgenti_m", 0.0),
2026-07-05 11:58:52 +02:00
"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()