# 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()