253 lines
9.7 KiB
Python
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()
|