OpenFOAM: trasferimento di calore, convezione naturale e numero di Nusselt

di giux CFD e OpenFOAM, in evidenza 55 min di lettura

Decima guida del percorso Imparare OpenFOAM 14 e seconda del modulo «Turbolenza, calore e materia» del corso avanzato, dopo quelle su server e trasferimenti, monitoraggio e convergenza, analisi quantitativa con ParaView, verifica e validazione, campagne parametriche, generazione della mesh, condizioni al contorno e modelli, simulazioni transitorie e turbolenza e modelli RANS. Questa affronta il trasferimento di calore e la convezione naturale: equazione dell’energia, proprietà termofisiche, gravità e p_rgh, condizioni termiche alle pareti, flusso termico con wallHeatFlux, numero di Nusselt e bilancio energetico sul caso ufficiale buoyantCavity. I riquadri di approfondimento e di consiglio pratico si aprono con un clic: contengono tutti gli script del pacchetto, la lettura del codice sorgente di OpenFOAM 14 dove decide il risultato (segni del flusso, Prandtl turbolento, uscita di wallHeatFlux), le stime fisiche del caso, tre script aggiuntivi per le stime, l’estrazione delle potenze e il confronto con i dati sperimentali distribuiti con il tutorial, e le procedure di verifica.

Collana: OpenFOAM su server Linux, operatività da Windows
Edizione: OpenFOAM Foundation 14 · ParaView 5.13.2 · ottobre 2026
Livello: intermedio · Esercitazione: cavità riscaldata, confronto parametrico e bilancio termico

Campo di applicazione. Questa guida introduce la soluzione accoppiata di moto ed energia: non è un tutorial di sola diffusione termica. Si lavora su copie del caso ufficiale fluid/buoyantCavity, con aria trattata come gas ideale e convezione naturale. I numeri nei grafici dimostrativi sono sintetici, non risultati di una simulazione eseguita per questa guida.

Schema della cavità riscaldata
Schema della cavità riscaldata
Videolezione sulla Guida 10 (in italiano), generata con Gemini Notebook a partire da questo articolo.

1. Obiettivi e prerequisiti

Alla fine della guida sarà possibile: identificare le variabili termiche, distinguere condizioni isoterme e adiabatiche, gestire un caso con gravità, leggere i campi T, U e p_rgh, stimare il flusso termico alle pareti e discutere il numero di Nusselt. Il calcolo gira sul server Linux; Windows serve per modificare i file con Notepad++, trasferire con FileZilla (SFTP) e analizzare in ParaView collegato a pvserver.

Sono dati per acquisiti: struttura 0/, constant/, system/; uso di SSH e foamRun; interpretazione di checkMesh, log e convergenza; conoscenze di base di energia e fluidodinamica. Questa guida non richiede VS Code.

Approfondimento · I numeri del caso prima di qualsiasi calcolo

Con i dati del tutorial (aria come gas perfetto, M = 28,96 g/mol, p = 10⁵ Pa, cp = 1004,4 J/(kg K), μ = 1,831·10⁻⁵ Pa s, Pr = 0,705) e le proprietà valutate alla temperatura media fra le pareti, lo script cavity_estimates.py (riquadro successivo) fornisce:

GrandezzaΔT = 19,6 KΔT = 29,6 KLettura
temperatura media297.95 K302.95 Kbase delle proprietà
densità parete calda / fredda1,132 / 1,209 kg/m³1,096 / 1,209 kg/m³gas perfetto, p = 10⁵ Pa
Δρ/ρ6,6 %9,8 %ancora nel campo in cui Boussinesq sarebbe accettabile
RaW = gβΔT W³/(να)8,1·10⁵1,2·10⁶sulla larghezza W = 0,076 m
RaH1,9·10¹⁰2,8·10¹⁰sull’altezza H = 2,18 m: strati limite turbolenti
velocità di galleggiamento √(gβΔT W)0,22 m/s0,27 m/sscala superiore della velocità verticale
potenza di sola conduzione (NuW = 1)7,6 W11,5 Wlimite inferiore fisico
NuW da correlazione (ERH 1982)5,656,37ordine di grandezza atteso
potenza stimata Nu · Qcond≈ 43 W≈ 73 Wda confrontare con wallHeatFlux

Il rapporto d’aspetto H/W = 28,7 e RaW di ordine 10⁶ collocano il problema nel campo di validità della correlazione di Elsherbiny, Raithby e Hollands per cavità verticali (5 ≤ H/W ≤ 110, RaW fino a circa 2·10⁷). L’intestazione dei file sperimentali distribuiti con il tutorial riporta Ra = 8,6·10⁵: la piccola differenza rispetto al valore calcolato dipende dalle proprietà usate per l’aria. La scheda del caso nella banca dati ERCOFTAC (KBwiki, UFR 4-10) riporta per l’esperimento un Nusselt medio di 5,85 con ΔT = 19,6 °C (Ra = 0,86·10⁶) e di 7,57 con ΔT = 39,9 °C (Ra = 1,43·10⁶): la correlazione è entro il 4 % e, con Nu = 5,85, la potenza attesa è circa 44,6 W. Se la potenza calcolata da OpenFOAM si discosta di un fattore 2 da questi valori, prima di ogni interpretazione fisica si cercano errori di segno, di area o di convergenza.

Approfondimento · Script aggiuntivo: cavity_estimates.py

Calcola proprietà al valore medio, numeri di Rayleigh, potenza di sola conduzione, Nusselt da correlazione e un limite superiore dello scambio radiativo fra due lastre parallele grigie (riquadro della sezione 6). Usa solo la libreria standard di Python e funziona su Windows e su Linux:

python cavity_estimates.py
python cavity_estimates.py --Th 317.75
python cavity_estimates.py --emissivity 0.05 0.9
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Order-of-magnitude estimates for the OpenFOAM 14 buoyantCavity case.

