OpenFOAM: trasferimento termico coniugato (CHT) fra fluido e solidi

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

Undicesima guida del percorso Imparare OpenFOAM 14 e terza 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, turbolenza e modelli RANS e trasferimento di calore e convezione naturale. Questa affronta il trasferimento termico coniugato (CHT): tre regioni accoppiate (acqua, parete metallica e riscaldatore), foamMultiRun, coupledTemperature, sorgenti volumetriche, accumulo di energia e bilancio energetico sul caso ufficiale heatedDuct. I riquadri di approfondimento e di consiglio pratico si aprono con un clic: contengono tutti gli script del pacchetto, i dati reali del tutorial letti dai suoi file (geometria, materiali, tempi, mesh), le stime fisiche che permettono di prevedere l’ordine di grandezza dei risultati, un file di funzioni di monitoraggio per misurare portate, entalpie, temperature medie e flussi alle interfacce direttamente con OpenFOAM, due script aggiuntivi e le procedure di verifica, compreso un test di linearità che il caso deve superare.

Una simulazione con tre regioni: fluido, parete metallica e riscaldatore
Ambiente: OpenFOAM Foundation 14 su Linux; ParaView 5.13.2, FileZilla e Notepad++ su Windows.
Livello: intermedio · Collana OpenFOAM, guida 11 di 20

Obiettivo. Preparare ed eseguire un caso CHT nel quale il fluido scorre lungo un condotto, un riscaldatore genera energia e una parete solida conduce il calore verso il fluido. Modificare due parametri, osservare i campi termici e verificare il bilancio energetico senza confondere temperature, potenze e flussi superficiali.

Schema delle regioni e delle interfacce
Schema delle regioni e delle interfacce
Videolezione sulla Guida 11 (in italiano), generata con Gemini Notebook a partire da questo articolo.

1. Perché serve il trasferimento termico coniugato

Nelle Guide 1–10 abbiamo affrontato il lavoro remoto, i log, ParaView, la verifica numerica, l’automazione, le mesh, le condizioni al contorno, i transitori, la turbolenza e la convezione naturale. Qui il problema si estende a due mezzi fisici accoppiati: nel fluido operano trasporto convettivo e diffusione termica, nel solido conduzione e accumulo di energia. La temperatura all’interfaccia non è un valore imposto arbitrariamente: risulta dall’accoppiamento delle equazioni nelle regioni confinanti.

Nel modello ideale, senza resistenza di contatto né sorgenti superficiali, alla stessa interfaccia si richiedono continuità della temperatura e del flusso termico normale, con versi opposti rispetto alle normali uscenti delle due regioni. Una differenza fra i flussi calcolati può essere conseguenza di errori, discretizzazione o convenzioni di segno: non va semplicemente corretta a posteriori.

Richiamo facoltativo — unità: temperatura T [K]; conducibilità k [W/(m K)]; flusso superficiale q″ [W/m²]; potenza termica Q̇ [W]; sorgente volumetrica q‴ [W/m³]. In particolare, q″ e q‴ non sono intercambiabili.

Approfondimento · Geometria reale del caso, letta dai file STL

I tre file in constant/geometry (heatedDuct.stl, fluidToMetal.stl, metalToHeater.stl) descrivono una geometria semplice, con l’asse del condotto lungo z:

RegioneEstensione (mm)Volume nominaleNote
fluidx 5–45, y 5–45, z 0–1502,40·10⁻⁴ m³canale quadrato 40 × 40 mm, ingresso 1,6·10⁻³ m²
metalx 0–50, y 0–50 meno il canale1,35·10⁻⁴ m³parete di 5 mm su quattro lati
heaterx 7.5–42.5, y −10–0, z 0–1505,25·10⁻⁵ m³piastra 35 × 10 mm sotto il condotto

L’interfaccia fluido-metallo è il perimetro interno del canale (4 × 40 mm × 150 mm = 0,024 m²); quella metallo-riscaldatore è la faccia superiore del riscaldatore (35 × 150 mm = 5,25·10⁻³ m²). Il calore generato nel riscaldatore deve quindi attraversare prima una sezione piccola e poi distribuirsi lungo la parete metallica: la conduzione «circonferenziale» nel metallo è la chiave fisica del caso. Con la sorgente del tutorial la potenza nominale è Q̇gen = 10⁷ × 5,25·10⁻⁵ = 525 W; quella reale dipende dal volume misurato sulla mesh (sezione 7).

Approfondimento · Le condizioni all’interfaccia e la loro discretizzazione

Per un’interfaccia ideale fra fluido (f) e solido (s), con normale n uscente dal fluido:

T_f = T_s                                continuità della temperatura
kappa_f dT_f/dn = kappa_s dT_s/dn        continuità del flusso normale

In una discretizzazione ai volumi finiti con le due celle adiacenti a distanza δf e δs dalla faccia, queste due condizioni danno una temperatura d’interfaccia pesata dalle conduttanze:

T_i = (kappa_f/delta_f * T_f + kappa_s/delta_s * T_s) / (kappa_f/delta_f + kappa_s/delta_s)
q   = (T_s - T_f) / (delta_f/kappa_f + delta_s/kappa_s)

È la «media armonica» delle conducibilità di Patankar: il flusso è limitato dalla resistenza maggiore. Con acqua (κ = 0,61) e alluminio (κ = 200) e celle uguali, la temperatura d’interfaccia è praticamente quella del metallo: la resistenza è tutta nel fluido. Per questo la qualità della mesh dal lato del fluido decide l’accuratezza del flusso scambiato.

Consiglio pratico · Le dimensioni nei file di questo tutorial

A differenza della Guida 10, i campi di heatedDuct dichiarano le dimensioni come vettore di sette esponenti [massa lunghezza tempo temperatura quantità corrente intensità luminosa]:

