Files
simulatore-induttori/valida_shell.py
T
2026-08-03 00:18:00 +02:00

253 lines
9.7 KiB
Python

# Validazione del solutore shell su casi con soluzione nota.
#
# Non è una suite di test automatica: stampa i risultati e i confronti perché
# molti sono verifiche di convergenza, dove conta l'andamento dell'errore più
# di una soglia di pass/fail. Va eseguita a mano dopo ogni modifica alla
# fisica o all'assemblaggio:
#
# python valida_shell.py
import math
import numpy as np
import assemblaggio as asm
import solutore as sol
import sorgente as src
from materials import MATERIALI
from mesh import campo_su_griglia, costruisci_mesh_cilindrica
MATERIALE = MATERIALI["banda_stagnata"]
K_TERM = MATERIALE["conducibilita_termica_W_mK"]
RHO = MATERIALE["densita_kg_m3"]
CP = MATERIALE["calore_specifico_J_kgK"]
ALPHA = K_TERM / (RHO * CP)
LUNGHEZZA_M = 0.100
RAGGIO_M = 0.035
SPESSORE_M = 0.18e-3
DT_S = 0.01
def _esito(condizione: bool) -> str:
return "OK" if condizione else "FALLITO"
def _campo_nullo(mesh: dict) -> np.ndarray:
return np.zeros(mesh["n_nodi"])
def verifica_matrici(mesh: dict) -> None:
print("=== 1. proprietà delle matrici assemblate ===")
C = asm.assembla_capacita(mesh, MATERIALE, SPESSORE_M)
K = asm.assembla_conduzione(mesh, MATERIALE, SPESSORE_M)
attesa = RHO * CP * SPESSORE_M * mesh["area_totale_m2"]
errore = abs(C.sum() / attesa - 1.0)
print(f" capacità totale {C.sum():.6f} vs rho*cp*s*A {attesa:.6f}"
f" err={errore:.1e} {_esito(errore < 1e-12)}")
# A temperatura costante il flusso deve essere nullo, quindi ogni riga
# della matrice di conduzione somma a zero.
somma_righe = np.abs(np.asarray(K.sum(axis=1))).max()
print(f" max |somma righe K_cond| = {somma_righe:.1e}"
f" {_esito(somma_righe < 1e-9 * K_TERM)}")
asimmetria = abs(K - K.T).max()
print(f" simmetria di K_cond: scarto {asimmetria:.1e}"
f" {_esito(asimmetria < 1e-12 * abs(K).max())}")
autovalori = np.linalg.eigvalsh(K.toarray()[:400, :400])
print(f" blocco campione semidefinito positivo:"
f" autovalore minimo {autovalori.min():.1e}"
f" {_esito(autovalori.min() > -1e-9)}")
def verifica_raffreddamento(mesh: dict) -> None:
# Campo uniforme senza sorgente: deve restare uniforme e decadere verso la
# temperatura ambiente con la costante di tempo rho*cp*s/(h_est+h_int).
# I bordi assiali sono esclusi, altrimenti il decadimento non è uniforme.
print("\n=== 2. raffreddamento convettivo uniforme ===")
aria = {
"temperatura_ambiente_C": 20.0,
"h_esterno_W_m2K": 12.0,
"h_interno_W_m2K": 8.0,
"h_bordi_W_m2K": 0.0,
}
C = asm.assembla_capacita(mesh, MATERIALE, SPESSORE_M)
K = asm.assembla_conduzione(mesh, MATERIALE, SPESSORE_M)
K_conv, f_ambiente = asm.assembla_convezione(mesh, aria, SPESSORE_M)
solutore = sol.costruisci_solutore(C, K + K_conv, DT_S)
sovratemperatura = 200.0
T = np.full(mesh["n_nodi"], aria["temperatura_ambiente_C"] + sovratemperatura)
n_passi = 3000
for _ in range(n_passi):
T = sol.passo_implicito(solutore, T, f_ambiente, _campo_nullo(mesh))
t = n_passi * DT_S
tau = RHO * CP * SPESSORE_M / (aria["h_esterno_W_m2K"] + aria["h_interno_W_m2K"])
attesa = aria["temperatura_ambiente_C"] + sovratemperatura * math.exp(-t / tau)
errore = abs(T.mean() - attesa)
# Eulero implicito è del primo ordine: l'errore atteso è dell'ordine di
# dt/(2*tau) sulla sovratemperatura residua.
tolleranza = 3.0 * (attesa - aria["temperatura_ambiente_C"]) * DT_S / (2 * tau)
print(f" tau = {tau:.3f} s, t = {t:.1f} s")
print(f" numerica {T.mean():.4f} °C vs analitica {attesa:.4f} °C"
f" err={errore:.4f} °C {_esito(errore < tolleranza)}")
dispersione = T.max() - T.min()
print(f" campo ancora uniforme: max-min = {dispersione:.1e} °C"
f" {_esito(dispersione < 1e-9)}")
def _sorgente_prova() -> dict:
return {
"x_inizio_m": 0.5,
"x_fine_m": 0.5,
"offset_y_percorso_m": 0.0,
"velocita_m_s": -1.0,
"numero_sorgenti": 1,
"distanza_sorgenti_m": 0.0,
"sigma_punto_m": 0.012,
"flusso_termico_picco_W_m2": 1e6,
"efficienza_riscaldamento": 1.0,
"zero_dopo_fine": True,
}
def verifica_sorgente(mesh: dict) -> None:
print("\n=== 3. vettore della sorgente e conservazione dell'energia ===")
sorgente = _sorgente_prova()
assemblatore = asm.prepara_assemblatore_sorgente(mesh)
fattore_arco = src.fattore_circonferenziale(
sorgente, RAGGIO_M, assemblatore["arco_gauss_m"]
)
# Con la gaussiana interamente dentro la fascetta la potenza assemblata
# deve valere l'integrale analitico 2*pi*sigma^2*q_max; con la sorgente
# centrata su un bordo assiale deve valerne esattamente la metà.
potenza_analitica = (
sorgente["flusso_termico_picco_W_m2"]
* 2.0 * math.pi * sorgente["sigma_punto_m"] ** 2
)
# x_riferimento(t) = x_sensore + x_inizio + v*t, con x_sensore = 0.05.
for t, descrizione, atteso in [
(0.50, "centrata a metà fascetta", 1.0),
(0.55, "centrata sul bordo x = 0", 0.5),
]:
_, flusso_x = src.profilo_flusso_x_W_m2(
sorgente, 0.05, t, assemblatore["x_gauss_m"]
)
f = asm.assembla_sorgente(assemblatore, flusso_x, fattore_arco)
rapporto = f.sum() / potenza_analitica
print(f" {descrizione:26s}: {f.sum():9.4f} W / {potenza_analitica:.4f} W"
f" = {rapporto:.4f} (atteso {atteso:.1f})"
f" {_esito(abs(rapporto - atteso) < 1e-3)}")
# Senza convezione tutta l'energia iniettata deve finire nel campo.
aria = {
"temperatura_ambiente_C": 20.0,
"h_esterno_W_m2K": 0.0,
"h_interno_W_m2K": 0.0,
"h_bordi_W_m2K": 0.0,
}
C = asm.assembla_capacita(mesh, MATERIALE, SPESSORE_M)
K = asm.assembla_conduzione(mesh, MATERIALE, SPESSORE_M)
_, f_ambiente = asm.assembla_convezione(mesh, aria, SPESSORE_M)
solutore = sol.costruisci_solutore(C, K, DT_S)
T = np.full(mesh["n_nodi"], 20.0)
energia_iniziale = float((C @ T).sum())
energia_iniettata = 0.0
t = 0.0
for _ in range(150):
_, flusso_x = src.profilo_flusso_x_W_m2(
sorgente, 0.05, t, assemblatore["x_gauss_m"]
)
f = asm.assembla_sorgente(assemblatore, flusso_x, fattore_arco)
energia_iniettata += f.sum() * DT_S
T = sol.passo_implicito(solutore, T, f_ambiente, f)
t += DT_S
incremento = float((C @ T).sum()) - energia_iniziale
errore = abs(incremento / energia_iniettata - 1.0)
print(f" iniettata {energia_iniettata:.4f} J, accumulata {incremento:.4f} J"
f" err={errore:.1e} {_esito(errore < 1e-10)}")
def verifica_modi_circonferenziali(mesh: dict) -> None:
# Un modo sin(n*theta) su un cilindro adiabatico decade come
# exp(-alpha * n^2 * t / R^2): confronto diretto con l'analitico.
print("\n=== 4. modi sinusoidali circonferenziali ===")
C = asm.assembla_capacita(mesh, MATERIALE, SPESSORE_M)
K = asm.assembla_conduzione(mesh, MATERIALE, SPESSORE_M)
solutore = sol.costruisci_solutore(C, K, DT_S)
theta = np.tile(mesh["theta_nodi_rad"], mesh["n_nodi_x"])
for n in (1, 2, 4):
T = 100.0 * np.sin(n * theta)
for _ in range(200):
T = sol.passo_implicito(
solutore, T, _campo_nullo(mesh), _campo_nullo(mesh)
)
t = 200 * DT_S
attesa = 100.0 * math.exp(-ALPHA * n**2 / RAGGIO_M**2 * t)
print(f" n={n}: numerica {np.abs(T).max():8.4f} vs analitica {attesa:8.4f}"
f" err={abs(np.abs(T).max() / attesa - 1):.3%}")
def verifica_convergenza() -> None:
print("\n=== 5. convergenza al raffinamento della mesh (modo n=4) ===")
for n_theta in (30, 60, 110, 220):
mesh = costruisci_mesh_cilindrica(LUNGHEZZA_M, RAGGIO_M, 20, n_theta)
C = asm.assembla_capacita(mesh, MATERIALE, SPESSORE_M)
K = asm.assembla_conduzione(mesh, MATERIALE, SPESSORE_M)
solutore = sol.costruisci_solutore(C, K, DT_S)
theta = np.tile(mesh["theta_nodi_rad"], mesh["n_nodi_x"])
T = 100.0 * np.sin(4 * theta)
for _ in range(200):
T = sol.passo_implicito(
solutore, T, _campo_nullo(mesh), _campo_nullo(mesh)
)
attesa = 100.0 * math.exp(-ALPHA * 16 / RAGGIO_M**2 * 200 * DT_S)
print(f" n_elementi_theta={n_theta:4d} (arco {mesh['lato_arco_m']*1e3:5.2f} mm):"
f" err={abs(np.abs(T).max() / attesa - 1):.4%}")
def verifica_periodicita(mesh: dict) -> None:
# Un impulso su theta = 0 deve diffondere in modo identico nei due versi:
# è la prova che la connettività riavvolge davvero la circonferenza.
print("\n=== 6. periodicità circonferenziale ===")
C = asm.assembla_capacita(mesh, MATERIALE, SPESSORE_M)
K = asm.assembla_conduzione(mesh, MATERIALE, SPESSORE_M)
solutore = sol.costruisci_solutore(C, K, DT_S)
T = np.full(mesh["n_nodi"], 20.0)
T[50 * mesh["n_elementi_theta"]] = 500.0
for _ in range(50):
T = sol.passo_implicito(solutore, T, _campo_nullo(mesh), _campo_nullo(mesh))
griglia = campo_su_griglia(mesh, T)
verso_positivo = griglia[50, 1:6]
verso_negativo = griglia[50, -1:-6:-1]
scarto = np.abs(verso_positivo - verso_negativo).max()
print(f" verso theta+ : {np.array2string(verso_positivo, precision=4)}")
print(f" verso theta- : {np.array2string(verso_negativo, precision=4)}")
print(f" scarto massimo = {scarto:.1e} {_esito(scarto < 1e-9)}")
def main() -> None:
mesh = costruisci_mesh_cilindrica(LUNGHEZZA_M, RAGGIO_M, 100, 110)
verifica_matrici(mesh)
verifica_raffreddamento(mesh)
verifica_sorgente(mesh)
verifica_modi_circonferenziali(mesh)
verifica_convergenza()
verifica_periodicita(mesh)
if __name__ == "__main__":
main()