Computes ideal-gas properties at the mean temperature, Rayleigh numbers based on
gap width and height, the conduction-only heat rate, the Elsherbiny-Raithby-
Hollands (1982) correlation for vertical cavities and the radiative exchange
between two parallel grey plates. Nothing here is a CFD result: use the numbers
only to judge whether simulated wall powers are plausible.
"""
import argparse
import math

R_UNIVERSAL = 8.314462618  # J/(mol K)
SIGMA = 5.670374419e-8     # W/(m2 K4)


def erh_nusselt(ra, aspect):
    """Elsherbiny, Raithby & Hollands (1982), vertical cavity, isothermal walls.

    Stated range: 5 <= H/W <= 110 and Ra_W up to about 2e7. Returns Nu_W.
    """
    nu1 = 0.0605 * ra ** (1.0 / 3.0)
    nu2 = (1.0 + (0.104 * ra ** 0.293 / (1.0 + (6310.0 / ra) ** 1.36)) ** 3) ** (1.0 / 3.0)
    nu3 = 0.242 * (ra / aspect) ** 0.272
    return max(nu1, nu2, nu3), (nu1, nu2, nu3)


def main():
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument('--Th', type=float, default=307.75, help='hot wall temperature [K]')
    p.add_argument('--Tc', type=float, default=288.15, help='cold wall temperature [K]')
    p.add_argument('--W', type=float, default=0.076, help='gap width [m]')
    p.add_argument('--H', type=float, default=2.18, help='cavity height [m]')
    p.add_argument('--D', type=float, default=0.52, help='cavity depth [m]')
    p.add_argument('--p', type=float, default=1e5, help='pressure [Pa]')
    p.add_argument('--M', type=float, default=28.96, help='molar mass [g/mol]')
    p.add_argument('--Cp', type=float, default=1004.4, help='specific heat [J/(kg K)]')
    p.add_argument('--mu', type=float, default=1.831e-5, help='dynamic viscosity [Pa s]')
    p.add_argument('--Pr', type=float, default=0.705, help='Prandtl number [-]')
    p.add_argument('--g', type=float, default=9.81, help='gravity [m/s2]')
    p.add_argument('--emissivity', type=float, nargs='*', default=[0.05, 0.1, 0.3, 0.9],
                   help='wall emissivities for the radiative bound (both walls equal)')
    a = p.parse_args()
    if not a.Th > a.Tc > 0:
        p.error('Expected Th > Tc > 0 K')

    dT = a.Th - a.Tc
    Tm = 0.5 * (a.Th + a.Tc)
    Rs = R_UNIVERSAL / (a.M * 1e-3)
    rho_m, rho_h, rho_c = (a.p / (Rs * T) for T in (Tm, a.Th, a.Tc))
    k = a.mu * a.Cp / a.Pr
    nu = a.mu / rho_m
    alpha = k / (rho_m * a.Cp)
    beta = 1.0 / Tm
    ra_w = a.g * beta * dT * a.W ** 3 / (nu * alpha)
    ra_h = ra_w * (a.H / a.W) ** 3
    aspect = a.H / a.W
    area = a.H * a.D
    q_cond = k * dT * area / a.W
    nu_erh, parts = erh_nusselt(ra_w, aspect)
    u0 = math.sqrt(a.g * beta * dT * a.W)

    print('Mean temperature            Tm   = %.2f K' % Tm)
    print('Density hot / mean / cold         = %.4f / %.4f / %.4f kg/m3' % (rho_h, rho_m, rho_c))
    print('Relative density difference       = %.2f %%' % (100 * (rho_c - rho_h) / rho_m))
    print('Conductivity k = mu Cp / Pr       = %.5f W/(m K)' % k)
    print('Kinematic viscosity nu (at Tm)    = %.4e m2/s' % nu)
    print('Thermal diffusivity alpha (at Tm) = %.4e m2/s' % alpha)
    print('Expansion coefficient beta = 1/Tm = %.4e 1/K' % beta)
    print('Aspect ratio H/W                  = %.1f' % aspect)
    print('Ra based on W                     = %.3e' % ra_w)
    print('Ra based on H                     = %.3e' % ra_h)
    print('Buoyancy velocity sqrt(g beta dT W) = %.3f m/s' % u0)
    print('Wall area H x D                   = %.4f m2' % area)
    print('Conduction-only power (Nu_W = 1)  = %.2f W' % q_cond)
    print('ERH 1982: Nu1, Nu2, Nu3           = %.2f, %.2f, %.2f' % parts)
    print('ERH 1982: Nu_W = max(...)         = %.2f  ->  Q ~ %.1f W' % (nu_erh, nu_erh * q_cond))
    if not (5 <= aspect <= 110 and ra_w <= 2e7):
        print('WARNING: outside the stated range of the ERH correlation')
    q_bb = SIGMA * (a.Th ** 4 - a.Tc ** 4)
    for eps in a.emissivity:
        if not 0 < eps <= 1:
            p.error('emissivity must be in (0, 1]')
        q_rad = q_bb / (2.0 / eps - 1.0)
        print('Radiation, eps = %.2f: q = %.1f W/m2, Q = %.1f W (infinite parallel plates)' % (eps, q_rad, q_rad * area))


if __name__ == '__main__':
    main()

Le formule della correlazione sono riportate nella funzione erh_nusselt: Nu₁ = 0,0605 Ra1/3, Nu₂ = [1 + (0,104 Ra0,293/(1 + (6310/Ra)1,36))³]1/3, Nu₃ = 0,242 (Ra/A)0,272, Nu = max(Nu₁, Nu₂, Nu₃). Sono correlazioni sperimentali con un’incertezza tipica dell’ordine del 10 %: servono per giudicare la plausibilità, non per validare.

Consiglio pratico · Definire le misure prima di lanciare

Scrivi in un file di testo nella cartella dello studio le grandezze che confronterai e la loro definizione esatta: «Qhot = valore Q della patch hot nel file di wallHeatFlux all’ultima iterazione, positivo se entra nel fluido», «NuW con L = 0,076 m, k = 0,026086 W/(m K), ΔT = Thot − Tcold», «convergenza: variazione di Qhot inferiore allo 0,1 % nelle ultime 100 iterazioni». Fissare definizioni e criteri prima dei risultati evita di scegliere a posteriori quella che conferma l’ipotesi.

2. Cosa cambia rispetto a una simulazione isoterma

In un caso isoterma incomprimibile bastano, per la fisica principale, la quantità di moto e la continuità. In presenza di scambio termico bisogna risolvere anche un’equazione dell’energia e specificare proprietà termofisiche e condizioni al contorno per T.

In forma schematica, per un fluido con proprietà costanti e senza sorgenti:

ρ cp ( ∂T/∂t + u·∇T ) = ∇·( k ∇T )

Questa relazione è illustrativa: OpenFOAM 14 nel caso scelto utilizza una formulazione in entalpia con proprietà termofisiche e gas ideale. Il campo di pressione p_rgh separa la componente idrostatica mediante la formulazione del modello: non va interpretato automaticamente come la pressione assoluta del gas.

Richiamo facoltativo — tre meccanismi. Conduzione: trasferimento per gradiente di temperatura. Convezione: trasporto di energia associato al movimento del fluido. Irraggiamento: scambio elettromagnetico, non incluso nel caso di base. Il moto in questo esperimento è indotto dalla spinta di galleggiamento dovuta alle differenze di densità.

Approfondimento · L’equazione dell’energia che OpenFOAM 14 risolve davvero

Nel modulo fluid (file applications/modules/fluid/thermophysicalPredictor.C del sorgente della versione 14) l’incognita è l’entalpia sensibile h, non la temperatura. L’equazione assemblata è, in forma simbolica:

ddt(rho, h) + div(phi, h) + ddt(rho, K) + div(phi, K) - dp/dt + divq(h)
    = rho*(U & g) + fvModels sources

K = |U|^2/2            energia cinetica per unità di massa
phi                    flusso di massa sulle facce [kg/s]
divq(h)                flusso termico molecolare + turbolento

Con thermo hConst l’entalpia sensibile è h = cp (T − Tstd), con Tstd = 298,15 K: dopo ogni soluzione T viene ricavata da h in thermo.correct(). I termini in K e il lavoro di pressione sono trascurabili qui (velocità di 0,2 m/s, variazioni di pressione di pochi pascal), ma sono presenti: per questo fvSchemes deve contenere div(phi,K). Il termine rho*(U & g) compare solo se il caso ha il campo p_rgh, cioè se è attiva la classe di galleggiamento.

Approfondimento · Che cos’è esattamente p_rgh

La classe buoyancy di OpenFOAM 14 (src/finiteVolume/cfdTools/general/buoyancy) definisce:

p_rgh = p - rho*gh - pRef
gh    = (g & x) - ghRef,      ghRef = -|g| hRef   (hRef opzionale, predefinito 0)

Nel tutorial constant/pRef vale 10⁵ Pa e 0/p parte da 10⁵ Pa, quindi p_rgh parte da zero. Con g = (0 −9,81 0) la differenza idrostatica fra fondo e sommità della cavità è ρ g H ≈ 1,17 · 9,81 · 2,18 ≈ 25 Pa, mentre la pressione dinamica ½ρU² con U ≈ 0,15 m/s vale circa 0,013 Pa: tre ordini di grandezza di meno. Risolvere su p_rgh toglie dall’incognita la parte idrostatica dominante, che altrimenti renderebbe imprecisi gradienti piccoli. Conseguenza pratica: per leggere pressioni fisiche usare p; per confrontare zone alla stessa quota si può usare p_rgh; mai confrontare p_rgh di casi con pRef o hRef diversi.

Approfondimento · Gas perfetto o Boussinesq

Il tutorial usa equationOfState perfectGas: ρ = p M/(R T) con la pressione locale. L’approssimazione di Boussinesq (densità costante salvo nel termine di galleggiamento, ρ ≈ ρ₀[1 − β(T − T₀)]) è considerata accurata quando βΔT ≲ 0,1. Qui βΔT = 0,066 nel caso base e 0,098 nella variante: il caso più caldo è al limite. Con il gas perfetto non serve scegliere β: è implicito (β = 1/T per un gas perfetto). Lasciare il modello del tutorial; se si vuole studiare l’effetto, si confronta in una copia nuova con un modello di densità diverso, documentandolo come variante di modello.

Consiglio pratico · Controllare nel log che cosa è stato selezionato

Le prime righe di logs/foamRun.log riportano i modelli effettivamente costruiti. Prima di guardare i risultati verifica:

cd "$HOME/OpenFOAM/guide10_thermal_study/baseline_19p6K"
grep -n "Selecting" logs/foamRun.log | head -20

Devono comparire, fra le altre, la termodinamica heRhoThermo con perfectGas, il modello kOmegaSST e la riga sul modello di trasporto termico turbolento (riquadro della sezione 4). Una riga mancante o diversa indica che il caso non è quello che si crede di avere.

3. Il caso ufficiale buoyantCavity: geometria e fisica

Il tutorial della Foundation si trova in $FOAM_TUTORIALS/fluid/buoyantCavity. Rappresenta una cavità verticale tridimensionale con aria e pareti laterali a temperatura imposta. La geometria è alta 2,180 m, larga 0,076 m, profonda 0,520 m; la discretizzazione originale ha 35 × 150 × 15 = 78 750 celle. Le pareti superiore, inferiore, anteriore e posteriore sono adiabatiche per il campo termico.

GrandezzaCaso ufficiale / baseVariante proposta
Parete calda hot307,75 K317,75 K
Parete fredda cold288,15 K288,15 K
ΔT tra pareti19,60 K29,60 K
Gravità(0 -9.81 0) m/s²Invariata
Geometria, mesh, modelloIdenticiIdentici

Il tutorial originale considera un esperimento di convezione naturale in cavità alta (Betts e Bokhari, 2000). Utilizziamo gli input distribuiti con la versione 14 e non attribuiamo ai nostri test eventuali risultati sperimentali presenti nelle pubblicazioni.

Approfondimento · La mesh del tutorial vista da vicino

Dal system/blockMeshDict del tutorial (unità in mm): x da 0 a 76, y da 0 a 2180, z da −260 a 260; un solo blocco (35 150 15) con simpleGrading (1 1 1). La parete cold è il piano x = 0, la parete hot il piano x = 76 mm; frontAndBack sono i piani z = ±260 mm, topAndBottom y = 0 e y = 2180 mm. Tutte le patch sono di tipo wall.

DirezioneCellePassoCommento
x (fra le pareti termiche)352,17 mmcentro della prima cella a 1,09 mm dalla parete
y (altezza)15014,5 mmrapporto di forma della cella Δy/Δx ≈ 6,7
z (profondità)1534,7 mmΔz/Δx ≈ 16: celle molto allungate

I dati sperimentali distribuiti con il tutorial (validation/exptData/mt_z0_50_lo.dat) mostrano a metà altezza un salto di circa 4 K entro i primi 3 mm dalla parete fredda: lo strato limite termico ripido è risolto da una o due celle. Stimando lo sforzo di parete dalla prima velocità misurata (−0,081 m/s a 1,2 mm) si ottiene uτ ≈ 0,03 m/s e quindi y⁺ ≈ 2 per il centro della prima cella: la parete è nel sottostrato viscoso, dove le funzioni di parete del tutorial (nutUWallFunction, omegaWallFunction) lavorano in regime a basso Reynolds. La conclusione operativa è che il flusso termico di parete, e quindi Nu, è sensibile alla risoluzione in x: lo studio di mesh della sezione 13 deve infittire soprattutto quella direzione.

Approfondimento · Il tutorial contiene già i dati sperimentali e la procedura di confronto

Oltre ai file del caso, la cartella del tutorial contiene system/sample, che estrae T e U lungo nove linee orizzontali a metà profondità (z = 0) alle quote y/H = 0,1 … 0,9, e validation/exptData, con i profili di temperatura media (mt_z0_*_lo.dat, x in mm, T in °C) e di velocità verticale media (mv_z0_*_lo.dat, x in mm, V in m/s) misurati da Betts e Bokhari. Il file README precisa che il caso considera la minore delle due differenze di temperatura dell’esperimento, 19,6 °C. L’Allrun ufficiale esegue:

runApplication blockMesh
runApplication foamRun
runApplication -s sample foamPostProcess -latestTime -func sample
runApplication validation/createGraphs

Lo script prepare_cases.py copia anche system/sample e validation/, quindi entrambe le copie possono essere confrontate con le misure (riquadri della sezione 10). La variante a 29,6 K invece non ha dati sperimentali corrispondenti: per essa sono possibili soltanto verifiche interne (bilancio, mesh, correlazioni).

Consiglio pratico · Verificare che la copia sia identica all’originale

Dopo la preparazione, un confronto ricorsivo deve mostrare per il caso base nessuna differenza e per la variante una sola riga cambiata:

cd "$HOME/OpenFOAM/guide10_thermal_study"
diff -ru "$FOAM_TUTORIALS/fluid/buoyantCavity" baseline_19p6K && echo "baseline identical"
diff -ru "$FOAM_TUTORIALS/fluid/buoyantCavity" hotter_29p6K

Eseguendo lo script sui file del repository ufficiale di OpenFOAM 14 la seconda differenza è esattamente value uniform 307.75; // 34.6 degC → value uniform 317.75; // 34.6 degC. Nota il commento: lo script cambia il valore ma lascia il commento originale, che nella variante diventa sbagliato (317,75 K corrispondono a 44,6 °C). Correggi il commento a mano nella copia oppure annotalo nel registro dello studio, per non confondere chi leggerà il file.

4. Dove sono le informazioni termiche

FileFunzione
0/TCampo iniziale e condizioni termiche delle pareti
0/UVelocità e pareti senza slittamento
0/p_rghPressione corretta rispetto alla quota gravitazionale
0/alphatDiffusività turbolenta (nel modello termico scelto è un campo con dimensioni dinamiche)
constant/gAccelerazione gravitazionale
constant/physicalPropertiesthermoType, gas ideale, Cp, viscosità dinamica mu, Pr
constant/momentumTransportModello turbolento RAS, nel tutorial kOmegaSST
system/controlDictsolver fluid, tempi e frequenza di scrittura
system/fvSchemes, fvSolutionSchemi e algoritmi numerici

Attenzione: la versione Foundation 14 non richiede di sostituire automaticamente physicalProperties con file chiamati thermophysicalProperties presi da un’altra famiglia/versione del software.

Approfondimento · La riga thermoType decodificata
thermoType
{
    type            heRhoThermo;
    mixture         pureMixture;
    transport       const;
    thermo          hConst;
    equationOfState perfectGas;
    specie          specie;
    energy          sensibleEnthalpy;
}
VoceSignificato
heRhoThermotermodinamica basata sull’energia (he) che calcola direttamente la densità
pureMixtureun solo componente: nessun trasporto di specie
transport constμ e Pr costanti; κ = μ cp/Pr
thermo hConstcp costante, entalpia lineare in T
equationOfState perfectGasρ = p M/(R T)
energy sensibleEnthalpyl’incognita è h, non l’energia interna e

Il file constant/physicalProperties del tutorial contiene anche hf 0 (entalpia di formazione, irrilevante senza reazioni). Qualsiasi variazione di questi termini è una variazione di modello, non di parametro, e va trattata in una copia separata.

Approfondimento · alphat, Prandtl turbolento e un dettaglio del codice da conoscere

Il flusso termico turbolento è modellato con una diffusività turbolenta αt = ρ νt/Prt (dimensioni [kg/(m s)], da cui l’indicazione «dimensioni dinamiche» della tabella). Nel tutorial 0/alphat usa su tutte le pareti compressible::alphatWallFunction con Prt 0.85. Nel volume, invece, il modello di trasporto termico si sceglie nel file constant/thermophysicalTransport, che il tutorial non contiene. Leggendo il sorgente di OpenFOAM 14 (RASThermophysicalTransportModel.C e unityLewisEddyDiffusivity.C) si vede che, in assenza del file, viene selezionato unityLewisEddyDiffusivity con Prt predefinito pari a 1.

Il log lo conferma con la riga «Selecting default RAS thermophysical transport model unityLewisEddyDiffusivity». Il caso usa quindi Prt = 0,85 alle pareti e 1 nel volume. Per renderlo esplicito, o per studiarne la sensibilità, in una copia nuova si crea:

// constant/thermophysicalTransport (FoamFile header: object thermophysicalTransport;)
RAS
{
    model           unityLewisEddyDiffusivity;
    Prt             0.85;
}

e si verifica nel log la riga «Selecting RAS thermophysical transport model unityLewisEddyDiffusivity» seguita dal valore stampato. La differenza fra le due scelte è una misura dell’incertezza di modello sul flusso termico: va riportata, non scelta in base all’accordo con un riferimento.

Approfondimento · Numerica del tutorial: che cosa implica per la lettura dei risultati
ImpostazioneValore nel tutorialConseguenza
ddtSchemessteadyStatecalcolo stazionario: il «tempo» è un contatore di iterazioni
endTime / deltaT1000 / 1al massimo 1000 iterazioni
writeInterval / purgeWrite50 / 3restano solo le ultime 3 scritture
residualControlp_rgh, U, h 1e-4; k, ω 1e-3arresto anticipato se tutti i residui iniziali scendono sotto soglia
rilassamentoU 0.3, h 0.3, p_rgh 0.7convergenza lenta ma robusta
div(phi,*)bounded Gauss limitedLinear 0.2schema TVD del secondo ordine limitato
momentumPredictornotipico dei casi a galleggiamento

Due conseguenze pratiche. Primo: se il calcolo arriva a 1000 iterazioni senza soddisfare residualControl, non è «convergito», è stato fermato; la cosa si riconosce dal log (riquadro della sezione 8). Secondo: con purgeWrite 3 non resta la storia dei campi, quindi l’andamento di Qhot durante le iterazioni si ottiene solo con una function object attiva durante il calcolo (riquadro della sezione 8), non a posteriori.

Estratto del campo T

boundaryField
{
    hot
    {
        type fixedValue;
        value uniform 307.75;
    }
    cold
    {
        type fixedValue;
        value uniform 288.15;
    }
    // Other walls use zeroGradient (adiabatic in the base model).
}

L’estratto mostra solo le due pareti principali; non sostituisce l’intero file originale. I commenti del codice sono in inglese.

5. Modello termofisico: proprietà e unità

Nel file constant/physicalProperties del tutorial sono riportati Cp = 1004.4 J/(kg K), mu = 1.831 × 10⁻⁵ Pa s e Pr = 0.705, oltre alla massa molare dell’aria (28.96). Per questa configurazione con trasporto a Prandtl costante, una stima della conducibilità del fluido è:

k = μ cp / Pr ≈ 0.02608 W m−1 K−1

Non confondere viscosità dinamica mu [Pa s], viscosità cinematica nu [m²/s], diffusività termica molecolare alpha = k/(rho Cp) [m²/s] e campo turbolento alphat: nel caso reale questi oggetti non sono intercambiabili. La densità segue il modello di gas ideale.

Richiamo facoltativo — Prandtl. Il numero di Prandtl confronta diffusività della quantità di moto e diffusività termica: Pr = nu/alpha. A parità di flusso, modificarlo altera la struttura degli strati limite termici.

Approfondimento · Proprietà derivate: i numeri da tenere a portata di mano
GrandezzaFormulaValore a 297,95 K
conducibilità κμ cp / Pr0,026086 W/(m K)
densità ρp M / (R T)1,169 kg/m³
viscosità cinematica νμ / ρ1,566·10⁻⁵ m²/s
diffusività termica ακ / (ρ cp)2,222·10⁻⁵ m²/s
coefficiente di dilatazione β1 / T3,356·10⁻³ 1/K

Il valore 0,02608 della guida è 0,026086 troncato: nei calcoli di Nu usa sempre lo stesso valore in tutti i casi e dichiaralo. Poiché κ = μ cp/Pr con tre costanti, κ non dipende dalla temperatura; ν e α invece cambiano con T attraverso la densità. Per l’aria reale fra 288 e 318 K la conducibilità aumenta di circa il 7 %: il modello a proprietà costanti è una scelta del tutorial, da citare fra le ipotesi.

Consiglio pratico · Controllo delle unità con i dimensionSet

Ogni campo di OpenFOAM dichiara le dimensioni nella riga dimensions. Nel tutorial si leggono nomi simbolici; la corrispondenza con le unità SI è:

FiledimensionsSI
0/T[temperature]K
0/p, 0/p_rgh[pressure]Pa
0/alphat[dynamicDiffusivity]kg/(m s)
0/k[turbulentKineticEnergy]m²/s²
0/omega[turbulentOmega]1/s
0/nut[turbulentViscosity]m²/s
constant/g[acceleration]m/s²

Se una variante richiede di scrivere temperature in °C o pressioni relative, convertile prima: OpenFOAM non converte, e una temperatura di 34,6 scritta al posto di 307,75 viene accettata senza errori come 34,6 K.

6. Condizioni termiche: quale imporre?

  • fixedValue su T modella una parete mantenuta alla temperatura prescritta, non un riscaldatore a potenza fissa.
  • zeroGradient impone gradiente normale nullo: per conduzione di Fourier, parete adiabaticamente isolata, se non intervengono altri termini fisici (ad esempio radiazione).
  • externalTemperature permette di prescrivere potenza o flusso, oppure convezione esterna con h e temperatura ambiente; è una condizione distinta e non va introdotta nel caso base senza riconfigurare il problema.
  • coupledTemperature riguarda interfacce fra regioni, utile per l’analisi termica coniugata fluido-solido.

Esempio puramente orientativo, non sostitutivo del blocco hot utilizzato:

// Example of external convection to an ambient reservoir.
wall
{
    type externalTemperature;
    Ta   constant 300;
    h    constant 10;
    value uniform 310;
}

La sintassi è descritta nella guida ufficiale delle condizioni termiche per OpenFOAM 14. L’esempio presuppone una parete esterna e non rappresenta la cavità isoterma dell’esercitazione.

Approfondimento · Che cosa significa fisicamente una parete fixedValue

Imporre T su una parete equivale a supporre una parete di capacità termica infinita, mantenuta isoterma qualunque sia il flusso che riceve. È una buona idealizzazione quando la parete reale è un buon conduttore termostatato (piastre metalliche con circuito di fluido, come negli esperimenti su cavità) e il suo numero di Biot verso il fluido è piccolo. Il flusso termico è allora un risultato del calcolo, distribuito in modo non uniforme sull’altezza: elevato dove lo strato limite inizia (in basso sulla parete calda, in alto sulla fredda) e minore dove è già sviluppato.

Approfondimento · Irraggiamento: un ordine di grandezza che non si può ignorare

Il caso di base non include l’irraggiamento. Per due lastre parallele grigie con la stessa emissività ε il flusso netto è q = σ(Th⁴ − Tc⁴)/(2/ε − 1). Con le temperature del tutorial cavity_estimates.py dà:

εq [W/m²]Q su 1,1336 m² [W]rispetto a ≈ 43 W convettivi
0,053,03,4≈ 8 %
0,106,27,0≈ 16 %
0,3020,823,5≈ 55 %
0,9096,3109≈ 2,5 volte

Le stime sono per lastre infinite e trascurano l’assorbimento dell’aria, ma dicono una cosa chiara: con superfici non lucidate lo scambio radiativo fra le pareti è dello stesso ordine o maggiore di quello convettivo. Nella simulazione non entra nel bilancio del fluido (l’aria è trasparente), ma modifica le temperature delle pareti adiabatiche e quindi la stratificazione. Prima di confrontare Nu con un esperimento verifica come la fonte ha trattato l’irraggiamento e quale emissività avevano le superfici.

Consiglio pratico · Sintassi di externalTemperature in OpenFOAM 14

Nel sorgente di OpenFOAM 14 la condizione externalTemperature accetta, in alternativa: potenza totale Q [W], flusso q [W/m²] oppure coefficiente h [W/(m² K)] con temperatura ambiente Ta [K]; se oltre a Q o q si danno anche h e Ta, i flussi si sommano. Sono opzionali emissivity (irraggiamento verso l’ambiente) e relaxation. Ta, Q e q sono funzioni del tempo (per esempio constant 300), mentre h è un campo sulla patch: nei tutorial della versione 14 è sempre scritto come h uniform 8;. Se la riga h constant 10; dell’esempio produce un errore di lettura, usa:

wall
{
    type            externalTemperature;
    Ta              constant 300;
    h               uniform 10;
    value           uniform 310;
}

Esempi completi e funzionanti sono nei tutorial ufficiali fluid/roomHeating e multiRegion/CHT/hotWire (h = 8) e multiRegion/CHT/vacuumFurnace (h = 100): leggerli con grep -A6 externalTemperature $FOAM_TUTORIALS/fluid/roomHeating/0/T.

Approfondimento · Le pareti adiabatiche non sono un dettaglio

Le pareti frontAndBack e topAndBottom hanno zeroGradient su T: nessun flusso le attraversa. In un esperimento reale le pareti «isolate» scambiano sempre un poco di calore, conducono lungo il proprio spessore e irraggiano verso le pareti termiche. Le differenze fra calcolo e misura vicino al fondo e alla sommità della cavità (y/H = 0,1 e 0,9) dipendono spesso da queste condizioni più che dal modello di turbolenza. Nel confronto con i dati della sezione 10 è utile separare le quote centrali da quelle estreme.

7. Preparare due esperienze senza modificare il tutorial

Il pacchetto contiene scripts/prepare_cases.py, che legge il tutorial ufficiale installato e crea due directory nuove e indipendenti. Il programma verifica la temperatura della parete fredda e lo schema atteso della parete calda; se la directory di destinazione esiste già, si ferma senza cancellarla.

Da PowerShell ci si collega al server:

ssh USER@SERVER_IP

Dopo aver trasferito la cartella scripts/ con FileZilla, nella shell Linux con OpenFOAM 14 già caricato:

foamVersion
python3 scripts/prepare_cases.py \
  --source "$FOAM_TUTORIALS/fluid/buoyantCavity" \
  --dest "$HOME/OpenFOAM/guide10_thermal_study"
find "$HOME/OpenFOAM/guide10_thermal_study" -maxdepth 2 -type d

Le cartelle risultanti sono baseline_19p6K e hotter_29p6K. Non copiare l’esercitazione dentro la directory dell’installazione OpenFOAM.

Preparazione e analisi della campagna
Preparazione e analisi della campagna
Approfondimento · Script del pacchetto: prepare_cases.py
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Create non-destructive OpenFOAM 14 buoyantCavity thermal study cases."""
import argparse
import os
from pathlib import Path
import re
import shutil
import sys