CampodimensionsSI
0/*/T[ 0 0 0 1 0 0 0 ]K
0/fluid/U[ 0 1 -1 0 0 0 0 ]m/s
0/fluid/p, p_rgh[ 1 -1 -2 0 0 0 0 ]Pa

Una sorgente in W/m³ ha dimensioni [1 −1 −3 0 0 0 0], una potenza in W [1 2 −3 0 0 0 0]: confrontare gli esponenti è il modo più rapido per accorgersi di aver scambiato q‴ e Q̇.

2. Il caso ufficiale usato nella guida

Il tutorial verificato è $FOAM_TUTORIALS/multiRegion/CHT/heatedDuct, presente nel repository ufficiale OpenFOAM Foundation 14. Comprende le regioni fluid (acqua), metal (solido conduttore) e heater (riscaldatore con sorgente volumetrica). Il file system/controlDict associa i moduli fluid e solid alle tre regioni; l’eseguibile è foamMultiRun. Non sostituire quest’ultimo con il comando chtMultiRegionFoam preso da materiale relativo ad altre distribuzioni.

RegioneCampo principaleTrattamento fisicoFile notevoli
fluidU, p, TMoto, trasporto energetico0/fluid/T, 0/fluid/U, constant/fluid/physicalProperties
metalTConduzione nel solido0/metal/T, constant/metal/physicalProperties
heaterTConduzione e sorgente0/heater/T, constant/heater/fvModels

Il tutorial ufficiale imposta in origine una temperatura d’ingresso di 300 K, una velocità prescritta di 0,001 m/s, una conducibilità di riferimento di 200 W/(m K) nel metallo e una sorgente volumetrica nel riscaldatore di 10⁷ W/m³. Questi valori sono parametri del tutorial, non una validazione sperimentale né un dimensionamento di uno scambiatore reale.

Approfondimento · I dati del tutorial, file per file
FileContenutoConseguenza
system/controlDictregionSolvers { fluid fluid; metal solid; heater solid; }un modulo per regione
system/controlDictendTime 20; deltaT 1e-3; adjustTimeStep yes; maxCo 1.0; writeInterval 1; writeFormat binary20 s di transitorio, scritture ogni secondo in binario
constant/fluid/momentumTransportsimulationType laminar;nessun modello di turbolenza
constant/fluid/gvalue (0 0 0);gravità nulla: nessuna convezione naturale
constant/fluid/physicalPropertiesrhoConst, hConst, commento «Water»densità costante 1000 kg/m³
constant/metal/physicalPropertiescommento «Aluminium»: ρ 2700, Cv 900, κ 200heSolidThermo, eConst
constant/heater/physicalPropertiescommento «Iron»: ρ 8000, Cv 450, κ 80heSolidThermo, eConst
0/fluid/Uingresso uniforme (0 0 1e-3)profilo non sviluppato all’ingresso
0/fluid/Tingresso 300 K, uscita inletOutletgestisce eventuali riflussi
0/metal/T, 0/heater/TzeroGradient sulle facce esternesistema adiabatico verso l’esterno

I commenti nei file del tutorial associano il metallo all’alluminio e il riscaldatore al ferro; le proprietà sono costanti e arrotondate. La cautela della guida resta valida: modificando solo κ non si ottiene un altro materiale reale (riquadro della sezione 5).

Approfondimento · Numeri adimensionali e scale dei tempi: che cosa succede in 20 secondi
GrandezzaValoreLettura
Re = U Dh/ν42laminare, coerente con il modello
Pr6,62acqua a temperatura ambiente
Pe = Re Pr276trasporto assiale dominato dalla convezione
tempo di attraversamento L/U150 s7,5 volte il tempo simulato
lunghezza d’ingresso idrodinamica ≈ 0.05 Re Dh0,083 mmetà del condotto
lunghezza d’ingresso termica ≈ 0.05 Re Pr Dh0,55 m3,7 volte il condotto: sempre in sviluppo
penetrazione termica in acqua dopo 20 s √(αt)1,7 mmlo strato riscaldato è sottile
costante di tempo globale Ctot/(ṁ cp)≈ 227 sordine del tempo per avvicinarsi allo stazionario

La conseguenza è netta: a t = 20 s l’acqua entrata dall’ingresso ha percorso solo 20 mm, e quella che esce è acqua presente nel condotto fin dall’inizio, riscaldata solo vicino alle pareti. La temperatura media di miscelamento all’uscita è quindi ancora molto vicina a 300 K e quasi tutta la potenza generata è accumulata nei solidi e nell’acqua vicina alle pareti. Il caso del tutorial è un transitorio iniziale, non uno stato stazionario: il bilancio della sezione 10 deve includere l’accumulo, esattamente come la guida raccomanda.

Approfondimento · Perché la gravità è nulla e che cosa cambierebbe

Con g = (0 0 0) e densità costante il moto non dipende dalla temperatura: il problema termico è disaccoppiato da quello fluidodinamico. In una prova reale con acqua riscaldata dal basso a 1 mm/s la situazione sarebbe opposta. Con β ≈ 2,76·10⁻⁴ 1/K e una differenza di 10 K su 40 mm, Gr = gβΔT D³/ν² ≈ 1,9·10⁶ e il numero di Richardson Gr/Re² ≈ 1000: la convezione naturale dominerebbe largamente quella forzata, con celle convettive trasversali. Il tutorial è quindi un dimostratore numerico del CHT, non un modello di un apparato reale. Per studiare la convezione mista servono una copia nuova con g diversa da zero, un’equazione di stato che faccia variare la densità e una mesh adeguata alle celle convettive.

Consiglio pratico · Leggere i valori senza aprire l’editor
cd "$FOAM_TUTORIALS/multiRegion/CHT/heatedDuct"
foamDictionary system/controlDict -entry regionSolvers
foamDictionary system/controlDict -entry endTime
foamDictionary constant/metal/physicalProperties -entry mixture/transport/kappa
foamDictionary constant/heater/fvModels -entry energySource/q
foamDictionary constant/fluid/g -entry value

foamDictionary legge e stampa una voce, con il percorso separato da «/». Usato senza opzioni di scrittura non modifica nulla: è il modo più sicuro per controllare i parametri di tutorial e copie prima di ogni esecuzione.

3. Anatomia del caso CHT

heatedDuct/
├── 0/
│   ├── fluid/        T, U, p, p_rgh
│   ├── metal/        T
│   └── heater/       T
├── constant/
│   ├── fluid/        physicalProperties, momentumTransport, g
│   ├── metal/        physicalProperties
│   ├── heater/       physicalProperties, fvModels
│   └── geometry/     geometry for snappyHexMesh
└── system/
    ├── controlDict   regionSolvers, time controls
    ├── blockMeshDict
    ├── snappyHexMeshDict
    ├── fluid/        fvSchemes, fvSolution
    ├── metal/        fvSchemes, fvSolution
    └── heater/       fvSchemes, fvSolution

La mesh è inizialmente unica e viene suddivisa per regioni. Ogni regione possiede quindi un campo di temperatura e proprie proprietà. La geometria e la mesh possono introdurre differenze fra i valori effettivi delle aree e quelli nominali: le misure energetiche vanno effettuate sulla geometria discretizzata risultante.

La sequenza di preparazione riprende quella del tutorial: blockMesh, snappyHexMesh, splitMeshRegions -cellZones all -defaultRegion fluid. L’esecuzione seriale della soluzione avviene poi con foamMultiRun. L’Allrun ufficiale utilizza invece il calcolo parallelo e ricostruisce i risultati; i due percorsi non vanno mescolati nella stessa directory.

Approfondimento · Come nasce la mesh a tre regioni
  • blockMesh: un blocco unico 20 × 24 × 60 = 28 800 celle cubiche da 2,5 mm che contiene tutta la geometria (x 0–50, y −10–50, z 0–150 mm, con 0,001 mm di margine).
  • snappyHexMesh: raffina al livello 1 (celle da 1,25 mm) vicino alle superfici esterne e alle due interfacce, aggancia la mesh alle superfici STL, non aggiunge strati (addLayers false). Le superfici fluidToMetal e metalToHeater creano le faceZone omonime e, con mode insidePoint, le cellZone metal e heater; il punto insidePoint (0.025 0.025 0.075) sta nel fluido.
  • splitMeshRegions -cellZones all -defaultRegion fluid: separa una mesh per ogni cellZone, assegna le celle rimanenti alla regione fluid e crea le patch d’interfaccia con nomi <regione>_to_<regione>.

Con celle da 1,25 mm la parete metallica di 5 mm è descritta da circa 4 celle nello spessore e il riscaldatore da circa 8. Per un primo studio va bene; per misure di flusso all’interfaccia è la risoluzione dal lato acqua quella da verificare (riquadro della sezione 1).

Consiglio pratico · Ispezionare le interfacce dopo la suddivisione
cd "$HOME/OpenFOAM/CHT_Guida11/reference"
grep -A8 "fluid_to_metal" constant/fluid/polyMesh/boundary
grep -A8 "metal_to_fluid" constant/metal/polyMesh/boundary
grep -A8 "metal_to_heater" constant/metal/polyMesh/boundary
grep -A8 "heater_to_metal" constant/heater/polyMesh/boundary

Ogni patch d’interfaccia deve essere di un tipo «mapped» che indica la regione e la patch confinanti, e il numero di facce (nFaces) delle due patch di una coppia deve essere uguale: le regioni derivano dalla stessa mesh e le facce coincidono. Numeri diversi o una regione confinante errata spiegano temperature discontinue all’interfaccia (sezione 12).

Approfondimento · Il percorso parallelo dell’Allrun, in una copia separata

L’Allrun ufficiale decompone in 4 sottodomini con method hierarchical; n (1 1 4); cioè taglia il condotto in quattro tronchi lungo z. Per usarlo in una copia nuova, dopo la mesh:

decomposePar -allRegions > guide11_logs/decomposePar.log 2>&1
mpirun -np 4 foamMultiRun -parallel > guide11_logs/solve_parallel.log 2>&1
reconstructPar -allRegions > guide11_logs/reconstructPar.log 2>&1

Con 28 800 celle di partenza il caso è piccolo e il guadagno parallelo è modesto; serve soprattutto come prova della procedura prima di mesh più fini. Non lanciare il percorso parallelo nella stessa copia in cui è stato eseguito solve_case.sh: i risultati seriali e ricostruiti finirebbero nelle stesse cartelle temporali.

4. Che cosa risolvono i moduli fisici

Nel solido isotropo con parametri costanti, il bilancio energetico locale può essere schematizzato come:

ρₛ cₛ ∂T/∂t = ∇·(kₛ ∇T) + q‴

Nel fluido l’energia è trasportata dalla velocità e dalla diffusione/conduzione, con eventuali contributi aggiuntivi del modello termofisico e di turbolenza. Il termine convettivo dipende dal campo di moto, perciò non basta risolvere la sola conduzione nel solido.

Alle superfici accoppiate l’energia attraversa il confine grazie al gradiente termico. La condizione coupledTemperature viene inserita su entrambe le facce della coppia di interfacce. Nel caso ufficiale sono fluid_to_metal ↔ metal_to_fluid e metal_to_heater ↔ heater_to_metal.

Resistenze termiche e flussi
Resistenze termiche e flussi

Richiamo facoltativo — resistenze termiche: per un modello unidimensionale ideale R_cond = L/(k A), R_conv = 1/(h A) e Q̇ ≈ ΔT / (R_cond + R_conv). Queste relazioni spiegano le tendenze, ma non sostituiscono un calcolo CHT tridimensionale con geometria e flusso non uniformi.

Approfondimento · Le equazioni assemblate dai due moduli

Nel sorgente della versione 14 il modulo solid (applications/modules/solid/solid.C) risolve per ogni regione solida l’energia interna sensibile e:

fvm::ddt(rho, e) + thermophysicalTransport->divq(e) == fvModels().source(rho, e)

Con eConst e rhoConst è esattamente ρ Cv ∂T/∂t = ∇·(κ∇T) + q‴, dove q‴ arriva dal heatSource di fvModels. Il modulo fluid risolve l’entalpia sensibile h con convezione, energia cinetica e lavoro di pressione (Guida 10); con g = 0 manca il termine di galleggiamento. Le due famiglie di equazioni sono accoppiate solo attraverso le condizioni coupledTemperature sulle patch d’interfaccia.

Approfondimento · Passo temporale, accoppiamento segregato e accuratezza nei solidi

foamMultiRun risolve le regioni una dopo l’altra in un ciclo PIMPLE esterno comune. Il passo è scelto dal minimo dei maxDeltaT dei moduli: il fluido lo limita con maxCo 1, mentre il modulo solid della versione 14 non ha un limite sul numero di diffusione e usa solo maxDeltaT di controlDict, se presente. Con U = 1 mm/s e celle da 1,25 mm, il Courant permette passi fino a circa 1 s. Il numero di Fourier di cella nell’alluminio diventa allora Fo = αΔt/Δx² ≈ 8,2·10⁻⁵ × 1/(1,25·10⁻³)² ≈ 50.

Lo schema di Eulero implicito resta stabile, ma con Fo ≫ 1 la storia temporale nei solidi è poco accurata, e con un solo passaggio del ciclo esterno l’accoppiamento fra regioni è ritardato di un passo. Per un transitorio da misurare (sezione 10) in una copia nuova si limitano il passo e si aumentano le correzioni esterne:

// system/controlDict
maxDeltaT       0.05;

// system/fvSolution (top level, not system/<region>/fvSolution)
PIMPLE
{
    nOuterCorrectors 3;
}

Il blocco PIMPLE di primo livello è quello che nel tutorial è vuoto; i tutorial CHT ufficiali della versione 14 vi impostano nOuterCorrectors. La verifica è sempre la stessa: dimezzare maxDeltaT e controllare che Tmax del riscaldatore e l’energia accumulata non cambino oltre la tolleranza scelta.

Approfondimento · Rete di resistenze e alette: una previsione per l’esercizio B
ResistenzaStimaValore
conduzione nello spessore del metallo0,005 / (200 × 0,024)1,0·10⁻³ K/W
stessa, κ = 500,005 / (50 × 0,024)4,2·10⁻³ K/W
metà spessore del riscaldatore0,005 / (80 × 5,25·10⁻³)1,2·10⁻² K/W
convezione lato acqua (h ≈ 350 W/(m² K))1 / (350 × 0,024)0,12 K/W

Il valore di h è una stima d’ordine (κacqua diviso lo spessore di penetrazione di circa 1,7 mm). La resistenza convettiva domina: ridurre κ del metallo da 200 a 50 cambia poco la resistenza in serie. Cambia invece molto la distribuzione: le pareti laterali e superiore funzionano come alette alimentate dal basso. Con il parametro d’aletta m = √(h/(κ t)) e t = 5 mm, la parete laterale (45 mm) ha efficienza ≈ 0,82 con κ = 200 e ≈ 0,55 con κ = 50 (cht_estimates.py). Ipotesi da verificare: con il metallo meno conduttivo la parete inferiore e il riscaldatore diventano più caldi, le pareti laterali e superiore più fredde, e la potenza ceduta all’acqua si concentra sul fondo del canale.

Approfondimento · Script aggiuntivo: cht_estimates.py

Raccoglie in un solo programma tutte le stime di questa guida: volumi e capacità termiche, potenza, portata, Re, Pr, Pe, tempi caratteristici, lunghezze d’ingresso, tempi di diffusione, efficienza d’aletta e numero di Richardson ipotetico. I volumi nominali vanno sostituiti con quelli misurati (--V-heater):

python cht_estimates.py
python cht_estimates.py --kappa-metal 50
python cht_estimates.py --q 5e6 --V-heater 5.31e-5
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Hand estimates for the OpenFOAM 14 multiRegion/CHT/heatedDuct case.

Nominal geometry from the tutorial STL files: square duct 50 x 50 mm outside,
40 x 40 mm fluid channel (5 mm metal wall), heater 35 x 10 mm under the duct,
all 150 mm long. Properties from constant/*/physicalProperties. The numbers are
order-of-magnitude checks for the CFD results, not results themselves; the
volumes must be replaced by the values measured on the actual mesh
(checkMesh -region <name>, 'Total volume').
"""
import argparse
import math


def main():
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument('--q', type=float, default=1e7, help='heater source density [W/m3]')
    p.add_argument('--kappa-metal', type=float, default=200.0, help='metal conductivity [W/(m K)]')
    p.add_argument('--U', type=float, default=1e-3, help='inlet velocity [m/s]')
    p.add_argument('--V-heater', type=float, help='measured heater volume [m3] (default: nominal)')
    p.add_argument('--h', type=float, default=350.0, help='assumed fluid-side film coefficient [W/(m2 K)] for the fin estimate')
    p.add_argument('--endTime', type=float, default=20.0, help='simulated time [s]')
    a = p.parse_args()

    L = 0.150
    fluid = dict(rho=1000.0, c=4181.0, mu=959e-6, Pr=6.62, V=0.040 * 0.040 * L)
    fluid['k'] = fluid['mu'] * fluid['c'] / fluid['Pr']
    metal = dict(rho=2700.0, c=900.0, k=a.kappa_metal, V=(0.050 ** 2 - 0.040 ** 2) * L)
    heater = dict(rho=8000.0, c=450.0, k=80.0, V=a.V_heater or 0.035 * 0.010 * L)

    area_in = 0.040 * 0.040
    dh = 0.040
    nu = fluid['mu'] / fluid['rho']
    mdot = fluid['rho'] * a.U * area_in
    q_gen = a.q * heater['V']
    re = a.U * dh / nu
    pe = re * fluid['Pr']

    print('Region   V [m3]      rho*c*V [J/K]   alpha [m2/s]   effusivity [W s^0.5/(m2 K)]')
    total_c = 0.0
    for name, r in (('fluid', fluid), ('metal', metal), ('heater', heater)):
        cap = r['rho'] * r['c'] * r['V']
        total_c += cap
        alpha = r['k'] / (r['rho'] * r['c'])
        eff = math.sqrt(r['k'] * r['rho'] * r['c'])
        r['alpha'] = alpha
        print(f'{name:7s}  {r["V"]:.4e}  {cap:12.1f}    {alpha:.3e}      {eff:9.0f}')
    print(f'Water conductivity k = mu cp / Pr = {fluid["k"]:.4f} W/(m K)')
    print(f'Heater power Q_gen = q * V_heater = {q_gen:.1f} W')
    print(f'Mass flow rate = rho U A = {mdot:.4e} kg/s')
    print(f'Re (D_h = 40 mm) = {re:.1f},  Pr = {fluid["Pr"]},  Pe = {pe:.0f}')
    print(f'Flow-through time L/U = {L / a.U:.0f} s  (simulated time {a.endTime:g} s)')
    print(f'Hydrodynamic entry length ~ 0.05 Re D_h = {0.05 * re * dh:.3f} m')
    print(f'Thermal entry length ~ 0.05 Re Pr D_h = {0.05 * pe * dh:.3f} m  (duct length {L} m)')
    print(f'Steady bulk temperature rise Q_gen/(mdot cp) = {q_gen / (mdot * fluid["c"]):.1f} K')
    print(f'Lumped time constant C_total/(mdot cp) = {total_c / (mdot * fluid["c"]):.0f} s')
    print(f'Energy generated up to endTime = {q_gen * a.endTime:.0f} J')
    print(f'Adiabatic heater heating rate q/(rho c) = {a.q / (heater["rho"] * heater["c"]):.2f} K/s')
    for name, r, d in (('metal wall (5 mm)', metal, 0.005), ('heater (10 mm)', heater, 0.010), ('water (5 mm)', fluid, 0.005)):
        print(f'Diffusion time d^2/alpha, {name}: {d * d / r["alpha"]:.2f} s')
    print(f'Thermal penetration in water after endTime sqrt(alpha t) = {1000 * math.sqrt(fluid["alpha"] * a.endTime):.2f} mm')
    m = math.sqrt(a.h / (metal['k'] * 0.005))
    for label, length in (('side wall (45 mm)', 0.045), ('side + top half (70 mm)', 0.070)):
        ml = m * length
        print(f'Fin estimate, {label}: mL = {ml:.2f}, efficiency tanh(mL)/mL = {math.tanh(ml) / ml:.2f}')
    beta, dT, g = 2.76e-4, 10.0, 9.81
    gr = g * beta * dT * dh ** 3 / nu ** 2
    print(f'If gravity were active (beta = {beta} 1/K, dT = {dT} K): Gr = {gr:.2e}, Ri = Gr/Re^2 = {gr / re ** 2:.0f}')


if __name__ == '__main__':
    main()

Uscita per il caso di riferimento (estratto):

Region   V [m3]      rho*c*V [J/K]   alpha [m2/s]   effusivity [W s^0.5/(m2 K)]
fluid    2.4000e-04        1003.4    1.449e-07           1591
metal    1.3500e-04         328.1    8.230e-05          22045
heater   5.2500e-05         189.0    2.222e-05          16971
Heater power Q_gen = q * V_heater = 525.0 W
Mass flow rate = rho U A = 1.6000e-03 kg/s
Flow-through time L/U = 150 s  (simulated time 20 s)
Steady bulk temperature rise Q_gen/(mdot cp) = 78.5 K
Lumped time constant C_total/(mdot cp) = 227 s
Adiabatic heater heating rate q/(rho c) = 2.78 K/s
Diffusion time d^2/alpha, metal wall (5 mm): 0.30 s
Diffusion time d^2/alpha, heater (10 mm): 4.50 s
Diffusion time d^2/alpha, water (5 mm): 172.58 s

5. Proprietà termofisiche: prima capire, poi modificare

Nel file constant/fluid/physicalProperties il tutorial imposta acqua con densità di riferimento 1000 kg/m³, Cp = 4181 J/(kg K), viscosità dinamica mu = 959e-6 Pa s e numero di Prandtl Pr = 6.62. Nel file del metallo si legge kappa = 200 W/(m K); nel riscaldatore kappa = 80 W/(m K). Gli esempi utilizzano leggi di proprietà semplici, non proprietà dipendenti dalla temperatura.

Per il primo confronto non modifichiamo densità, capacità termica o geometria. Cambiamo unicamente kappa in constant/metal/physicalProperties, da 200 a 50 W/(m K), e lo chiamiamo materiale ipotetico a conducibilità inferiore. Non attribuiamo automaticamente i nuovi parametri a un metallo reale, perché una sostituzione di materiale richiederebbe anche proprietà coerenti di densità e capacità termica.

La sorgente è definita in constant/heater/fvModels:

energySource
{
    type       heatSource;
    cellZone   all;
    q          1e7;
}

In questo dizionario q rappresenta potenza per unità di volume (W/m³), non la potenza totale. Per determinare quest’ultima occorre conoscere il volume effettivo della regione riscaldatore nella mesh: Q̇_gen = q‴ · V_heater se la sorgente è uniforme e applicata a tutte le celle della regione.

Approfondimento · thermoType dei solidi e del fluido
VoceFluidoMetallo e riscaldatore
typeheRhoThermoheSolidThermo
transportconst (μ, Pr → κ = μ cp/Pr)constIsoSolid (κ isotropa costante)
thermohConst (cp)eConst (Cv)
equationOfStaterhoConstrhoConst
energysensibleEnthalpysensibleInternalEnergy

Per i solidi la capacità termica si legge sotto Cv, non Cp: con densità costante le due coincidono. Nel fluido l’entalpia di hConst è cp(T − Tref) più il contributo p/ρ dell’equazione di stato rhoConst, con Tref = 298,15 K se non specificato (sorgente hConstThermo.C); il termine p/ρ è trascurabile qui perché le pressioni relative sono minime.

Approfondimento · Diffusività ed effusività: chi impone la temperatura al contatto
Materialeκ [W/(m K)]ρc [J/(m³ K)]α [m²/s]e = √(κρc)
acqua0,6064,18·10⁶1,45·10⁻⁷1 591
metallo (κ = 200)2002,43·10⁶8,23·10⁻⁵22 045
metallo (κ = 50)502,43·10⁶2,06·10⁻⁵11 023
riscaldatore803,60·10⁶2,22·10⁻⁵16 971

Quando due corpi a temperature diverse vengono messi a contatto, la temperatura dell’interfaccia è la media delle due pesata con le effusività. Il metallo ha un’effusività circa 14 volte quella dell’acqua: nei transitori impone la propria temperatura all’interfaccia e l’acqua si adegua. La diffusività dice invece quanto rapidamente il calore si propaga all’interno: nell’alluminio 570 volte più in fretta che nell’acqua, ed è per questo che la parete si uniforma in frazioni di secondo mentre l’acqua resta fredda a pochi millimetri dalla parete.

Approfondimento · heatSource: q per unità di volume oppure Q totale

Nel sorgente della versione 14 il modello heatSource (src/fvModels/general/heatSource) richiede o la potenza totale Q [W] o la densità di potenza q [W/m³], entrambe funzioni del tempo, applicate alla cellZone indicata (all significa tutte le celle della regione). Con q la potenza effettiva dipende dal volume della mesh; con Q è fissata indipendentemente dalla mesh. In uno studio di mesh conviene quindi una copia con:

energySource
{
    type            heatSource;
    cellZone        all;
    Q               525;
}

così le differenze fra mesh non includono una variazione della potenza iniettata. Va scritta una sola delle due voci: con entrambe il modello non sa quale usare.

Consiglio pratico · Un materiale alternativo coerente

Se interessa un metallo reale meno conduttivo, si cambiano insieme tutte le proprietà. Per un acciaio al carbonio valori tipici da manuale sono κ ≈ 45–50 W/(m K), ρ ≈ 7850 kg/m³, c ≈ 460–490 J/(kg K). In una copia nuova:

// constant/metal/physicalProperties (mixture section)
equationOfState { rho 7850; }
transport       { kappa 50; }
thermodynamics  { hf 0; Cv 470; }

La capacità termica volumica sale da 2,43·10⁶ a 3,69·10⁶ J/(m³ K): nel transitorio il metallo si scalda più lentamente a parità di potenza. Confrontare questo caso con lowConductivity separa l’effetto della conducibilità da quello dell’accumulo, che il caso «ipotetico» mescola.

6. Preparazione da Windows e copia senza sovrascritture

Scaricare il pacchetto della guida. Copiare i file di esercitazione sul server con FileZilla/SFTP oppure eseguire gli script già disponibili sul server. Gli script sono pensati per essere lanciati con python3, bash e gli strumenti di OpenFOAM in ambiente Linux; Notepad++ è adeguato per modificarli da Windows. Assicurarsi che i file .sh utilizzino terminatori di riga Unix (LF).

ssh nomeutente@server-linux
source /opt/OpenFOAM-14/etc/bashrc    # adjust for your installation
foamVersion
python3 prepare_cases.py --output "$HOME/OpenFOAM/CHT_Guida11"

Il programma di preparazione crea tre directory nuove senza modificare il tutorial installato: reference, lowConductivity, lowHeatSource. Per la seconda porta kappa del metallo a 50; per la terza porta la sorgente del riscaldatore a 5×10⁶ W/m³. Se una directory esiste già, la procedura si arresta senza sovrascriverla.

Prima di tutto lavorare sul solo caso reference e verificarne completamente mesh e campi; eseguire gli altri solo dopo questo controllo.

Approfondimento · Script del pacchetto: prepare_cases.py
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Create isolated CHT tutorial variants without changing existing work."""
import argparse
import os
from pathlib import Path
import re
import shutil
import sys