def update_hot_temperature(text: str, new_temp: float) -> str:
    # This parser is intentionally constrained to the verified OF14 tutorial input.
    pattern = r'(\bhot\s*\{[^{}]*?\bvalue\s+uniform\s+)([0-9.+eE-]+)(\s*;)'
    matches = list(re.finditer(pattern, text, re.DOTALL))
    if len(matches) != 1 or float(matches[0].group(2)) != 307.75:
        raise ValueError('Unexpected hot patch: tutorial version differs from verified OF14 source')
    return re.sub(pattern, lambda m: m.group(1) + format(new_temp, '.2f') + m.group(3), text, count=1, flags=re.DOTALL)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--source', type=Path, default=Path(os.environ.get('FOAM_TUTORIALS', '')) / 'fluid/buoyantCavity', help='Official OF14 buoyantCavity source directory')
    parser.add_argument('--dest', type=Path, required=True, help='New, nonexisting study directory')
    args = parser.parse_args()
    source = args.source.expanduser().resolve()
    dest = args.dest.expanduser().resolve()
    if not (source/'0/T').is_file() or not (source/'system/controlDict').is_file():
        parser.error('Official tutorial source not found: load OpenFOAM 14 shell first')
    if dest.exists():
        parser.error(f'Destination already exists; refusing to overwrite: {dest}')
    temp_data = (source/'0/T').read_text(encoding='utf-8')
    if 'uniform 288.15' not in temp_data:
        parser.error('Unexpected cold-wall temperature in source tutorial')
    # Validate before creating anything.
    baseline_text = update_hot_temperature(temp_data, 307.75)
    hot_text = update_hot_temperature(temp_data, 317.75)
    dest.mkdir(parents=True)
    for name, content in (('baseline_19p6K', baseline_text), ('hotter_29p6K', hot_text)):
        target = dest/name
        shutil.copytree(source, target, ignore=shutil.ignore_patterns('processor*', 'postProcessing', 'log.*'))
        (target/'0/T').write_text(content, encoding='utf-8')
    print(f'Created two independent cases in {dest}')
    print('Baseline: Th=307.75 K, Tc=288.15 K, dT=19.60 K')
    print('Hotter:   Th=317.75 K, Tc=288.15 K, dT=29.60 K')