CASES = {
    "reference": None,
    "lowConductivity": ("constant/metal/physicalProperties", "kappa", "50"),
    "lowHeatSource": ("constant/heater/fvModels", "q", "5e6"),
}


def replace_dictionary_scalar(file_path: Path, key: str, value: str) -> None:
    text = file_path.read_text(encoding="utf-8")
    pattern = rf"(?m)^(\s*{re.escape(key)}\s+)([0-9eE.+-]+)(\s*;)"
    matches = list(re.finditer(pattern, text))
    if len(matches) != 1:
        raise ValueError(f"Expected exactly one {key} scalar entry in {file_path}, found {len(matches)}")
    updated = re.sub(pattern, lambda match: match.group(1) + value + match.group(3), text, count=1)
    file_path.write_text(updated, encoding="utf-8")


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--output", required=True, type=Path, help="Directory containing NEW case folders")
    parser.add_argument("--tutorial", type=Path, help="Optional explicit path to the official heatedDuct case")
    args = parser.parse_args()
    tutorials = os.environ.get("FOAM_TUTORIALS", "")
    tutorial = args.tutorial or (Path(tutorials) / "multiRegion/CHT/heatedDuct" if tutorials else None)
    if not tutorial or not tutorial.is_dir():
        parser.error("Official heatedDuct tutorial not found; source OpenFOAM and set FOAM_TUTORIALS or use --tutorial")
    expected = ["system/controlDict", "constant/metal/physicalProperties", "constant/heater/fvModels", "0/fluid/T"]
    if any(not (tutorial / name).is_file() for name in expected):
        parser.error("The template is not the expected OpenFOAM Foundation 14 heatedDuct case")
    out = args.output.expanduser().resolve()
    if any((out / case).exists() for case in CASES):
        parser.error("One or more destination case directories already exist; refusing to overwrite")
    out.mkdir(parents=True, exist_ok=True)
    created = []
    try:
        for name, change in CASES.items():
            destination = out / name
            shutil.copytree(tutorial, destination, symlinks=False)
            created.append(destination)
            if change:
                relative, key, value = change
                replace_dictionary_scalar(destination / relative, key, value)
            (destination / "GUIDE11_CASE.txt").write_text(
                "Guide 11 (OpenFOAM Foundation 14)\n"
                f"Case name: {name}\n"
                f"Official source: {tutorial.resolve()}\n"
                f"Modification: {change or 'none'}\n"
                "Generated from a copy; upstream tutorial was not modified.\n",
                encoding="utf-8",
            )
            print(f"Prepared {destination}")
    except Exception as exc:
        # Preserve all created data to support forensic review; never clean up silently.
        print(f"Preparation stopped: {exc}. Existing copies were left in place.", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
  • replace_dictionary_scalar cerca una riga che inizi con la chiave (kappa o q) seguita da un numero e da «;» e pretende che ce ne sia esattamente una; altrimenti si ferma. Nei file ufficiali kappa 200; e q 1e7; compaiono una sola volta.
  • Ogni copia riceve un file GUIDE11_CASE.txt con origine e modifica: gli script di mesh e soluzione lo pretendono, quindi non possono essere lanciati per errore sul tutorial installato.
  • Se una qualsiasi delle tre destinazioni esiste, non viene creato nulla. In caso di errore a metà, le copie già create restano per l’analisi: lo script non cancella mai.
  • Eseguito sui file del repository ufficiale di OpenFOAM 14, diff -r reference lowConductivity mostra soltanto kappa 200; → kappa 50; e il file descrittivo; diff -r reference lowHeatSource soltanto q 1e7; → q 5e6;. Una seconda esecuzione con la stessa destinazione termina con codice 2.
Consiglio pratico · Percorso di installazione e preparazione dell’ambiente

Il comando source /opt/OpenFOAM-14/etc/bashrc va adattato: con i pacchetti Ubuntu distribuiti da openfoam.org il percorso tipico è /opt/openfoam14/etc/bashrc (minuscolo, senza trattino); con una compilazione da sorgente è nella cartella dell’utente. Per scoprirlo e verificarlo:

ls -d /opt/openfoam* /opt/OpenFOAM* 2>/dev/null
source /opt/openfoam14/etc/bashrc     # adjust to what ls shows
foamVersion
echo "$FOAM_TUTORIALS"
ls "$FOAM_TUTORIALS/multiRegion/CHT/heatedDuct"

Lo script di preparazione va lanciato indicando il suo percorso (python3 scripts/prepare_cases.py dalla cartella del pacchetto, come nel README) oppure entrando prima nella cartella scripts, come nel comando della guida.

Consiglio pratico · Terminatori di riga e controllo sintattico
cd ~/guide11_kit
file scripts/*.sh          # must NOT report "with CRLF line terminators"
bash -n scripts/mesh_case.sh && bash -n scripts/solve_case.sh && echo "syntax OK"
python3 -m py_compile scripts/prepare_cases.py scripts/energy_balance.py && echo "python OK"

Se file segnala CRLF, in Notepad++ usa Modifica → Conversione fine riga → Unix (LF), salva e ritrasferisci con FileZilla in modalità binaria. bash -n controlla la sintassi senza eseguire nulla. py_compile crea una cartella __pycache__ accanto agli script: è innocua.

7. Generazione mesh e controlli preliminari

cd "$HOME/OpenFOAM/CHT_Guida11/reference"
bash /percorso/agliscript/mesh_case.sh "$PWD"

Lo script lancia blockMesh, snappyHexMesh, splitMeshRegions -cellZones all -defaultRegion fluid e poi checkMesh separatamente per fluid, metal e heater. Conserva un log per ogni comando in guide11_logs/ e rifiuta di sovrascrivere un log esistente. Il caso originale non viene toccato.

Controllare la presenza dei tre constant/<region>/polyMesh e delle patch accoppiate; verificare inoltre celle negative, non-ortogonalità e qualità delle interfacce. Non considerare sufficiente un checkMesh riuscito se risultano superfici scoperte o accoppiamenti errati. La visualizzazione delle tre regioni in ParaView deve confermare la geometria.

Approfondimento · Script del pacchetto: mesh_case.sh
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-or-later
# Mesh a fresh OpenFOAM Foundation 14 heatedDuct case and retain all logs.
set -euo pipefail
if [[ $# -ne 1 ]]; then
    echo "Usage: bash mesh_case.sh /absolute/path/to/case" >&2
    exit 2
fi
case_dir="$(realpath -- "$1")"
[[ -f "$case_dir/GUIDE11_CASE.txt" ]] || { echo "Not a prepared Guide 11 case: $case_dir" >&2; exit 2; }
command -v blockMesh >/dev/null || { echo "Source the OpenFOAM 14 environment first" >&2; exit 2; }
command -v snappyHexMesh >/dev/null || exit 2
command -v splitMeshRegions >/dev/null || exit 2
command -v checkMesh >/dev/null || exit 2
cd "$case_dir"
if [[ -d constant/fluid/polyMesh || -d constant/metal/polyMesh || -d constant/heater/polyMesh ]]; then
    echo "Regional meshes already exist; refusing to rebuild the case in place" >&2
    exit 1
fi
mkdir -p guide11_logs
run_logged() {
    local output="$1"; shift
    if [[ -e "$output" ]]; then
        echo "Existing log $output; refusing to overwrite" >&2
        return 1
    fi
    printf 'Running:'; printf ' %q' "$@"; printf '\n'
    "$@" > >(tee "$output") 2> >(tee -a "$output" >&2)
}
run_logged guide11_logs/mesh_blockMesh.log blockMesh
run_logged guide11_logs/mesh_snappyHexMesh.log snappyHexMesh
run_logged guide11_logs/mesh_splitMeshRegions.log splitMeshRegions -cellZones all -defaultRegion fluid
for region in fluid metal heater; do
    if [[ ! -f "constant/$region/polyMesh/boundary" ]]; then
        echo "No region mesh boundary file for $region" >&2
        exit 1
    fi
    run_logged "guide11_logs/checkMesh_${region}.log" checkMesh -region "$region"
done
printf 'Regional meshes created and checked in %s\n' "$case_dir"
  • Il controllo su GUIDE11_CASE.txt garantisce che il caso sia una copia preparata; quello sulle tre polyMesh impedisce di rifare la mesh sopra una esistente.
  • run_logged stampa il comando, scrive l’uscita a video e nel log con tee (anche gli errori, con tee -a) e si rifiuta di riusare un log esistente.
  • Dopo splitMeshRegions verifica che esista constant/<regione>/polyMesh/boundary per ciascuna regione prima di lanciare checkMesh -region.
Consiglio pratico · Il volume del riscaldatore e la potenza reale
cd "$HOME/OpenFOAM/CHT_Guida11/reference"
grep -E "cells:|Total volume" guide11_logs/checkMesh_fluid.log guide11_logs/checkMesh_metal.log guide11_logs/checkMesh_heater.log

checkMesh stampa il numero di celle e, nel controllo geometrico, il volume totale della regione. Atteso per il riscaldatore: circa 5,25·10⁻⁵ m³. La potenza effettiva è Q̇gen = 10⁷ × Vheater: uno scarto del 2 % nel volume è uno scarto del 2 % nella potenza, da usare nel bilancio al posto dei 525 W nominali. Annota anche i volumi di fluid e metal: servono per le capacità termiche della sezione 10.

Consiglio pratico · Che cosa guardare in checkMesh per ogni regione
  • «Mesh OK» alla fine di ciascuno dei tre log; in caso contrario leggere quali controlli falliscono.
  • Non-ortogonalità massima: le celle tagliate da snappyHexMesh vicino alle interfacce sono le più distorte; oltre circa 70° servono correttori non ortogonali.
  • Elenco delle patch: nel fluido fluidInlet, fluidOutlet, fluid_to_metal; nel metallo metalInlet, metalOutlet, metalExternal, metal_to_fluid, metal_to_heater; nel riscaldatore heaterInlet, heaterOutlet, heaterExternal, heater_to_metal.
  • Patch d’ingresso, d’uscita o d’interfaccia con zero facce, oppure regioni con un numero di celle diverso fra le tre copie preparate (che usano la stessa geometria e lo stesso dizionario di mesh): indicano un problema di mesh, non di fisica.

8. Avvio della soluzione e lettura dei log

bash /percorso/agliscript/solve_case.sh "$HOME/OpenFOAM/CHT_Guida11/reference"
tail -n 35 "$HOME/OpenFOAM/CHT_Guida11/reference/guide11_logs/solve_foamMultiRun.log"

Lo script controlla la presenza delle mesh regionali e si interrompe se il file di log esiste già: non è uno strumento per il riavvio indiscriminato di simulazioni parziali. Per sessioni persistenti usare tmux, come nella Guida 1. I dati e i log possono essere trasferiti a Windows con FileZilla.

Il log deve essere osservato per eventuali errori di accoppiamento interfaccia, divergenze, residui energetici, numero di Courant e tempi di calcolo. I residui numerici non misurano direttamente la conservazione dell’energia; questa richiede una verifica separata.

Approfondimento · Script del pacchetto: solve_case.sh
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-or-later
# Run a meshed CHT tutorial case once; do not silently restart old solutions.
set -euo pipefail
if [[ $# -ne 1 ]]; then
    echo "Usage: bash solve_case.sh /absolute/path/to/case" >&2
    exit 2
fi
case_dir="$(realpath -- "$1")"
[[ -f "$case_dir/GUIDE11_CASE.txt" ]] || { echo "Not a prepared Guide 11 case" >&2; exit 2; }
command -v foamMultiRun >/dev/null || { echo "Source the OpenFOAM 14 environment first" >&2; exit 2; }
cd "$case_dir"
for region in fluid metal heater; do
    [[ -f "constant/$region/polyMesh/boundary" ]] || { echo "Missing $region mesh" >&2; exit 1; }
done
mkdir -p guide11_logs
log="guide11_logs/solve_foamMultiRun.log"
if [[ -e "$log" ]]; then
    echo "Solver log already exists; refusing to overwrite or restart" >&2
    exit 1
fi
# Use one serial solver process; the official Allrun is a separate MPI workflow.
foamMultiRun > >(tee "$log") 2> >(tee -a "$log" >&2)
printf 'Solver finished; log: %s/%s\n' "$case_dir" "$log"

Lo script controlla il file descrittivo, la disponibilità di foamMultiRun e le tre mesh regionali, poi esegue il calcolo seriale una sola volta, salvando uscita ed errori in guide11_logs/solve_foamMultiRun.log. Il commento finale ricorda che l’Allrun ufficiale è un percorso MPI distinto.

Approfondimento · Misurare durante il calcolo: un file system/functions per il CHT

Le grandezze del bilancio si misurano meglio con OpenFOAM stesso, a ogni passo e sulla mesh effettiva. In una copia preparata e già con la mesh, prima di solve_case.sh, salva questo file come system/functions (OpenFOAM 14 lo legge automaticamente):

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

// Mass flow rate [kg/s]: in the fluid module phi is a mass flux.
#includeFunc patchFlowRate(name=mdotIn, region=fluid, patch=fluidInlet)
#includeFunc patchFlowRate(name=mdotOut, region=fluid, patch=fluidOutlet)

// Signed enthalpy flow sum(phi*h) [W]: negative on the inlet, positive on the outlet.
// Q_fluid = hFlowOut + hFlowIn
hFlowIn
{
    type            surfaceFieldValue;
    libs            ("libfieldFunctionObjects.so");
    writeControl    timeStep;
    writeInterval   1;
    log             yes;
    writeFields     false;
    region          fluid;
    patch           fluidInlet;
    operation       sum;
    weightField     phi;
    fields          (h);
}

hFlowOut
{
    type            surfaceFieldValue;
    libs            ("libfieldFunctionObjects.so");
    writeControl    timeStep;
    writeInterval   1;
    log             yes;
    writeFields     false;
    region          fluid;
    patch           fluidOutlet;
    operation       sum;
    weightField     phi;
    fields          (h);
}

// Mass-flow-weighted (bulk) temperatures [K]: sum(phi*T)/sum(phi).
TbulkIn
{
    type            surfaceFieldValue;
    libs            ("libfieldFunctionObjects.so");
    writeControl    timeStep;
    writeInterval   1;
    log             yes;
    writeFields     false;
    region          fluid;
    patch           fluidInlet;
    operation       average;
    weightField     phi;
    fields          (T);
}

TbulkOut
{
    type            surfaceFieldValue;
    libs            ("libfieldFunctionObjects.so");
    writeControl    timeStep;
    writeInterval   1;
    log             yes;
    writeFields     false;
    region          fluid;
    patch           fluidOutlet;
    operation       average;
    weightField     phi;
    fields          (T);
}

// Volume-averaged temperatures [K]: stored energy E = rho*c*V*Tavg + const.
#includeFunc volAverage(name=TavgFluid, region=fluid, T)
#includeFunc volAverage(name=TavgMetal, region=metal, T)
#includeFunc volAverage(name=TavgHeater, region=heater, T)

// Hottest cell of the heater [K].
#includeFunc cellMax(name=TmaxHeater, region=heater, T)

// Heat flow through every wall patch of each region [W], positive = into the region.
// Interface check: Q(fluid_to_metal) = -Q(metal_to_fluid), Q(metal_to_heater) = -Q(heater_to_metal).
#includeFunc wallHeatFlux(name=qFluid, region=fluid, writeFields=false, log=yes)
#includeFunc wallHeatFlux(name=qMetal, region=metal, writeFields=false, log=yes)
#includeFunc wallHeatFlux(name=qHeater, region=heater, writeFields=false, log=yes)

// ************************************************************************* //
  • patchFlowRate somma il flusso phi sulla patch: nel modulo fluid è un flusso di massa, quindi il risultato è ṁ in kg/s (negativo all’ingresso).
  • Con operation sum e weightField phi la surfaceFieldValue calcola Σ φf hf, il flusso di entalpia con segno. Q̇fluid = hFlowOut + hFlowIn è la potenza netta portata via dall’acqua, già corretta per eventuali riflussi.
  • Con operation average e weightField phi si ottiene Σ φT/Σ φ: la temperatura media pesata dalla portata della sezione 9.
  • wallHeatFlux per regione dà la potenza su ogni patch di parete, positiva se entra nella regione: Q(fluid_to_metal) è il calore che entra nell’acqua, e deve valere −Q(metal_to_fluid).

Operazioni e pesi sono verificati nel sorgente di surfaceFieldValue della versione 14; la sintassi riprende i tutorial ufficiali wallBoiling, hotWire e vacuumFurnace. All’avvio il log deve elencare la creazione di ogni function object e, a ogni passo, i valori. I file finiscono sotto postProcessing/: find postProcessing -name "*.dat" li elenca tutti.

Consiglio pratico · Leggere un log multiregione
L="$HOME/OpenFOAM/CHT_Guida11/reference/guide11_logs/solve_foamMultiRun.log"
grep -c "^Time = " "$L"                    # passi eseguiti
grep "^deltaT = " "$L" | tail -3            # passo attuale
grep "Courant Number" "$L" | tail -3
grep "Solving for h" "$L" | tail -3        # fluido
grep "Solving for e" "$L" | tail -6        # metallo e riscaldatore
grep -i -E "warning|error|nan" "$L" | head

Il passo deve crescere da 10⁻³ s fino al limite imposto dal Courant e poi stabilizzarsi. «Solving for e» compare due volte per passo, una per regione solida. Il tempo di calcolo per secondo simulato, letto da ExecutionTime, permette di stimare la durata di una copia con endTime più lungo prima di lanciarla.

9. Post-processing con ParaView da Windows

Collegare ParaView 5.13.2 al pvserver Linux attraverso la configurazione già descritta nella Guida 1. Per un caso multiregione selezionare ogni regione necessaria e verificare quale lettore OpenFOAM esponga mesh, patch e serie temporale. Non confondere i campi T di regioni differenti se presentati in un’unica vista.

La prima sequenza grafica proposta è: T del riscaldatore → T nel metallo → T del fluido → velocità nel fluido. Creare una Slice longitudinale e una trasversale, applicare la stessa scala di temperatura quando si confrontano i casi, salvare il file .pvsm e annotare tempo fisico, unità, intervalli e regione visualizzata.

Percorso di analisi quantitativa
Percorso di analisi quantitativa

Per la misura dell’energia convettiva occorrono portata massica e temperatura media pesata dalla portata, non semplicemente la media aritmetica delle temperature dei nodi:

T̄_m = [∫ₐ T ρ (U·n) dA] / [∫ₐ ρ (U·n) dA]

Il flusso con segno cambia tra sezione di ingresso e uscita perché le normali uscenti puntano in versi opposti. Controllare eventuali zone di flusso inverso; se presenti, trattarle esplicitamente invece di usare alla cieca una temperatura media unica.

Approfondimento · ParaView con un caso multiregione
  • Crea il file vuoto di apertura nella cartella del caso: touch reference.foam. Aprendolo da ParaView collegato a pvserver il lettore OpenFOAM mostra in «Mesh Regions» le mesh interne e le patch di ogni regione, con il nome della regione come prefisso.
  • Seleziona le tre mesh interne e premi Apply; con Extract Block separa le regioni per colorarle in modo indipendente. Il campo si chiama T in tutte e tre: dopo l’estrazione è chiaro a quale regione appartiene.
  • Il caso è scritto in binario (writeFormat binary): ParaView lo legge senza problemi, ma i file non si possono ispezionare con un editor di testo.
  • Per un calcolo eseguito in parallelo e non ricostruito, imposta «Case Type» su Decomposed Case.
Consiglio pratico · Un profilo che attraversa le tre regioni

Con Plot Over Line da (0,025, −0,010, 0,075) a (0,025, 0,045, 0,075) si attraversano, a metà lunghezza, il riscaldatore, il fondo metallico e l’acqua. La continuità del flusso impone che le pendenze ai due lati di un’interfaccia stiano nel rapporto inverso delle conducibilità: all’interfaccia metallo-acqua il gradiente nell’acqua deve essere circa 330 volte quello nell’alluminio (200/0,606). Nel grafico la temperatura appare quasi piatta nel metallo e ripida nell’acqua: se il salto fra le due regioni non è continuo, l’accoppiamento non funziona.

Approfondimento · La temperatura media di miscelamento calcolata da OpenFOAM

La formula della guida coincide con quanto calcolano le function object TbulkIn e TbulkOut (sezione 8): sulle facce di bordo φf = ρ (U·n) Af, e Σ φfTf/Σ φf è la versione discreta di ∫ρT(U·n)dA/∫ρ(U·n)dA. Per un tempo già scritto, senza rilanciare il calcolo, si può usare foamPostProcess sulla sola regione fluida, con il solutore esplicito perché phi sia disponibile:

cd "$HOME/OpenFOAM/CHT_Guida11/reference"
foamPostProcess -region fluid -solver fluid -latestTime \
  -func "patchFlowRate(patch=fluidOutlet)" > guide11_logs/post_mdot_outlet.log 2>&1
grep -A3 "patchFlowRate" guide11_logs/post_mdot_outlet.log

Il modulo -solver è necessario anche per wallHeatFlux (Guida 10): senza, foamPostProcess legge i campi ma non costruisce il modello di trasporto termico. Per le regioni solide si usa -region metal -solver solid.

10. Bilancio energetico e confronto quantitativo

In un intervallo stazionario, con proprietà e portate note, la potenza acquisita dal fluido è approssimabile con:

Q̇_fluid ≈ ṁ · cₚ · (T̄_out − T̄_in)

Per un sistema isolato dall’esterno e con accumulo energetico trascurabile, ci si aspetta Q̇_gen ≈ Q̇_fluid. Per questo tutorial l’energia può essere ancora immagazzinata nei solidi e nel fluido: nel transitorio il bilancio corretto include la derivata dell’energia interna e le eventuali perdite attraverso le superfici esterne:

Q̇_gen − Q̇_fluid − Q̇_ext = dE_stored/dt

Non interpretare uno scostamento istantaneo come errore solver senza prima verificare gli accumuli. Lo script energy_balance.py accetta un CSV di misure effettive con m_dot_kg_s, T_in_K, T_out_K, cp_J_kgK, q_gen_W, q_ext_W e dE_dt_W, e calcola il residuo di bilancio con convenzioni dichiarate. Il file CSV di esempio è solo dimostrativo, non proviene da OpenFOAM.

Curva transitoria illustrativa, non CFD reale
Curva transitoria illustrativa, non CFD reale
Approfondimento · Che cosa aspettarsi dal bilancio a t = 20 s
TermineStimaCommento
Q̇gen≈ 525 Wcostante dal primo passo
energia generata in 20 s≈ 10,5 kJQ̇gen × 20 s
ṁ cp(T̄out − T̄in)vicino a zerol’acqua riscaldata non ha ancora raggiunto l’uscita
dEstored/dtquasi tutta Q̇gensolidi e acqua vicina alle pareti
calore all’interfaccia acqua Q(fluid_to_metal)frazione crescenteentra nell’acqua ma vi resta accumulato

Due grandezze spesso confuse vanno quindi tenute distinte: il calore che attraversa la parete verso l’acqua (misurato da wallHeatFlux) e il calore che l’acqua porta fuori dal dominio (ṁ cpΔT). La loro differenza è l’energia accumulata nell’acqua. Solo in regime stazionario coincidono, e coincidono con Q̇gen.

Approfondimento · Raggiungere lo stazionario: tempi e un limite fisico

Lo stazionario richiede alcune costanti di tempo globali (≈ 227 s) più il tempo di attraversamento (150 s): un endTime di 1500–2000 s in una copia nuova. In quelle condizioni il bilancio darebbe ΔT̄ = Q̇gen/(ṁ cp) = 525/(1,6·10⁻³ × 4181) ≈ 78,5 K, cioè acqua in uscita a circa 378 K: oltre l’ebollizione a pressione atmosferica, con pareti ancora più calde. Il modello a densità costante e monofase non può rappresentarlo. Per uno studio stazionario fisicamente sensato si riduce la potenza (per esempio q 1e6, ΔT̄ ≈ 7,9 K) o si aumenta la velocità d’ingresso, sempre in una copia nuova e documentando la scelta.

Nota sulla figura sintetica: la curva «uscita fluido» sale a 312 K in 20 s, mentre le stime indicano una temperatura di miscelamento all’uscita quasi invariata in quell’intervallo. La figura illustra la forma di un transitorio, non una previsione per questo caso.

Approfondimento · Script del pacchetto: energy_balance.py
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Evaluate signed CHT energy balance from measured CSV rows, not CFD files."""
import argparse
import csv
from pathlib import Path
import sys

REQUIRED = ["case", "time_s", "m_dot_kg_s", "T_in_K", "T_out_K", "cp_J_kgK", "q_gen_W", "q_ext_W", "dE_dt_W"]


def parse_float(row, key, number):
    try:
        value = float(row[key])
    except (ValueError, KeyError, TypeError) as exc:
        raise ValueError(f"Row {number}: invalid {key}") from exc
    if not -float("inf") < value < float("inf"):
        raise ValueError(f"Row {number}: {key} is not finite")
    return value


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("input_csv", type=Path)
    parser.add_argument("--output", type=Path, help="New output CSV; existing files are never overwritten")
    args = parser.parse_args()
    target = args.output or args.input_csv.with_name(args.input_csv.stem + "_balance.csv")
    if target.resolve() == args.input_csv.resolve() or target.exists():
        parser.error("Output is the input or already exists: no overwriting")
    with args.input_csv.open(encoding="utf-8-sig", newline="") as source:
        reader = csv.DictReader(source)
        if not reader.fieldnames or any(key not in reader.fieldnames for key in REQUIRED):
            parser.error("Required columns: " + ", ".join(REQUIRED))
        entries = []
        for number, row in enumerate(reader, 2):
            data = {key: parse_float(row, key, number) for key in REQUIRED[1:]}
            q_fluid = data["m_dot_kg_s"] * data["cp_J_kgK"] * (data["T_out_K"] - data["T_in_K"])
            residual = data["q_gen_W"] - q_fluid - data["q_ext_W"] - data["dE_dt_W"]
            denom = max(abs(data["q_gen_W"]), 1e-12)
            entries.append(dict(row, q_fluid_W=f"{q_fluid:.12g}", residual_W=f"{residual:.12g}", residual_percent_source=f"{100*residual/denom:.9g}"))
    with target.open("x", encoding="utf-8", newline="") as out:
        writer = csv.DictWriter(out, fieldnames=reader.fieldnames + ["q_fluid_W", "residual_W", "residual_percent_source"])
        writer.writeheader()
        writer.writerows(entries)
    print(f"Wrote {len(entries)} rows to {target}")
    print("Convention: q_gen - q_fluid - q_ext - dE/dt = residual; positive q_ext is heat leaving the system.")
    print("This arithmetic does not prove that the CFD measurements are correct.")
    return 0


if __name__ == "__main__":
    sys.exit(main())
  • Colonne obbligatorie: case, time_s, m_dot_kg_s, T_in_K, T_out_K, cp_J_kgK, q_gen_W, q_ext_W, dE_dt_W. Per ogni riga calcola qfluid = ṁ cp(Tout − Tin) e il residuo qgen − qfluid − qext − dE/dt, anche in percentuale della sorgente.
  • Senza --output scrive accanto all’ingresso un file con suffisso _balance.csv, rifiutando di sovrascriverlo.
  • Convenzione stampata a fine esecuzione: qext positivo è calore che esce dal sistema.
  • Eseguito sul CSV sintetico del pacchetto produce un file identico a example_measured_values_SYNTHETIC_balance.csv. Il residuo di 0,02 W della riga reference deriva dall’arrotondamento di Tout nel file di esempio, non dal calcolo.
Approfondimento · File di esercitazione del pacchetto

esercitazione/measurement_template.csv

case,time_s,m_dot_kg_s,T_in_K,T_out_K,cp_J_kgK,q_gen_W,q_ext_W,dE_dt_W

esercitazione/example_measured_values_SYNTHETIC.csv (valori inventati)

case,time_s,m_dot_kg_s,T_in_K,T_out_K,cp_J_kgK,q_gen_W,q_ext_W,dE_dt_W
reference,10,0.004,300,309.565893326,4181,1000,100,740
lowConductivity,10,0.004,300,305.979430758,4181,1000,100,800
lowHeatSource,10,0.004,300,305.979430758,4181,500,50,350

esercitazione/example_measured_values_SYNTHETIC_balance.csv (uscita attesa)

case,time_s,m_dot_kg_s,T_in_K,T_out_K,cp_J_kgK,q_gen_W,q_ext_W,dE_dt_W,q_fluid_W,residual_W,residual_percent_source
reference,10,0.004,300,309.565893326,4181,1000,100,740,159.979999984,0.0200000159764,0.0020000016
lowConductivity,10,0.004,300,305.979430758,4181,1000,100,800,99.9999999968,3.20835624734e-09,3.20835625e-10
lowHeatSource,10,0.004,300,305.979430758,4181,500,50,350,99.9999999968,3.20835624734e-09,6.41671249e-10

esercitazione/README_IT_EN.md

# Guida 11 — Materiale di esercitazione / Exercise kit

**IT** — Questa cartella contiene tre strumenti: `prepare_cases.py` (copia il tutorial ufficiale e prepara tre casi indipendenti), `mesh_case.sh` (costruisce e controlla le mesh regionali) e `solve_case.sh` (esegue la soluzione seriale con `foamMultiRun`). `energy_balance.py` elabora esclusivamente misure registrate in un CSV; non legge automaticamente i campi OpenFOAM. I file sorgente e tutti i commenti nel codice sono in inglese. Gli script non sovrascrivono log, simulazioni o file di output già esistenti. Occorre avere OpenFOAM Foundation 14 attivato nel terminale Linux.

**EN** — This directory contains `prepare_cases.py` (copies the upstream tutorial and prepares three independent cases), `mesh_case.sh` (generates and checks the region meshes), and `solve_case.sh` (runs the serial solution using `foamMultiRun`). `energy_balance.py` processes values that **you measured and entered** into CSV; it does not automatically extract fields from OpenFOAM. The scripts do not overwrite prior cases, solver logs or outputs. Source OpenFOAM Foundation 14 in the Linux shell before running the scripts.

```bash
source /opt/OpenFOAM-14/etc/bashrc
python3 scripts/prepare_cases.py --output "$HOME/OpenFOAM/CHT_Guide11"
bash scripts/mesh_case.sh "$HOME/OpenFOAM/CHT_Guide11/reference"
bash scripts/solve_case.sh "$HOME/OpenFOAM/CHT_Guide11/reference"
python3 scripts/energy_balance.py esercitazione/example_measured_values_SYNTHETIC.csv
```

**Warning:** the output CSV generated by the last command is based on **synthetic test values**, not on a completed CFD simulation. The real heater power depends on the *actual* heater region volume and mesh. The transient energy balance must include internal energy storage and external losses where applicable. The included `measurement_template.csv` is intentionally empty and requires measured values.

**Official case:** https://github.com/OpenFOAM/OpenFOAM-14/tree/master/tutorials/multiRegion/CHT/heatedDuct
**Licence for new scripts:** GNU GPL v3 or later. The upstream OpenFOAM case is **not redistributed** in this bundle; it is copied from the user's installed OpenFOAM tutorial directory.
Approfondimento · Script aggiuntivo: cht_balance_series.py

Calcola il bilancio transitorio passo per passo a partire dai file delle function object della sezione 8. Con densità e calori specifici costanti l’energia di una regione è E = ρcV·T̄vol + costante, quindi la derivata si ottiene dalle temperature medie con differenze centrate. Le capacità C = ρcV si calcolano con i volumi misurati da checkMesh:

cd "$HOME/OpenFOAM/CHT_Guida11/reference"
find postProcessing -name "*.dat"
# example with nominal volumes: fluid 1000*4181*2.4e-4, metal 2700*900*1.35e-4, heater 8000*450*5.25e-5
python3 cht_balance_series.py --qgen 525 \
  --hin  <path>/hFlowIn/.../surfaceFieldValue.dat \
  --hout <path>/hFlowOut/.../surfaceFieldValue.dat \
  --region fluid:<path>/TavgFluid/.../volFieldValue.dat:1003.4 \
  --region metal:<path>/TavgMetal/.../volFieldValue.dat:328.1 \
  --region heater:<path>/TavgHeater/.../volFieldValue.dat:189.0 \
  --every 20 --output ../reference_balance_series.csv
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Transient energy balance of a CHT run from OpenFOAM function-object files.

Inputs are the .dat files written by the monitoring functions of Guide 11:
  --hin / --hout   surfaceFieldValue 'sum' of h weighted by phi on the fluid
                   inlet and outlet [W] (signed: negative on the inlet);
  --region         NAME:FILE:C  volume-averaged temperature of a region and its
                   heat capacity C = rho*c*V [J/K] (V measured with checkMesh).
With constant density and specific heat the stored energy of a region is
E = C * T_avg + const, so dE/dt follows from finite differences in time.
Balance: residual = Q_gen - Q_fluid - Q_ext - dE/dt, Q_fluid = hout + hin.
The first column of each file is time, the selected column (default 2) the
value. A NEW output CSV is written; existing files are never overwritten.
"""
import argparse
import csv
from pathlib import Path
import sys


def read_series(path, column):
    t, v = [], []
    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.replace('(', ' ').replace(')', ' ').split()
            try:
                t.append(float(parts[0]))
                v.append(float(parts[column - 1]))
            except (IndexError, ValueError) as exc:
                raise ValueError(f'{path}:{number}: cannot read column {column}') from exc
    if len(t) < 3:
        raise ValueError(f'{path}: at least three records are needed')
    pairs = sorted(dict(zip(t, v)).items())
    return [p[0] for p in pairs], [p[1] for p in pairs]


def interp(ts, vs, x):
    if x <= ts[0]:
        return vs[0]
    if x >= ts[-1]:
        return vs[-1]
    lo, hi = 0, len(ts) - 1
    while hi - lo > 1:
        mid = (lo + hi) // 2
        if ts[mid] <= x:
            lo = mid
        else:
            hi = mid
    w = (x - ts[lo]) / (ts[hi] - ts[lo])
    return vs[lo] + w * (vs[hi] - vs[lo])


def main():
    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument('--qgen', type=float, required=True, help='heater power [W] = q * V_heater')
    p.add_argument('--qext', type=float, default=0.0, help='heat leaving through external walls [W]')
    p.add_argument('--hin', type=Path, required=True)
    p.add_argument('--hout', type=Path, required=True)
    p.add_argument('--region', action='append', required=True, help='NAME:FILE:C, repeat for each region')
    p.add_argument('--column', type=int, default=2, help='value column in the .dat files')
    p.add_argument('--every', type=int, default=1, help='keep one output row every N time records')
    p.add_argument('--output', type=Path, required=True)
    a = p.parse_args()
    if a.output.exists():
        p.error('Output already exists; refusing to overwrite it')
    hin = read_series(a.hin, a.column)
    hout = read_series(a.hout, a.column)
    regions = []
    for spec in a.region:
        try:
            name, file_name, cap = spec.rsplit(':', 2)
            regions.append((name, read_series(Path(file_name), a.column), float(cap)))
        except ValueError as exc:
            p.error(f'bad --region {spec}: {exc}')
    times = regions[0][1][0]
    energy = [sum(c * interp(s[0], s[1], t) for _, s, c in regions) for t in times]
    rows = []
    for i in range(1, len(times) - 1, max(1, a.every)):
        t = times[i]
        dedt = (energy[i + 1] - energy[i - 1]) / (times[i + 1] - times[i - 1])
        q_fluid = interp(*hout, t) + interp(*hin, t)
        residual = a.qgen - q_fluid - a.qext - dedt
        rows.append(dict(time_s=f'{t:.6g}', Q_gen_W=f'{a.qgen:.6g}', Q_fluid_W=f'{q_fluid:.6g}',
                         dE_dt_W=f'{dedt:.6g}', residual_W=f'{residual:.6g}',
                         residual_percent=f'{100 * residual / a.qgen:.4g}' if a.qgen else 'nan'))
    with a.output.open('x', newline='', encoding='utf-8') as f:
        w = csv.DictWriter(f, fieldnames=list(rows[0]))
        w.writeheader()
        w.writerows(rows)
    last = rows[-1]
    print(f'{len(rows)} rows written to {a.output}')
    print(f"Last row t = {last['time_s']} s: Q_fluid = {last['Q_fluid_W']} W, dE/dt = {last['dE_dt_W']} W, "
          f"residual = {last['residual_W']} W ({last['residual_percent']} % of Q_gen)")
    print('Central differences need closely spaced records; check the residual for sensitivity to --every.')


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

Provato su serie analitiche costruite in modo che la somma dei termini sia nulla: il residuo calcolato resta sotto 10⁻³ W su 525 W. Sui dati reali un residuo di pochi percento nei primi passi è normale (passi piccoli, differenze centrate su un transitorio rapido); un residuo che non si riduce con passi più fini indica una misura sbagliata o un problema di conservazione all’interfaccia, da controllare con i valori di wallHeatFlux delle due regioni. Sostituisci <path> con i percorsi elencati da find.

11. Tre esercitazioni progressive

A — Caso ufficiale. Creare reference, generare mesh, eseguire la soluzione, visualizzare le tre regioni e registrare temperature, portata e volume del riscaldatore. Salvare i log e lo stato ParaView.

B — Conducibilità del metallo. Confrontare reference e lowConductivity. Riportare profilo termico del metallo, temperatura media del fluido in uscita e potenza trasferita. Formulare un’ipotesi prima di confrontare i risultati: una minore conducibilità non implica necessariamente una diminuzione monotona di ogni temperatura locale in regime transitorio.

C — Potenza termica del riscaldatore. Confrontare reference e lowHeatSource. Confrontare potenza volumetrica, temperatura massima, energia accumulata e temperatura in uscita. Le temperature non sono obbligate a variare linearmente se il campo convettivo o le proprietà fisiche cambiano.

Casok metal [W/(m K)]q‴ heater [W/m³]T_out [K]Q̇_fluid [W]Stato
reference2001,0×10⁷da misurareda misurarenon eseguito
lowConductivity501,0×10⁷da misurareda misurarenon eseguito
lowHeatSource2005,0×10⁶da misurareda misurarenon eseguito
Approfondimento · Esercizio C come test di verifica: il problema è lineare

In questo tutorial tutte le proprietà sono costanti, la gravità è nulla e la densità non dipende da T: il campo di moto è identico nei tre casi e l’equazione dell’energia è lineare nella differenza T − 300 K, con sorgente proporzionale a q‴. Anche i passi temporali coincidono, perché dipendono solo dalla velocità. Ne segue una previsione esatta: in lowHeatSource ogni incremento di temperatura deve essere esattamente metà di quello di reference, in ogni cella e a ogni istante.

(T_lowHeatSource - 300) / (T_reference - 300) = 0.5

Lo scostamento misurato, per esempio su Tmax del riscaldatore e sulle temperature medie delle function object, dà direttamente l’ordine dell’errore delle tolleranze dei solutori lineari. La guida osserva giustamente che le temperature non variano linearmente se il campo convettivo o le proprietà cambiano: qui, per costruzione, non cambiano, e il test sfrutta proprio questo. Se lo scostamento supera qualche millesimo, controlla che le due copie differiscano davvero solo per q (diff -r).

Consiglio pratico · Una scheda di misura più completa
CasoVheater [m³]Q̇gen [W]Tmax,heater [K]T̄out [K]Q(fluid_to_metal) [W]ṁcpΔT [W]dE/dt [W]residuo [%]
reference……………………
lowConductivity……………………
lowHeatSource……………………

Compila una riga per ogni caso allo stesso istante (t = 20 s) e indica sempre: istante, tolleranze, passo massimo, volumi da checkMesh. Per l’esercizio C aggiungi il rapporto (T − 300)/(Tref − 300) e per l’esercizio B le temperature medie della parete inferiore e di quella superiore, che misurano l’effetto «aletta».

12. Errori frequenti e loro interpretazione

SintomoPossibile causaAzione di controllo
Unknown patchNomi incoerenti fra mesh e campoControllare constant/<region>/polyMesh/boundary
Temperatura discontinua in interfacciaCoppia di patch o condizione errataVerificare coupledTemperature su entrambi i lati
Sorgente energetica sproporzionataConfusione fra W e W/m³Misurare il volume della regione heater
Gravi squilibri energetici inizialiAccumulo transitorio omessoStimare dE_stored/dt
Confronto inconcludenteScale colore/tempi differentiIdentificare istante fisico e range comune
Divergenza numericaMesh, time step o accoppiamentoAnalizzare checkMesh, Courant e log senza cambiare tutto insieme

Richiamo facoltativo — contatto termico: il tutorial utilizza accoppiamento ideale. Una resistenza di contatto reale va modellata e documentata esplicitamente; non è equivalente a diminuire arbitrariamente la conducibilità del volume metallico.

Approfondimento · Altri errori tipici dei casi multiregione
SintomoCausa probabileControllo
«cannot find file … constant/polyMesh/…»utilità lanciata senza -regioncheckMesh -region metal, foamPostProcess -region fluid
«Unable to find thermophysicalTransportModel»foamPostProcess senza -solver-region fluid -solver fluid, -region metal -solver solid
ParaView mostra una sola regioneselezione in «Mesh Regions»seleziona le tre mesh interne
passo temporale che non cresceCourant massimo in poche celle distortecelle con Co massimo, checkMesh
riscaldatore che non si scaldafvModels nella regione sbagliata o cellZone erratail log deve riportare la creazione di energySource
valori negativi di ṁ all’uscitariflussosegno di phi sulla patch, inletOutlet
Consiglio pratico · Cambiare una cosa per volta

In caso di divergenza o di risultati incoerenti, crea una copia nuova per ogni tentativo e annota la modifica nel file GUIDE11_CASE.txt (aggiungendo una riga, senza cancellare le precedenti). Le prove tipiche, nell’ordine: maxDeltaT più piccolo; nOuterCorrectors maggiore; correttori non ortogonali nelle regioni con celle distorte; mesh più fine. Cambiare più parametri insieme rende impossibile capire quale abbia agito.

13. Checklist finale e consegna scientifica

  • ☐ Versione foamVersion registrata e tutorial originale conservato.
  • ☐ Geometria, regioni e interfacce controllate; checkMesh letto criticamente.
  • ☐ Parametri kappa, q, proprietà del fluido e tempo finale registrati.
  • ☐ Log conservati senza sovrascrivere risultati precedenti.
  • ☐ ParaView mostra tutte le regioni con scala e tempo documentati.
  • ☐ Temperature in ingresso/uscita pesate per portata, potenza della sorgente e accumulo valutati con unità coerenti.
  • ☐ I tre casi confrontati soltanto dopo aver verificato quello di riferimento.
  • ☐ CSV, file .pvsm, immagini e commento sui limiti scientifici archiviati.
Consiglio pratico · Controlli aggiuntivi per un lavoro di ricerca
  • Volume del riscaldatore misurato e Q̇gen calcolato con quel volume.
  • Conservazione all’interfaccia: Q(fluid_to_metal) + Q(metal_to_fluid) ≈ 0 e Q(metal_to_heater) + Q(heater_to_metal) ≈ 0.
  • Distinzione dichiarata fra calore entrato nell’acqua e calore portato fuori dall’acqua.
  • Test di linearità dell’esercizio C superato, con lo scostamento riportato.
  • Sensibilità al passo temporale (maxDeltaT dimezzato) documentata.
  • Limiti del modello dichiarati: gravità nulla, proprietà costanti, nessuna ebollizione, contatto ideale.

14. Fonti ufficiali e limiti

Stato di verifica: i contenuti e i file chiave del tutorial sono stati confrontati con il repository ufficiale. Gli script allegati sono stati controllati sintatticamente e su copie fittizie. Tutti i grafici didattici allegati sono schematici o generati da valori sintetici.

Approfondimento · Letture di riferimento e altri tutorial CHT della versione 14
  • S. V. Patankar, Numerical Heat Transfer and Fluid Flow, Hemisphere, 1980, Routledge: discretizzazione ai volumi finiti, media armonica delle conducibilità alle interfacce;
  • T. L. Bergman, A. S. Lavine, F. P. Incropera, D. P. DeWitt, Fundamentals of Heat and Mass Transfer, 8th ed., Wiley, 2017, Wiley: resistenze termiche, alette, transitori conduttivi, convezione in condotti;
  • R. K. Shah, A. L. London, Laminar Flow Forced Convection in Ducts, Academic Press, 1978, Elsevier: soluzioni di riferimento per condotti a sezione quadrata e lunghezze d’ingresso;
  • J. H. Ferziger, M. Perić, R. L. Street, Computational Methods for Fluid Dynamics, 4th ed., Springer, 2020, doi:10.1007/978-3-319-99693-6: accoppiamento segregato e passo temporale.

La cartella $FOAM_TUTORIALS/multiRegion/CHT della versione 14 contiene, oltre a heatedDuct: VoFcoolingCylinder2D, circuitBoardCooling, coolingCylinder2D, coolingSphere, engine2Valve2D, heatExchanger, hotWire, misalignedDuct, multiphaseCoolingCylinder2D, notchedRoller, reverseBurner, shellAndTubeHeatExchanger, thermalBending, vacuumFurnace, wallBoiling. I loro file system/functions e system/fvSolution sono esempi verificati di misure e controlli dell’accoppiamento.

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:

Approfondimento · Script del pacchetto: apply_media_url.py

Utilità per chi pubblica i frammenti HTML della guida su un proprio sito: sostituisce il segnaposto __MEDIA_BASE_URL__ con l’indirizzo delle immagini in una copia nuova del file, senza mai sovrascrivere.

#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Replace WordPress media placeholders in a NEW HTML copy."""
import argparse
from pathlib import Path

p = argparse.ArgumentParser(description=__doc__)
p.add_argument('html', type=Path)
p.add_argument('--base-url', required=True, help='Absolute URL of the WordPress upload folder, without final slash')
p.add_argument('--output', type=Path, required=True)
a=p.parse_args()
if not a.base_url.startswith(('https://','http://')):
    p.error('Base URL must start with https:// or http://')
if a.output.resolve()==a.html.resolve() or a.output.exists():
    p.error('Output cannot overwrite an existing file')
s=a.html.read_text(encoding='utf-8')
if '__MEDIA_BASE_URL__' not in s:
    p.error('No media URL placeholder was found')
a.output.write_text(s.replace('__MEDIA_BASE_URL__', a.base_url.rstrip('/')),encoding='utf-8')
print('Created',a.output)
Consiglio pratico · Archiviare lo studio
cd "$HOME/OpenFOAM"
foamVersion > CHT_Guida11/OPENFOAM_VERSION.txt 2>&1
tar czf CHT_Guida11_$(date +%Y%m%d).tar.gz CHT_Guida11
sha256sum CHT_Guida11_*.tar.gz > CHT_Guida11_SHA256.txt

L’archivio comprende le tre copie con log, GUIDE11_CASE.txt, file system/functions e postProcessing. Conserva accanto la scheda di misura, i CSV di bilancio e gli stati ParaView. Nessun file viene rimosso.

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, trasferimento di calore e convezione naturale e il percorso Imparare OpenFOAM 14.

#CFD #CHT #foamMultiRun #OpenFOAM #trasferimento di calore #trasferimento termico coniugato