if __name__ == '__main__':
    try:
        main()
    except (ValueError, OSError) as exc:
        sys.exit(f'ERROR: {exc}')
  • L’espressione regolare cerca, dentro il blocco hot, la riga value uniform …;; se le occorrenze non sono esattamente una o il valore non è 307,75, si ferma: protegge da tutorial di versioni diverse.
  • Le due versioni del file 0/T vengono costruite e validate prima di creare qualsiasi cartella.
  • shutil.ignore_patterns('processor*', 'postProcessing', 'log.*') evita di copiare risultati di eventuali esecuzioni precedenti nella cartella sorgente.
  • Se la destinazione esiste, parser.error termina con codice 2 senza toccare nulla.
Consiglio pratico · Una terza copia per le prove

Le function object di monitoraggio (sezione 8), il file di Prt (sezione 4) o una mesh più fitta (sezione 13) vanno inseriti in copie aggiuntive, non nelle due di riferimento. Lo script accetta qualunque destinazione nuova:

python3 scripts/prepare_cases.py \
  --source "$FOAM_TUTORIALS/fluid/buoyantCavity" \
  --dest "$HOME/OpenFOAM/guide10_thermal_study_monitor"
# then edit only baseline_19p6K inside the new folder, before running it

Annota nel registro dello studio quale copia contiene quale modifica e verificala con diff -ru rispetto al tutorial.

8. Esecuzione e controllo dei log

Per ogni caso appena preparato:

bash scripts/run_case.sh \
  "$HOME/OpenFOAM/guide10_thermal_study/baseline_19p6K"

Ripetere con hotter_29p6K solo dopo aver verificato il primo calcolo. Lo script esegue blockMesh, checkMesh, foamRun, archiviando i log in logs/ e provando il post-processing wallHeatFlux. Non elimina dati e rifiuta l’esecuzione su un caso già mesciato o con log esistenti; per riprovare creare una nuova copia o procedere manualmente dopo un controllo.

Per simulazioni di lunga durata usare tmux (Guida 1). In un altro terminale:

tail -n 35 "$HOME/OpenFOAM/guide10_thermal_study/baseline_19p6K/logs/foamRun.log"

Controllare residui, stabilità del campo T, eventuali errori sui modelli termofisici e chiusura del solver. L’assenza di errori numerici non dimostra un bilancio energetico corretto.

Approfondimento · Script del pacchetto: run_case.sh
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-or-later
# Run an untouched study case exactly once; preserve all existing outputs.
set -euo pipefail
if [[ $# -ne 1 ]]; then
  echo 'Usage: run_case.sh /absolute/path/to/new/case' >&2
  exit 2
fi
CASE_DIR=$(realpath -- "$1")
[[ -f "$CASE_DIR/system/controlDict" ]] || { echo 'Invalid case' >&2; exit 2; }
command -v foamRun >/dev/null || { echo 'Load the OpenFOAM 14 environment first' >&2; exit 2; }
if [[ -e "$CASE_DIR/logs" || -e "$CASE_DIR/constant/polyMesh" ]]; then
  echo 'Case already has logs or a mesh. Refusing to rerun or overwrite it.' >&2
  exit 3
fi
mkdir "$CASE_DIR/logs"
cd "$CASE_DIR"
blockMesh > logs/blockMesh.log 2>&1
checkMesh > logs/checkMesh.log 2>&1
foamRun > logs/foamRun.log 2>&1
# Optional post-process. Its failure does not change the solver success state.
if command -v foamPostProcess >/dev/null; then
  if ! foamPostProcess -latestTime -func wallHeatFlux > logs/wallHeatFlux.log 2>&1; then
    echo 'Warning: wallHeatFlux post-processing failed; review logs/wallHeatFlux.log' >&2
  fi
fi
echo "Solver completed. Logs are in $CASE_DIR/logs"
  • set -euo pipefail: lo script si ferma al primo comando fallito, su variabili non definite e su errori in una pipeline; quindi se checkMesh fallisce, foamRun non parte.
  • Il controllo su logs e constant/polyMesh impedisce di rieseguire un caso già lanciato (codice di uscita 3).
  • Il post-processing wallHeatFlux è dentro un if !: il suo eventuale fallimento produce solo un avviso e non annulla il successo del solver.
  • Lo script non esegue sample: per il confronto con i dati sperimentali vedi la sezione 10.
Consiglio pratico · Convergito o fermato? Leggerlo nel log
cd "$HOME/OpenFOAM/guide10_thermal_study/baseline_19p6K"
grep -c "^Time = " logs/foamRun.log            # iterazioni eseguite
grep -n -i "converged" logs/foamRun.log | tail -3
grep "Solving for h" logs/foamRun.log | tail -3  # residui di h
tail -n 5 logs/foamRun.log                      # deve terminare con End

Se il numero di iterazioni è 1000 e non compare alcun messaggio di convergenza, il calcolo è stato fermato da endTime. Per estrarre l’andamento dei residui senza disturbare la cartella logs/ del caso, esegui foamLog da una cartella separata, perché crea a sua volta una sottocartella logs nella directory corrente:

mkdir "$HOME/OpenFOAM/guide10_thermal_study/foamLog_baseline"
cd "$HOME/OpenFOAM/guide10_thermal_study/foamLog_baseline"
foamLog ../baseline_19p6K/logs/foamRun.log
ls logs/   # h_0, p_rgh_0, Ux_0 ... una colonna per iterazione
Approfondimento · Seguire Q durante il calcolo con un file system/functions

OpenFOAM 14 legge automaticamente il file system/functions, se presente, oltre all’eventuale voce functions di controlDict. In una copia nuova, prima di run_case.sh, salva questo file come system/functions:

/*--------------------------------*- C++ -*----------------------------------*\
  Guide 10 - monitoring functions for a NEW copy of buoyantCavity.
  Save as <case>/system/functions before running the solver.
\*---------------------------------------------------------------------------*/
FoamFile
{
    format      ascii;
    class       dictionary;
    location    "system";
    object      functions;
}

// Integrated wall heat flow Q [W] on the hot and cold walls every 10 iterations.
// Positive values: heat entering the fluid. No field is written (writeFields false).
wallHeatFluxMonitor
{
    type            wallHeatFlux;
    libs            ("libfieldFunctionObjects.so");
    patches         (hot cold);
    executeControl  timeStep;
    executeInterval 10;
    writeControl    timeStep;
    writeInterval   10;
    writeFields     false;
    log             yes;
}

// Initial residuals of the solved fields, one line per iteration.
#includeFunc residuals(p_rgh, U, h, k, omega)

// Maximum velocity magnitude: a cheap indicator of the circulation strength.
#includeFunc cellMaxMag(name=UmaxMag, U)

// ************************************************************************* //

La sintassi è quella dei tutorial ufficiali della versione 14 (fluid/concentricCylinders, multiRegion/CHT/wallBoiling); writeFields false evita di scrivere il campo wallHeatFlux a ogni esecuzione. Il risultato è un file postProcessing/wallHeatFluxMonitor/0/wallHeatFlux.dat con una riga per patch ogni 10 iterazioni, leggibile con whf_extract.py (sezione 9): l’andamento di Qhot e Qcold è il criterio di convergenza fisicamente significativo. Nel log, all’avvio, deve comparire la creazione delle tre function object; se una manca, controlla l’intestazione del file.

9. Flusso termico alle pareti con wallHeatFlux

OpenFOAM 14 dispone della function object wallHeatFlux, che calcola un campo di flusso termico sulle patch di parete. Nel caso ufficiale è opportuno iniziare con l’opzione non invasiva:

foamPostProcess -case \
  "$HOME/OpenFOAM/guide10_thermal_study/baseline_19p6K" \
  -latestTime -func wallHeatFlux

Il comando è presente anche nello script run_case.sh, ma il suo eventuale fallimento viene segnalato separatamente e non falsifica l’esito del solver. Se la funzione necessita di una configurazione esplicita o il proprio build utilizza un’organizzazione diversa, consultare foamInfo wallHeatFlux e la documentazione di classe della versione 14.

Per trasformare il flusso specifico [W/m²] in potenza [W] occorre integrare sulla superficie: Q = ∫_A q'' dA. Prima dell’integrazione verificare segno, orientazione delle normali, componenti e associazione dei dati (celle/facce). Evitare di trattare una media aritmetica dei valori come un’integrazione superficiale.

Approfondimento · Che cosa calcola wallHeatFlux, letto nel sorgente

Dal file src/functionObjects/field/wallHeatFlux/wallHeatFlux.C di OpenFOAM 14:

  • il campo vale meno il flusso termico del modello di trasporto sulla faccia di bordo, wallHeatFlux = −q, dove q è positivo nel verso della normale uscente dal dominio. Ne segue che wallHeatFlux > 0 significa calore che entra nel fluido attraverso la parete;
  • se esiste un campo radiativo qr viene sottratto anch’esso, quindi il risultato include l’irraggiamento quando è modellato;
  • per ogni patch vengono scritti min, max, la potenza integrata Q = Σ |S<sub>f</sub>| q [W] e la media d’area q = Q/Σ|S<sub>f</sub>| [W/m²], sia nel log sia nel file postProcessing/<nome>/<tempo>/wallHeatFlux.dat;
  • senza voce patches vengono elaborate tutte le patch di tipo wall.

Quindi l’integrazione della sezione 9 è già fatta da OpenFOAM, sulla mesh effettiva e con le aree esatte delle facce: Qhot è il valore Q della patch hot, Qcold è il valore Q della patch cold cambiato di segno. Le patch adiabatiche, con zeroGradient, devono dare Q = 0. ParaView resta utile come verifica indipendente e per la distribuzione locale.

Consiglio pratico · Se wallHeatFlux.log riporta un errore: aggiungere -solver fluid

Nel sorgente di foamPostProcess (versione 14) i modelli fisici vengono costruiti solo se si indica il solutore con -solver; altrimenti l’utilità legge soltanto i campi. wallHeatFlux ha bisogno del modello di trasporto termico e, se non lo trova, termina con «Unable to find thermophysicalTransportModel». È proprio il caso in cui lo script run_case.sh stampa l’avviso senza compromettere il calcolo. Controlla e, se serve, ripeti con il solutore esplicito e un log con nome nuovo:

cd "$HOME/OpenFOAM/guide10_thermal_study/baseline_19p6K"
grep -i "error\|unable" logs/wallHeatFlux.log
foamPostProcess -solver fluid -latestTime -func wallHeatFlux > logs/wallHeatFlux_solver.log 2>&1
grep "for patch" logs/wallHeatFlux_solver.log

Nell’uscita cerca le righe «min, max, Q [W], q [W/m^2] for patch hot = …» e «… for patch cold = …»: il terzo numero è la potenza integrata. Lo stesso vale per yPlus e per ogni function object che richiede il modello di turbolenza o di trasporto termico; la function object sample invece legge solo T e U e funziona anche senza -solver, come nell’Allrun ufficiale.

Approfondimento · Script aggiuntivo: whf_extract.py

Legge uno o più file wallHeatFlux.dat, converte i segni nella convenzione della guida, mostra la somma con segno su tutte le patch (a convergenza deve tendere a zero), la deriva di Qhot nelle ultime registrazioni e, a richiesta, scrive la riga di misura per thermal_balance.py:

cd "$HOME/OpenFOAM/guide10_thermal_study/baseline_19p6K"
find postProcessing -name wallHeatFlux.dat
python3 whf_extract.py postProcessing/wallHeatFlux/*/wallHeatFlux.dat \
  --case baseline_19p6K --Th 307.75 --Tc 288.15 \
  --output ../baseline_measurements.csv
python3 scripts/thermal_balance.py ../baseline_measurements.csv ../baseline_results.csv
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Read OpenFOAM 14 wallHeatFlux.dat files and prepare a thermal_balance.py row.

The wallHeatFlux function object writes, for every processed time and wall
patch: Time, patch, min [W/m2], max [W/m2], Q [W], q [W/m2]. In OpenFOAM 14 the
field is minus the boundary heat flux along the outward normal, so a positive Q
means heat entering the fluid through that patch. This tool converts the raw
signed values to the positive Q_hot / Q_cold convention of Guide 10, reports the
sum over all wall patches and, when several times are present, the drift of Q
over the last records. It never overwrites an existing output file.
"""
import argparse
import csv
from collections import OrderedDict
from pathlib import Path
import sys


def read_dat(paths):
    data = OrderedDict()  # time -> {patch: Q}
    for path in paths:
        with open(path, encoding='utf-8') as f:
            for number, line in enumerate(f, 1):
                s = line.strip()
                if not s or s.startswith('#'):
                    continue
                parts = s.split()
                if len(parts) < 6:
                    raise ValueError(f'{path}:{number}: expected 6 columns, found {len(parts)}')
                t, patch, q_total = float(parts[0]), parts[1], float(parts[4])
                data.setdefault(t, {})[patch] = q_total
    if not data:
        raise ValueError('No data rows found')
    return OrderedDict(sorted(data.items()))


def main():
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument('dat', nargs='+', type=Path, help='wallHeatFlux.dat file(s)')
    p.add_argument('--case', required=True, help='case label for the output row')
    p.add_argument('--hot', default='hot', help='hot patch name')
    p.add_argument('--cold', default='cold', help='cold patch name')
    p.add_argument('--Th', type=float, required=True, help='hot wall temperature [K]')
    p.add_argument('--Tc', type=float, required=True, help='cold wall temperature [K]')
    p.add_argument('--area-hot', type=float, default=1.1336, help='hot wall area [m2]')
    p.add_argument('--area-cold', type=float, default=1.1336, help='cold wall area [m2]')
    p.add_argument('--L', type=float, default=0.076, help='characteristic length [m]')
    p.add_argument('--k', type=float, default=0.026086, help='fluid conductivity [W/(m K)]')
    p.add_argument('--last', type=int, default=10, help='records used for the drift check')
    p.add_argument('--output', type=Path, help='NEW csv for thermal_balance.py')
    a = p.parse_args()

    data = read_dat(a.dat)
    t_last, row = next(reversed(data.items()))
    for name in (a.hot, a.cold):
        if name not in row:
            p.error(f'patch {name} not found at time {t_last}; available: {", ".join(row)}')
    q_hot, q_cold = row[a.hot], -row[a.cold]
    print(f'Last time: {t_last:g}  ({len(data)} time records)')
    for name, value in row.items():
        print(f'  raw Q[{name}] = {value:+.6g} W')
    print(f'Q_hot  = {q_hot:.6g} W   (heat entering the fluid)')
    print(f'Q_cold = {q_cold:.6g} W   (heat leaving the fluid)')
    print(f'Sum of signed Q over all wall patches = {sum(row.values()):+.6g} W')
    if q_hot <= 0 or q_cold <= 0:
        print('WARNING: unexpected sign; check patch names and the sign convention before continuing')
    if len(data) > 1:
        tail = list(data.items())[-max(2, a.last):]
        series = [r[a.hot] for _, r in tail if a.hot in r]
        ref = max(abs(v) for v in series)
        drift = (max(series) - min(series)) / ref * 100 if ref else float('nan')
        print(f'Drift of Q[{a.hot}] over the last {len(series)} records: {drift:.3g} %')
    if a.output:
        if a.output.exists():
            p.error('Output already exists; refusing to overwrite it')
        with a.output.open('x', newline='', encoding='utf-8') as f:
            w = csv.writer(f)
            w.writerow(['case', 'Th_K', 'Tc_K', 'Ahot_m2', 'Acold_m2', 'Qhot_W', 'Qcold_W', 'L_m', 'k_W_mK'])
            w.writerow([a.case, a.Th, a.Tc, a.area_hot, a.area_cold, f'{q_hot:.6g}', f'{q_cold:.6g}', a.L, a.k])
        print('Measurement row written to', a.output)


if __name__ == '__main__':
    try:
        main()
    except (OSError, ValueError) as exc:
        sys.exit(f'ERROR: {exc}')

Provato su un file costruito con il formato scritto dal sorgente (colonne Time, patch, min, max, Q, q): con Q(hot) = 36,5 W e Q(cold) = −35,8 W restituisce Qhot = 36,5 W, Qcold = 35,8 W, somma +0,7 W, e la riga prodotta dà in thermal_balance.py Nuhot = 4,786 con k = 0,026086. Rifiuta un file di uscita esistente e una patch inesistente.

Approfondimento · Il flusso di parete dipende dal modello di parete

Sulla faccia di parete il flusso è calcolato come κeff ∂T/∂n con κeff = κ + cp αt,w, dove αt,w è dato da alphatWallFunction con Prt = 0,85, e il gradiente normale è valutato fra il centro della prima cella e la parete. Se y⁺ della prima cella è dell’ordine di 1–2, νt alla parete è quasi nullo e il flusso è essenzialmente molecolare; se la mesh è più grossolana, la funzione di parete sostituisce la parte mancante del profilo con una legge universale, che per la convezione naturale è meno affidabile che per gli strati limite forzati. Il valore di Nu va sempre riportato insieme a y⁺ della prima cella e alla mesh usata.

Consiglio pratico · Calcolare y⁺ alle pareti termiche
cd "$HOME/OpenFOAM/guide10_thermal_study/baseline_19p6K"
foamPostProcess -solver fluid -latestTime -func yPlus > logs/yPlus.log 2>&1
grep -A4 "patch hot\|patch cold" logs/yPlus.log

La function object yPlus stampa minimo, massimo e media per ogni patch di parete e scrive il campo yPlus visualizzabile in ParaView lungo l’altezza. Il nome del log è nuovo, quindi non sovrascrive nulla; se esiste già, scegline un altro.

10. Misure con ParaView da Windows

Aprire il caso tramite il pvserver del server Linux (procedura delle Guide 1 e 3). Caricare i campi T, U e, se esportato, wallHeatFlux.

  1. Slice a metà profondità: colorare per T e osservare stratificazione e strati limite.
  2. Glyph o Stream Tracer su U: verificare il ricircolo da convezione naturale; scegliere punti di semina e scala coerenti.
  3. Extract Surface o selezione delle patch hot/cold, poi Integrate Variables sul campo di flusso: ottenere potenze nelle unità corrette.
  4. Save Data in CSV per esportare i risultati finali. Annotare il tempo, le superfici e la convenzione di segno.

Non usare automaticamente Gradient calcolato sui soli valori di cella come sostituto del flusso numerico di parete; vicino al bordo può differire dalle condizioni discretizzate.

Approfondimento · Integrate Variables: le impostazioni che cambiano il risultato
  • Seleziona nel lettore OpenFOAM solo la patch (nell’elenco «Mesh Regions» le patch compaiono con il prefisso patch/), oppure isolala con Extract Block: integrare l’intero volume produce un numero senza significato.
  • Integra l’array di celle wallHeatFlux: il lettore crea per default anche valori ai punti interpolati, che sul bordo non coincidono con quelli di faccia.
  • Lascia disattivata l’opzione «Divide Cell Data By Volume»: con l’opzione attiva il filtro restituisce una media, non un integrale.
  • Il filtro riporta anche Area: per la parete calda deve valere 1,1336 m². Un valore diverso rivela una selezione sbagliata.
  • Il valore integrato deve coincidere, a meno di arrotondamenti, con il Q stampato da wallHeatFlux per la stessa patch e lo stesso tempo.
Approfondimento · Script aggiuntivo: compare_expt.py

Dopo aver generato i profili con la function object sample del tutorial, lo script confronta T e velocità verticale con i dati di Betts e Bokhari interpolando il calcolo nelle ascisse sperimentali e riportando scarto medio, RMS e massimo per ogni quota:

cd "$HOME/OpenFOAM/guide10_thermal_study/baseline_19p6K"
foamPostProcess -latestTime -func sample > logs/sample.log 2>&1
ls postProcessing/sample/*/
python3 compare_expt.py . --output ../baseline_vs_expt.csv
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Compare buoyantCavity sample profiles with the Betts & Bokhari data.

Reads postProcessing/sample/<latest time>/y0.<i>.xy written by
'foamPostProcess -latestTime -func sample' (columns: x T Ux Uy Uz, as assumed by
the tutorial's validation/createGraphs script) and the experimental files
validation/exptData/mt_z0_<i>0_lo.dat (x [mm], T [degC]) and
mv_z0_<i>0_lo.dat (x [mm], Uy [m/s]). The simulated profile is linearly
interpolated at each experimental abscissa. Prints mean, RMS and maximum
deviations per height and optionally writes them to a NEW CSV file.
"""
import argparse
import csv
import math
from pathlib import Path
import sys


def read_columns(path):
    rows = []
    with open(path, encoding='utf-8') as f:
        for line in f:
            s = line.strip()
            if not s or s.startswith('#'):
                continue
            rows.append([float(v) for v in s.split()])
    if not rows:
        raise ValueError(f'No numeric data in {path}')
    return rows


def interp(xs, ys, x):
    if x < xs[0] or x > xs[-1]:
        return None
    for i in range(1, len(xs)):
        if x <= xs[i]:
            x0, x1, y0, y1 = xs[i - 1], xs[i], ys[i - 1], ys[i]
            return y0 if x1 == x0 else y0 + (y1 - y0) * (x - x0) / (x1 - x0)
    return ys[-1]


def stats(diffs):
    n = len(diffs)
    mean = sum(diffs) / n
    rms = math.sqrt(sum(d * d for d in diffs) / n)
    worst = max(diffs, key=abs)
    return n, mean, rms, worst


def latest_time_dir(root):
    dirs = []
    for d in root.iterdir():
        try:
            dirs.append((float(d.name), d))
        except ValueError:
            continue
    if not dirs:
        raise ValueError(f'No time directories in {root}')
    return max(dirs)[1]


def main():
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument('case', type=Path, help='case directory containing postProcessing/sample')
    p.add_argument('--expt', type=Path, help='experimental data directory (default: <case>/validation/exptData)')
    p.add_argument('--output', type=Path, help='NEW csv summary file')
    a = p.parse_args()
    expt = a.expt or a.case / 'validation' / 'exptData'
    sdir = latest_time_dir(a.case / 'postProcessing' / 'sample')
    print('Simulation profiles from', sdir)
    if a.output and a.output.exists():
        p.error('Output already exists; refusing to overwrite it')
    results = []
    for i in range(1, 10):
        sim_file = sdir / f'y0.{i}.xy'
        if not sim_file.is_file():
            continue
        sim = sorted(read_columns(sim_file))
        xs = [r[0] for r in sim]
        for kind, col, fname, conv, unit in (('T', 1, f'mt_z0_{i}0_lo.dat', 273.15, 'K'),
                                              ('Uy', 3, f'mv_z0_{i}0_lo.dat', 0.0, 'm/s')):
            efile = expt / fname
            if not efile.is_file():
                continue
            ys = [r[col] for r in sim]
            diffs = []
            for row in read_columns(efile):
                value = interp(xs, ys, row[0] / 1000.0)
                if value is not None:
                    diffs.append(value - (row[1] + conv))
            if not diffs:
                continue
            n, mean, rms, worst = stats(diffs)
            results.append(dict(y_over_H=f'0.{i}', quantity=kind, unit=unit, points=n,
                                mean_dev=f'{mean:.4g}', rms_dev=f'{rms:.4g}', max_abs_dev=f'{worst:.4g}'))
            print(f'y/H=0.{i}  {kind:2s}: n={n:2d}  mean={mean:+.4g} {unit}  rms={rms:.4g} {unit}  worst={worst:+.4g} {unit}')
    if not results:
        p.error('No matching simulation/experiment pairs found')
    if a.output:
        with a.output.open('x', newline='', encoding='utf-8') as f:
            w = csv.DictWriter(f, fieldnames=list(results[0]))
            w.writeheader()
            w.writerows(results)
        print('Summary written to', a.output)
    print('Deviation = simulation - experiment. The data come from the tutorial; cite the original publication.')


if __name__ == '__main__':
    try:
        main()
    except (OSError, ValueError) as exc:
        sys.exit(f'ERROR: {exc}')

Lo script assume il formato a colonne «x T Ux Uy Uz» dei file y0.N.xy, lo stesso usato da validation/createGraphs del tutorial (colonna 4 per Uy): se ls mostra nomi o colonne diversi, adegua gli indici prima di usarlo. È stato provato su profili costruiti ad arte e sui file sperimentali reali del tutorial. Una deviazione di pochi decimi di kelvin al centro e di 1–2 K vicino alle pareti è tipica dei calcoli RANS su questo caso; scarti di molti kelvin indicano un problema.

Consiglio pratico · Profili confrontabili in ParaView
  • Per confrontare a vista con i dati sperimentali usa Plot Over Line da (0, y, 0) a (0,076, y, 0) con y = 1,09 m (y/H = 0,5): sono le stesse coordinate della linea y0.5 di system/sample.
  • Imposta la stessa scala di colore (per esempio 288–318 K) per entrambi i casi e salvala come preset: confronti fra immagini con scale diverse non hanno valore.
  • Per la stratificazione, un Plot Over Line verticale al centro (x = 0,038 m, z = 0) mostra l’andamento di T con l’altezza: nelle cavità alte il nucleo è stratificato in modo approssimativamente lineare.
  • Salva lo stato con File → Save State (.pvsm) accanto ai CSV esportati.

11. Dal flusso termico al numero di Nusselt

Definiamo Q_hot > 0 come potenza che entra nel fluido dalla parete calda e Q_cold > 0 come potenza che esce dal fluido verso quella fredda. L’area di ciascuna delle due pareti principali, nella geometria originale, è A = 2.18 × 0.52 = 1.1336 m².

Con lunghezza caratteristica dichiarata come distanza fra le pareti L = 0.076 m:

Nuhot = ( Qhot / Ahot ) L / [ k ( Thot − Tcold ) ]

Il bilancio relativo si esprime, per due potenze positive:

εE = 100 · | Qhot − Qcold | / max( Qhot, Qcold ) %

Importante: nella letteratura sulle cavità alte si trovano anche altre convenzioni di lunghezza caratteristica. Un Nu ricavato con la larghezza non va confrontato direttamente con un riferimento che usa l’altezza. Il segno di wallHeatFlux può variare con l’orientazione; convertire i risultati grezzi in Q_hot e Q_cold positivi solo dopo verifica.

Approfondimento · Nusselt: definizioni, riferimenti e un controllo sui dati sperimentali
DefinizioneFormulaCommento
NuWq̄ W / (κ ΔT)quella della guida; NuW = 1 è la sola conduzione
NuHq̄ H / (κ ΔT) = NuW · H/W28,7 volte più grande per lo stesso flusso
Nu locale Nu(y)q(y) W / (κ ΔT)dal campo wallHeatFlux lungo l’altezza

Un controllo indipendente si ottiene dai dati del tutorial: a metà altezza vicino alla parete fredda la temperatura misurata passa da 15,0 °C (parete) a 15,62 °C a 0,33 mm, poi 15,94 °C a 0,58 mm e 16,45 °C a 0,84 mm. Il gradiente vicino alla parete è quindi fra 1,3 e 2 K/mm, cioè un Nu locale fra circa 5 e 7,5. È coerente con NuW ≈ 5,65 della correlazione e con il valore sintetico 4,79 della figura: l’ordine di grandezza è giusto, i numeri precisi vanno ottenuti dal calcolo. Il valore medio misurato da Betts e Bokhari per questo ΔT, riportato dalla scheda ERCOFTAC, è Nu = 5,85: è il riferimento sperimentale con cui confrontare Nuhot e Nucold, dopo aver verificato nella fonte che la definizione (lunghezza W, conducibilità, ΔT) coincida con quella della guida.

Approfondimento · Propagazione dell’incertezza su Nu

Per Nu = Q L / (A κ ΔT), con grandezze indipendenti, l’incertezza relativa è:

(dNu/Nu)^2 = (dQ/Q)^2 + (dL/L)^2 + (dA/A)^2 + (dk/k)^2 + (d(DeltaT)/DeltaT)^2

In una simulazione L, A e ΔT sono esatti per costruzione; κ è un dato di ingresso (incertezza di modello, non numerica); il termine dominante è dQ/Q, da stimare come combinazione di: scarto fra Qhot e Qcold, deriva iterativa delle ultime iterazioni e differenza fra mesh (sezione 13). Per il valore centrale usa la media (Qhot + Qcold)/2 e riporta metà della differenza come una delle componenti dell’incertezza.

Consiglio pratico · Nu locale lungo l’altezza

In ParaView, sulla patch hot, usa un Calculator sui dati di cella con l’espressione wallHeatFlux*0.076/(0.026086*19.6) e chiamala Nu_local; poi Plot Data o Plot Over Line lungo y a z = 0. La curva attesa è alta in basso, dove lo strato limite caldo nasce, e decresce verso l’alto; per la parete fredda l’andamento è speculare (e i valori sono negativi per la convenzione di segno). La media d’area di Nu_local deve coincidere con Nuhot calcolato da Q.

12. Esercitazione: tabella delle misure e analisi CSV

Il pacchetto contiene examples/measurement_TEMPLATE.csv (vuoto) e example_MEASUREMENTS_SYNTHETIC.csv (dati inventati a scopo di prova). Dopo aver raccolto le misure reali da ParaView, compilare le colonne Th_K, Tc_K, Ahot_m2, Acold_m2, Qhot_W, Qcold_W, L_m, k_W_mK, poi eseguire da Windows o Linux:

python scripts/thermal_balance.py examples/measurement_TEMPLATE.csv results.csv

Nota: il template contiene solo l’intestazione: prima vanno aggiunte una o più righe di misura; in caso contrario lo script segnala l’errore. Per provare subito il calcolatore senza dati CFD:

python scripts/thermal_balance.py examples/example_MEASUREMENTS_SYNTHETIC.csv examples/test_results.csv

Lo script calcola Nu_hot, Nu_cold ed errore di bilancio, non produce risultati simulati; rifiuta un output già esistente. Non modificare il file sintetico facendolo passare per una misura sperimentale.

Nusselt calcolato esclusivamente da numeri sintetici
Nusselt calcolato esclusivamente da numeri sintetici
Approfondimento · Script del pacchetto: thermal_balance.py
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Evaluate signed wall-power magnitudes, balance error, and cavity Nusselt number."""
import argparse
import csv
from pathlib import Path
import sys

REQUIRED = ('case', 'Th_K', 'Tc_K', 'Ahot_m2', 'Acold_m2', 'Qhot_W', 'Qcold_W', 'L_m', 'k_W_mK')


def evaluate(row):
    label = row['case'].strip()
    if not label:
        raise ValueError('Empty case name')
    values = {}
    for col in REQUIRED[1:]:
        values[col] = float(row[col])
    th, tc = values['Th_K'], values['Tc_K']
    ah, ac = values['Ahot_m2'], values['Acold_m2']
    qh, qc = values['Qhot_W'], values['Qcold_W']
    gap, k = values['L_m'], values['k_W_mK']
    if not th > tc or min(ah, ac, qh, qc, gap, k) <= 0:
        raise ValueError('Expected Th > Tc and strictly positive areas, powers, gap, conductivity')
    denom = k * (th - tc)
    return dict(case=label, deltaT_K=f'{th-tc:.6g}', Nu_hot=f'{qh*gap/ah/denom:.6g}',
                Nu_cold=f'{qc*gap/ac/denom:.6g}',
                relative_imbalance_percent=f'{100*abs(qh-qc)/max(qh,qc):.6g}',
                Qhot_W=f'{qh:.6g}', Qcold_W=f'{qc:.6g}')


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('input_csv', type=Path)
    parser.add_argument('output_csv', type=Path)
    args = parser.parse_args()
    if args.output_csv.exists():
        parser.error('Output already exists; refusing to overwrite it')
    with args.input_csv.open(newline='', encoding='utf-8-sig') as f:
        reader = csv.DictReader(f)
        if not set(REQUIRED).issubset(set(reader.fieldnames or [])):
            parser.error('CSV missing required columns: ' + ', '.join(REQUIRED))
        rows = [evaluate(row) for row in reader]
    if not rows:
        parser.error('Empty data table')
    with args.output_csv.open('x', newline='', encoding='utf-8') as f:
        writer = csv.DictWriter(f, fieldnames=list(rows[0]))
        writer.writeheader()
        writer.writerows(rows)
    for row in rows:
        print(f"{row['case']}: Nu_hot={row['Nu_hot']}, Nu_cold={row['Nu_cold']}, balance error={row['relative_imbalance_percent']}%")
    print('Results saved:', args.output_csv)


if __name__ == '__main__':
    try:
        main()
    except (OSError, ValueError, KeyError) as exc:
        sys.exit(f'ERROR: {exc}')
  • Colonne obbligatorie: case, Th_K, Tc_K, Ahot_m2, Acold_m2, Qhot_W, Qcold_W, L_m, k_W_mK; il BOM di Excel è gestito da utf-8-sig.
  • Rifiuta Th ≤ Tc e qualsiasi area, potenza, distanza o conducibilità non positiva: per questo Qcold va inserito già positivo.
  • Il file di uscita è aperto in modalità 'x': se esiste, l’operazione fallisce invece di sovrascrivere.
  • Eseguito sul file sintetico del pacchetto produce un file identico a examples/derived_SYNTHETIC.csv (Nuhot 4,78722 e 5,29766, scarti 1,91781 % e 0,983607 %); sul modello vuoto termina con «Empty data table», come indicato nella guida.
Approfondimento · File di esempio del pacchetto

examples/measurement_TEMPLATE.csv

case,Th_K,Tc_K,Ahot_m2,Acold_m2,Qhot_W,Qcold_W,L_m,k_W_mK

examples/example_MEASUREMENTS_SYNTHETIC.csv (dati inventati)

case,Th_K,Tc_K,Ahot_m2,Acold_m2,Qhot_W,Qcold_W,L_m,k_W_mK
baseline_SYNTHETIC,307.75,288.15,1.1336,1.1336,36.5,35.8,0.076,0.02608
hotter_SYNTHETIC,317.75,288.15,1.1336,1.1336,61.0,60.4,0.076,0.02608

examples/derived_SYNTHETIC.csv (risultato atteso del calcolatore)

case,deltaT_K,Nu_hot,Nu_cold,relative_imbalance_percent,Qhot_W,Qcold_W
baseline_SYNTHETIC,19.6,4.78722,4.69541,1.91781,36.5,35.8
hotter_SYNTHETIC,29.6,5.29766,5.24555,0.983607,61,60.4
Consiglio pratico · Verificare il calcolatore prima di usarlo
python scripts/thermal_balance.py examples/example_MEASUREMENTS_SYNTHETIC.csv examples/test_results.csv
fc examples\test_results.csv examples\derived_SYNTHETIC.csv        # Windows
diff examples/test_results.csv examples/derived_SYNTHETIC.csv      # Linux

Se il confronto non riporta differenze, il calcolatore e l’interprete Python funzionano come previsto. Usa poi una cartella separata per i risultati reali (per esempio results/), così i file sintetici non si mescolano mai con le misure.

13. Valutazione critica: convergenza e incertezza

La verifica del caso non si esaurisce nella convergenza dei residui. Documentare almeno: variazione temporale/iterativa di Q_hot e Q_cold, differenza fra potenze, sensibilità alla mesh e alle proprietà termofisiche, stabilità delle zone di ricircolo, definizione di Nu e qualità delle pareti.

Richiamo facoltativo — bilancio energetico. In regime stazionario, per una cavità chiusa senza sorgenti interne e senza irraggiamento, il calore che entra deve uguagliare quello che esce entro l’incertezza numerica. Uno scarto persistente può indicare calcolo incompleto, problemi di post-processing o errori di unità e segno.

Il tutorial originale include una geometria tridimensionale e un modello RANS; non generalizzare le sue impostazioni a una cavità laminare senza motivazione. Variare ΔT mantenendo invariata la mesh non garantisce identica accuratezza: può cambiare il regime del flusso.

Approfondimento · Uno studio di mesh mirato allo strato limite termico

In una copia nuova si può infittire solo vicino alle pareti termiche con una gradazione a più tratti nel blockMeshDict. Esempio, in cui ogni metà della larghezza ha metà delle celle e un rapporto 4 fra cella centrale e cella di parete:

blocks
(
    hex (0 1 2 3 4 5 6 7) (50 150 15)
    simpleGrading
    (
        ((0.5 0.5 4) (0.5 0.5 0.25))   // x: refined at both walls
        1
        1
    )
);

Ogni terna è (frazione della lunghezza, frazione delle celle, rapporto fra ultima e prima cella del tratto). Prima di ogni calcolo controlla checkMesh e la dimensione della prima cella. Una sequenza di tre mesh con raffinamento costante (per esempio 35, 50 e 70 celle in x, gradazione invariata) permette di applicare il metodo di Richardson e il GCI della Guida 4 a Qhot. Mantieni costante il numero di celle in y e z nella prima serie: cambiare tutto insieme confonde gli effetti.

Approfondimento · Quando il calcolo stazionario non converge

A RaH ≈ 2·10¹⁰ il flusso nella cavità alta può presentare instabilità e fluttuazioni a bassa frequenza. Un calcolo stazionario RANS può allora oscillare indefinitamente fra stati vicini: residui fermi su un plateau e Qhot che oscilla di qualche percento. In questo caso non si stringono i fattori di rilassamento fino a «forzare» la convergenza; si riporta l’ampiezza dell’oscillazione come incertezza iterativa oppure, in una copia nuova, si passa a un calcolo transitorio (ddtSchemes Euler o backward, passo controllato dal numero di Courant come nella Guida 8) e si mediano le grandezze su un intervallo statisticamente stazionario.

Consiglio pratico · Matrice delle sensibilità da documentare
FattoreVariazione suggeritaGrandezza osservata
mesh in x35 → 50 → 70 celleQhot, y⁺, Nu
Prt nel volume1 (predefinito) → 0,85Qhot
modello di turbolenzakOmegaSST → kEpsilon (con funzioni di parete coerenti)profili e Q
convergenzasoglie 1e-4 → 1e-5deriva di Q
ΔT19,6 → 29,6 KNu (attesa crescita circa con Ra0,3)

Cambia un fattore per volta, ognuno in una copia propria, e riporta i risultati in una tabella unica. Con l’esponente 0,293 della correlazione, passando da ΔT = 19,6 a 29,6 K ci si aspetta NuW più alto di circa il 13 %: un aumento molto diverso va spiegato.

14. Estensioni consigliate

  • Convezione forzata in un condotto: ingresso U e T, parete calda, misura di T_bulk, portata massica e potenza ṁ Cp (Tout−Tin). Per liquidi con proprietà variabili occorre scegliere un modello coerente, non importare senza verifica la termodinamica del gas ideale.
  • Scambio termico coniugato: separare regioni fluido/solido con coupledTemperature e verificare la continuità del flusso all’interfaccia.
  • Parete con scambio esterno: usare externalTemperature in una nuova copia del caso e verificare la sensibilità a h.
  • Ripetibilità: esportare .pvsm, log, CSV e file di configurazione, con date, versione esatta e ipotesi fisiche.
Approfondimento · Convezione forzata: i riferimenti analitici per la verifica

Nell’estensione a un condotto riscaldato esistono soluzioni di riferimento che permettono una vera verifica del codice, non solo una plausibilità:

  • flusso laminare completamente sviluppato in tubo circolare: NuD = 3,66 a temperatura di parete costante e 4,36 a flusso di parete costante;
  • canale piano fra due lastre: NuDh = 7,54 (temperatura costante) e 8,23 (flusso costante) su entrambe le pareti;
  • lunghezza d’ingresso termica laminare ≈ 0,05 Re Pr Dh: il dominio deve essere più lungo per raggiungere il valore sviluppato;
  • in turbolento, correlazioni come Gnielinski hanno incertezze del 10–20 % e servono solo come confronto di ordine di grandezza.

Il bilancio ṁ cp (Tout − Tin) va calcolato con la temperatura media pesata dalla portata, non con la media d’area: in OpenFOAM 14 si ottiene con una surfaceFieldValue con operation average e weightField phi (procedura descritta nella Guida 11).

Approfondimento · Tutorial ufficiali utili per le estensioni
TutorialChe cosa mostra
fluid/concentricCylindersintegrazione di wallHeatFlux e del flusso radiativo qr su patch con #includeFunc patchIntegrate
fluid/roomHeatingparete esterna con externalTemperature e h
multiRegion/CHT/heatedDuctil caso a tre regioni della Guida 11
multiRegion/CHT/vacuumFurnacemedia di T in un solido e flusso radiativo su patch

Si elencano tutti con ls $FOAM_TUTORIALS/fluid $FOAM_TUTORIALS/multiRegion/CHT. Il file system/functions di ciascuno è un buon modello per le proprie misure.

Consiglio pratico · Archiviare lo studio
cd "$HOME/OpenFOAM"
foamVersion > guide10_thermal_study/OPENFOAM_VERSION.txt 2>&1
tar czf guide10_thermal_study_$(date +%Y%m%d).tar.gz guide10_thermal_study
sha256sum guide10_thermal_study_*.tar.gz > guide10_thermal_study_SHA256.txt

L’archivio è una copia con impronta: nessun file viene rimosso. Conserva insieme i CSV di misura, i risultati di thermal_balance.py, il confronto sperimentale e gli stati ParaView.

15. Checklist scientifica

  • ☐ La copia del caso si trova nell’area utente, non nella cartella dei tutorial ufficiali.
  • ☐ Le temperature sono espresse in kelvin e le proprietà del materiale sono state controllate.
  • ☐ checkMesh e solver sono terminati senza errori e i log sono stati conservati.
  • ☐ Il flusso alle pareti è stato integrato con area e segni coerenti.
  • ☐ Q_hot, Q_cold e lo scarto relativo sono registrati.
  • ☐ Il Nu dichiara chiaramente L, k, ΔT e la definizione di flusso.
  • ☐ I risultati reali sono separati dagli esempi sintetici.
  • ☐ Sono state valutate almeno convergenza numerica e sensibilità alla mesh.
Consiglio pratico · Controlli aggiuntivi per un lavoro di ricerca
  • Q delle patch adiabatiche pari a zero e somma con segno di tutte le patch prossima a zero.
  • Riga «Selecting … thermophysical transport model» letta e Prt dichiarato (parete 0,85, volume 1 se non diversamente impostato).
  • Numero di iterazioni e motivo dell’arresto (convergenza o endTime) registrati.
  • y⁺ della prima cella sulle pareti termiche riportato.
  • Q confrontato con la stima da correlazione (≈ 43 W nel caso base) e scostamento spiegato.
  • Profili confrontati con i dati sperimentali del tutorial, con scarti per quota.
  • Effetto dell’irraggiamento discusso, almeno come ordine di grandezza.
  • Commento della variante corretto o annotato in hotter_29p6K/0/T.

Riferimenti e limiti di verifica

  1. OpenFOAM Foundation 14, tutorial ufficiale fluid/buoyantCavity: https://github.com/OpenFOAM/OpenFOAM-14/tree/master/tutorials/fluid/buoyantCavity.
  2. OpenFOAM 14 User Guide, modelli termofisici: https://doc.cfd.direct/openfoam/user-guide-v14/thermophysical-models.
  3. OpenFOAM 14 User Guide, condizioni termiche: https://doc.cfd.direct/openfoam/user-guide-v14/thermal-boundary-conditions.
  4. OpenFOAM 14 Source Guide, wallHeatFlux: https://cpp.openfoam.org/v14/classFoam_1_1functionObjects_1_1wallHeatFlux.html.
  5. OpenFOAM 14 User Guide, post-processing: https://doc.cfd.direct/openfoam/user-guide-v14/post-processing-functionality.

Verifica effettuata per questa guida: confronto dei principali file del tutorial con il repository ufficiale, controllo della sintassi degli script e prova del calcolatore sui dati sintetici. I casi creati con gli script vanno collaudati sul server OpenFOAM 14 prima di trarre conclusioni scientifiche.

Materiale di esercitazione originale: GNU GPL v3 o successiva. Le immagini schematiche sono didattiche; le simulazioni e gli eventuali riferimenti upstream conservano le loro licenze originali.

Approfondimento · Letture di riferimento, con collegamenti
  • P. L. Betts, I. H. Bokhari, Experiments on turbulent natural convection in an enclosed tall cavity, International Journal of Heat and Fluid Flow 21(6) (2000) 675–683, doi:10.1016/S0142-727X(00)00033-3: l’esperimento del tutorial (citato nel suo README);
  • Scheda del caso nella banca dati ERCOFTAC: KBwiki UFR 4-10, natural convection in a tall cavity (geometria, Ra, Nusselt medi misurati, dati scaricabili);
  • S. M. ElSherbiny, G. D. Raithby, K. G. T. Hollands, Heat transfer by natural convection across vertical and inclined air layers, Journal of Heat Transfer 104(1) (1982) 96–102, ASME Digital Collection: la correlazione usata nelle stime;
  • I. B. Celik, U. Ghia, P. J. Roache, C. J. Freitas, H. Coleman, P. E. Raad, Procedure for estimation and reporting of uncertainty due to discretization in CFD applications, Journal of Fluids Engineering 130 (2008) 078001, testo pubblicato da ASME: il GCI usato nello studio di mesh;
  • T. L. Bergman, A. S. Lavine, F. P. Incropera, D. P. DeWitt, Fundamentals of Heat and Mass Transfer, 8th ed., Wiley, 2017 (ISBN 978-1-119-35388-1), Wiley: convezione naturale in cavità, irraggiamento fra superfici, convezione forzata in condotti;
  • A. Bejan, Convection Heat Transfer, 4th ed., Wiley, 2013 (ISBN 978-0-470-90037-6): analisi di scala degli strati limite di convezione naturale;
  • F. R. Menter, Two-equation eddy-viscosity turbulence models for engineering applications, AIAA Journal 32(8) (1994) 1598–1605: il modello k-ω SST del tutorial (Guida 9).

Il README del tutorial indica anche la pagina del database ERCOFTAC/Manchester (caso 79) da cui provengono i dati sperimentali.

Approfondimento · I file sorgente di OpenFOAM 14 citati nei riquadri

Ogni affermazione sul comportamento del codice si può verificare direttamente nel repository ufficiale della Foundation:

Guide collegate: server Linux da Windows, monitoraggio e convergenza, ParaView per l’analisi quantitativa, verifica e validazione, campagne parametriche, generazione e controllo delle mesh, condizioni al contorno e modelli fisici, simulazioni transitorie, turbolenza e modelli RANS e il percorso Imparare OpenFOAM 14.

#CFD #convezione naturale #numero di Nusselt #OpenFOAM #trasferimento di calore #wallHeatFlux