OpenFOAM: mezzi porosi, Darcy–Forchheimer e perdite di carico
Quattordicesima guida del percorso Imparare OpenFOAM 14 e seconda del modulo «Multifase e sistemi in movimento» 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, trasferimento di calore, trasferimento termico coniugato, trasporto di specie e fluidi multifase VOF. Questa affronta i mezzi porosi con il modello di Darcy–Forchheimer: un ostacolo poroso aggirabile e uno strato che occupa tutta la sezione del canale, costruiti a partire dal caso ufficiale porousBlockage con cellZone, il fvModel porosityForce, la conversione della pressione cinematica in pascal, la regressione ΔP/L = aU + bU² e la relazione di Ergun. I riquadri di approfondimento e di consiglio pratico si aprono con un clic: contengono tutti gli script e i file del pacchetto, i dati reali del tutorial letti dai suoi file, l’implementazione del modello nel sorgente di OpenFOAM 14, la tabella dei numeri della campagna, la soluzione esatta dello strato, la correzione necessaria allo script di esecuzione (che nella versione del pacchetto segnala come riuscito anche un calcolo fallito), un file di monitoraggio con sonde di pressione e portate, due script aggiuntivi provati (stime e soluzione esatta, serie temporali delle sonde) e i riferimenti bibliografici con collegamento. In fondo all’articolo trovi l’archivio da scaricare con tutti gli script e i file.
Collana OpenFOAM da Windows — Modelli Darcy–Forchheimer, permeabilità e verifica quantitativa
OpenFOAM Foundation 14 · Server Linux · ParaView client Windows · Edizione didattica IT
Scopo della guida. Simulare un ostacolo poroso e uno strato poroso che occupa tutta la sezione del canale; misurare la perdita di carico, distinguere gli effetti viscosi da quelli inerziali e stimare i coefficienti del modello da risultati realmente calcolati. Le immagini e le curve teoriche della guida sono illustrative, non output CFD.

1. Obiettivi e prerequisiti
La Guida 13 ha introdotto le interfacce multifase. Qui torniamo a un fluido monofase incomprimibile, nel quale una porzione di dominio si comporta come un mezzo con resistenza distribuita. Al termine sapremo: individuare una cellZone; attivare porosityForce in constant/fvModels; distinguere la permeabilità intrinseca da un numero di perdita di carico globale; misurare p in ParaView; costruire una curva ΔP–U; separare il termine di Darcy da quello di Forchheimer; conservare input, log e misure.
Occorrono OpenFOAM Foundation 14 configurato sul server Linux, blockMesh, createZones, checkMesh, foamRun, Python 3 per preparare i casi e ParaView collegato via pvserver. Su Windows sono sufficienti FileZilla, PowerShell, Notepad++ e il client ParaView: non serve un IDE.
Approfondimento · La scheda del tutorial porousBlockage, letta dai suoi file
| Grandezza | Valore | File |
|---|---|---|
| dominio | x da −2 a 6 m, y da −2 a 2 m, spessore 0,2 m (una cella) | system/blockMeshDict |
| mesh | un blocco 64 × 32 × 1, celle quadrate da 0,125 m (2048 celle) | system/blockMeshDict |
| contorni | inlet e outlet di tipo patch; top e bottom symmetryPlane; frontAndBack empty | system/blockMeshDict |
| zona porosa | box (-0.5 -0.5 -1) (0.5 0.5 1); → 8 × 8 = 64 celle | system/createZonesDict |
| modello | porosityForce, DarcyForchheimer, D 1000; d ($D $D $D); f (0 0 0); | constant/fvModels |
| fluido | viscosityModel constant; nu 5e-3; // Re_blockage = 200, simulationType laminar | constant/physicalProperties, constant/momentumTransport |
| condizioni | ingresso U = (1 0 0) = $internalField, p zeroGradient; uscita p = 0, U pressureInletOutletVelocity | 0/U, 0/p |
| tempo | incompressibleFluid; endTime 100; deltaT 0.05; writeInterval 1; (passo fisso) | system/controlDict |
| schemi e solutori | Euler; div(phi,U) Gauss LUST grad(U); p GAMG; PIMPLE nCorrectors 2 | system/fvSchemes, system/fvSolution |
| Allrun | blockMesh; createZones; foamRun | Allrun |
Fonte: tutorials/incompressibleFluid/porousBlockage. Due conseguenze utili: poiché la condizione d’ingresso è value $internalField, cambiare internalField in 0/U (come fa lo script) cambia anche la velocità d’ingresso; poiché i bordi della zona (±0,5 m) cadono esattamente su facce della mesh, la zona contiene celle intere e la lunghezza porosa discreta è esattamente L = 8 × 0,125 = 1 m.
Consiglio pratico · Che cosa dice il tutorial stesso sui regimi di scia
Il file constant/fvModels ufficiale contiene, come commenti, la mappa dei regimi che la guida esplora nell’esperienza A:
// D 100; // Very little blockage
// D 200; // Some blockage but steady flow
// D 500; // Slight waviness in the far wake
D 1000; // Fully shedding behavior
Con ν = 5·10⁻³ m²/s, U = 1 m/s e lato del blocco 1 m il numero di Reynolds è 200 (lo stesso commento di physicalProperties): per un corpo quasi impermeabile è un regime con distacco periodico di vortici. Per D 1000 la scia è quindi intrinsecamente non stazionaria e le grandezze vanno mediate nel tempo.
2. Che cosa significa rappresentare un mezzo poroso
Un modello a geometria risolta disegna realmente i pori e le superfici solide: richiede molte celle ma permette di studiare scie e tensioni locali. Un modello omogeneizzato evita di rappresentare ogni poro e aggiunge una forza resistente al bilancio della quantità di moto delle celle selezionate. La cellZone identifica le celle su cui agisce la sorgente; non rappresenta automaticamente le particelle del letto.
Conseguenza: il parametro d non equivale alla porosità geometrica ε. Densità di superficie, ECSA, porosità, permeabilità e fattori di forma sono proprietà differenti e non vanno scambiate. Per un letto reale, i coefficienti devono provenire da caratterizzazione sperimentale, correlazioni giustificate o simulazioni microscopiche.
RICHIAMO FACOLTATIVO — Velocità superficiale e interstiziale. Nelle correlazioni per letti granulari, la velocità superficiale è Q/A con A sezione geometrica totale; la velocità media negli spazi vuoti, in una descrizione ideale, è circa U/ε. Occorre mantenere la stessa definizione usata per calibrare d e f.
Approfondimento · Il modello omogeneizzato in OpenFOAM: velocità superficiale e media di volume
Le equazioni dei mezzi porosi derivano dalla media su un volume rappresentativo (REV) delle equazioni di Navier–Stokes nei pori (Whitaker, Transport in Porous Media 1, 1986; per il termine inerziale Whitaker, «The Forchheimer equation: a theoretical development», 1996). In porosityForce la velocità U nella zona è la velocità superficiale (portata/area totale): la continuità resta ∇·U = 0 e il modello non contiene la porosità ε. Per questo, attraversando uno strato a tutta sezione, U non accelera, mentre la velocità interstiziale reale U/ε sarebbe maggiore.
Il termine viscoso ν∇²U resta attivo anche dentro la zona: l’equazione risolta è quindi di tipo Darcy–Brinkman–Forchheimer. Con le resistenze del caso (ν d = 5 s⁻¹) il termine di Brinkman conta solo in uno strato di spessore ≈ √K = 0,03 m vicino ai bordi della zona, cioè meno di una cella.
Consiglio pratico · Che cosa NON fa la zona porosa
- Non riduce il volume disponibile al fluido: inventari e tempi di permanenza calcolati con il volume delle celle vanno corretti con ε se servono valori fisici.
- Non modifica il trasporto di scalari o di calore (servirebbero diffusività efficaci e, per l’energia, la capacità termica della matrice).
- Non rappresenta la turbolenza generata dai pori: con un modello RANS servono condizioni dedicate dentro la zona.
3. La legge di Darcy–Forchheimer
Il sorgente di quantità di moto implementato dal modello è, in notazione isotropa:

Qui S è una forza resistente per unità di volume (N/m³), μ la viscosità dinamica (Pa s), ρ la densità (kg/m³), d il coefficiente viscoso (m⁻²) e f quello inerziale (m⁻¹). Per flusso quasi uniforme, stazionario, unidimensionale e con spessore poroso L, la caduta di pressione attribuibile al solo mezzo è approssimativamente:

Nel limite darciano (f = 0), la permeabilità è:

Attenzione: sono identità del modello idealizzato, non un’autorizzazione a calcolare K dalla differenza fra pressione all’ingresso e all’uscita di un dominio che contiene curve, imbocchi, zone libere o bypass laterali.

Approfondimento · Come OpenFOAM 14 applica la legge, riga per riga
Dalla descrizione di DarcyForchheimer.H e dal file DarcyForchheimerTemplates.C:
D = diag(dx, dy, dz) rotated into the coordinateSystem of the zone
F = 0.5 * diag(fx, fy, fz) "leading 0.5 is from 1/2*rho"
Cd = mu*D + rho*|U|*F per cell, |U| from the current velocity
Udiag += V * tr(Cd) implicit part (trace = 3c for isotropic Cd)
Usource -= V * (Cd - I*tr(Cd)) & U explicit correction
- Per un tensore isotropo Cd = c I la parte implicita vale 3c e la correzione esplicita −2c: a convergenza resta esattamente −c U. Questo trattamento «sovra-implicito» rende la matrice più diagonale-dominante e quindi più stabile con resistenze grandi.
- In un caso incomprimibile (pressione cinematica) il codice usa ρ = 1 e μ = ν: la sorgente è per unità di massa, −(ν d + |U| f/2) U.
- I coefficienti negativi non sono ammessi come tali: un valore negativo è interpretato come moltiplicatore del massimo coefficiente positivo. Il modello etc/caseDicts/fvModels/porosity/porousZone lo usa per un mezzo fortemente anisotropo:
d (1e7 -1000 -1000);significa 1·10⁷ in x e 1·10¹⁰ in y e z.
Approfondimento · I numeri della campagna (script porous_estimates.py)
| Caso | K (m²) | ReK = U√K/ν | f U/(2 ν d) | Δp cinematica ideale (m²/s²) | ΔP (Pa, ρ = 1000) | τ = 1/(ν d + f U/2) | Co |
|---|---|---|---|---|---|---|---|
| obstacle_D0100 | 0.01 | 20 | 0 | 0.5 | 500 | 2 s | 0.40 |
| obstacle_D1000 | 0.001 | 6.3 | 0 | 5 | 5000 | 0.2 s | 0.40 |
| slab_D1000_F00_U025 | 0.001 | 1.6 | 0 | 1.25 | 1250 | 0.2 s | 0.10 |
| slab_D1000_F00_U100 | 0.001 | 6.3 | 0 | 5 | 5000 | 0.2 s | 0.40 |
| slab_D1000_F20_U025 | 0.001 | 1.6 | 0.5 | 1.875 | 1875 | 0.13 s | 0.10 |
| slab_D1000_F20_U100 | 0.001 | 6.3 | 2 | 15 | 15000 | 0.067 s | 0.40 |
Il rapporto f U/(2 ν d) misura il peso del termine di Forchheimer rispetto a quello di Darcy: vale 0,5, 1 e 2 alle tre velocità con f = 20 m⁻¹, quindi la campagna attraversa proprio la transizione fra i due regimi ed è ben progettata per separarli con la regressione. ReK = U√K/ν è il numero di Reynolds basato sulla permeabilità, usato in letteratura per indicare l’inizio degli effetti inerziali (ordine di grandezza 1–10). τ è il tempo con cui la velocità nella zona si adatta a una variazione: con passi di 0,05 s il transitorio iniziale si esaurisce in pochi passi. Valori ottenuti dallo script con i dati del tutorial; sono stime teoriche, non risultati CFD.
python3 porous_estimates.py --manifest "$HOME/cases/guide14_campaign_01/manifest.csv" --ergun
python3 porous_estimates.py --profile exact_slab_F20_U100.csv --case slab_D1000_F20_U100
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Order-of-magnitude checks and exact slab solution for the Guide 14 campaign.
Data of the OpenFOAM 14 porousBlockage tutorial: domain x -2..6 m, y -2..2 m,
64 x 32 x 1 cells (0.125 m), nu = 5e-3 m2/s, deltaT = 0.05 s, endTime = 100 s,
porous length L = 1 m. For every case of manifest.csv (or of the built-in
list) the script prints: permeability K = 1/d, Darcy number K/L^2,
Re_K = U sqrt(K)/nu, inertial/viscous ratio f U/(2 nu d), the ideal 1D
kinematic drop dp = (nu d U + f U^2/2) L and its value in Pa for the declared
rho, the momentum relaxation time 1/(nu d + f U/2) in the zone, the Courant
number and the flow-through time. --ergun inverts the Ergun relation used in
the guide (d = 150(1-e)^2/(e^3 dp^2), f = 3.5(1-e)/(e^3 dp)) for porosity e
and equivalent particle diameter dp. --profile writes a NEW csv with the
exact kinematic pressure p(x) of the slab case (uniform plug flow, symmetry
walls, outlet p = 0), comparable with a ParaView Plot Over Line export.
Theoretical values, not CFD results.
"""
import argparse
import csv
import math
from pathlib import Path
CASES = [('obstacle_D0100', 'obstacle', 100, 0, 1.0), ('obstacle_D0200', 'obstacle', 200, 0, 1.0),
('obstacle_D0500', 'obstacle', 500, 0, 1.0), ('obstacle_D1000', 'obstacle', 1000, 0, 1.0),
('slab_D1000_F00_U025', 'slab', 1000, 0, 0.25), ('slab_D1000_F00_U050', 'slab', 1000, 0, 0.5),
('slab_D1000_F00_U100', 'slab', 1000, 0, 1.0), ('slab_D1000_F20_U025', 'slab', 1000, 20, 0.25),
('slab_D1000_F20_U050', 'slab', 1000, 20, 0.5), ('slab_D1000_F20_U100', 'slab', 1000, 20, 1.0),
('slab_D0000_F00_U100', 'slab', 0, 0, 1.0)]
def ergun_inverse(d, f):
"""Porosity e and particle diameter dp from d [1/m2] and f [1/m] (guide's Ergun form)."""
e3 = 3.5 ** 2 * d / (150.0 * f * f) # d/f^2 = (150/12.25) e^3
e = e3 ** (1.0 / 3.0)
if not 0 < e < 1:
return None
dp = 3.5 * (1 - e) / (e3 * f)
return e, dp
def main():
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument('--manifest', type=Path, help='manifest.csv of the campaign (default: built-in list)')
p.add_argument('--nu', type=float, default=5e-3)
p.add_argument('--rho', type=float, default=1000.0, help='declared reference density [kg/m3]')
p.add_argument('--L', type=float, default=1.0)
p.add_argument('--dx', type=float, default=0.125)
p.add_argument('--dt', type=float, default=0.05)
p.add_argument('--length', type=float, default=8.0, help='domain length [m]')
p.add_argument('--ergun', action='store_true')
p.add_argument('--profile', type=Path, help='NEW csv with exact slab p(x) for the case given by --case')
p.add_argument('--case', default='slab_D1000_F20_U100')
a = p.parse_args()
if a.manifest:
with a.manifest.open(newline='', encoding='utf-8') as h:
cases = [(r['case'], r['kind'], float(r['d_m_inv2']), float(r['f_m_inv']), float(r['U_m_s'])) for r in csv.DictReader(h)]
else:
cases = CASES
mu = a.rho * a.nu
print(f'mu = rho nu = {mu:g} Pa s (declared rho = {a.rho:g} kg/m3)')
print(f"{'case':22s} {'K [m2]':>9s} {'Da=K/L2':>8s} {'Re_K':>7s} {'fU/2nud':>8s} {'dp_kin':>8s} {'dp [Pa]':>9s} {'tau [s]':>8s} {'Co':>5s} {'T_flow':>7s}")
for name, kind, d, f, U in cases:
K = 1.0 / d if d > 0 else float('inf')
ratio = f * U / (2 * a.nu * d) if d > 0 else float('nan')
dpk = (a.nu * d * U + 0.5 * f * U * U) * a.L
res = a.nu * d + 0.5 * f * U
tau = 1.0 / res if res > 0 else float('inf')
reK = U * math.sqrt(K) / a.nu if d > 0 else float('inf')
print(f'{name:22s} {K:9.3g} {K / a.L ** 2:8.3g} {reK:7.3g} {ratio:8.3g} {dpk:8.4g} {a.rho * dpk:9.4g} {tau:8.3g} {U * a.dt / a.dx:5.2f} {a.length / U:7.3g}')
print('dp_kin = (nu d U + f U^2/2) L in m2/s2 (kinematic, as OpenFOAM p); dp [Pa] = rho dp_kin.')
print('For the obstacle cases dp is NOT the expected domain drop: the flow bypasses the block.')
if a.ergun:
print('\nErgun inversion (guide form, f term with the factor 1/2 of OpenFOAM):')
for d, f in sorted({(c[2], c[3]) for c in cases if c[2] > 0 and c[3] > 0}):
r = ergun_inverse(d, f)
print(f' d = {d:g} 1/m2, f = {f:g} 1/m -> ' + (f'porosity e = {r[0]:.3f}, dp = {r[1]:.4g} m' if r else 'no physical solution (e outside 0..1)'))
if a.profile:
if a.profile.exists():
p.error('profile output exists; refusing to overwrite')
sel = [c for c in cases if c[0] == a.case]
if not sel or sel[0][1] != 'slab':
p.error('--case must be a slab case of the list')
_, _, d, f, U = sel[0]
G = a.nu * d * U + 0.5 * f * U * U # kinematic gradient inside the zone
with a.profile.open('x', newline='', encoding='utf-8') as h:
w = csv.writer(h)
w.writerow(['x_m', 'p_exact_m2_s2', 'p_exact_Pa'])
for i in range(0, 601):
x = -1.5 + 3.0 * i / 600
pk = G * a.L if x <= -0.5 * a.L else (0.0 if x >= 0.5 * a.L else G * (0.5 * a.L - x))
w.writerow([f'{x:.5f}', f'{pk:.8g}', f'{a.rho * pk:.8g}'])
print(f'\nExact slab profile for {a.case}: p_up = {G * a.L:.6g} m2/s2, p_down = 0 -> written {a.profile}')
if __name__ == '__main__':
main()
4. Pressione di OpenFOAM: controllare le dimensioni
Il caso scelto usa incompressibleFluid e il campo 0/p dichiara dimensions [kinematicPressure]. Quindi il valore letto in ParaView non è espresso in pascal, bensì in m²/s². Per una densità di riferimento costante:

Il tutorial di partenza usa nu = 5e-3 m²/s e non specifica una densità assoluta, perché la soluzione incomprimibile cinematica non la richiede. Per convertire in Pa nella guida assumiamo ρ = 1000 kg/m³, esclusivamente come riferimento dichiarato, non come risultato del calcolo. In generale μ = ρν.
RICHIAMO FACOLTATIVO — Segno della perdita di carico. Definiamo sempre ΔP = P a monte − P a valle. Un valore negativo indica pressione a valle superiore, punti scambiati o una situazione fisica che richiede spiegazione. Le componenti di pressione devono essere campionate nello stesso istante.
Approfondimento · Pressione cinematica: unità, riferimento e conversione
[kinematicPressure]equivale a m²/s² (Pa diviso kg/m³). In ParaView e nei file dipostProcessingi valori sono sempre cinematici.- Il livello di pressione è fissato dall’uscita,
p = 0: ogni valore letto è quindi una differenza rispetto all’uscita. Le vocipRefCell 0; pRefValue 0;inPIMPLEnon intervengono, perché esiste già un contorno a pressione imposta. - Per un fluido reale si sceglie ρ e si imposta ν = μ/ρ nel caso; i coefficienti d e f non cambiano, perché sono proprietà della matrice porosa. Esempio: acqua a 20 °C (μ ≈ 1,0·10⁻³ Pa s) con lo stesso d = 1000 m⁻² darebbe una resistenza viscosa 5000 volte più piccola di quella del caso didattico.
foamDictionary 0/p -entry dimensions # [kinematicPressure]
foamDictionary constant/physicalProperties -entry nu
Consiglio pratico · Pascal direttamente in ParaView, senza toccare i dati
- Filtro Calculator sul caso: Attribute Type Cell Data (o Point Data), Result Array Name
p_Pa, espressionep*1000(ρ dichiarata). Il campo cinematico originale resta disponibile per i confronti. - Plot Over Line da (−1,5; 0,0625; 0) a (1,5; 0,0625; 0) con Resolution 600: lo stesso campionamento del file
--profilediporous_estimates.py, quindi i due CSV si confrontano colonna per colonna. - Scrivere ρ nel nome del campo o nella didascalia del grafico (per esempio «p_Pa, ρ = 1000 kg/m³ dichiarata»): chi rilegge il grafico tra un anno deve sapere che il pascal non viene dal calcolo.
5. Caso ufficiale e struttura dei file
Il riferimento verificato è tutorials/incompressibleFluid/porousBlockage del repository OpenFOAM/OpenFOAM-14. I file principali sono system/blockMeshDict, system/createZonesDict, constant/fvModels, constant/coordinateSystems, constant/physicalProperties, 0/U, 0/p, system/controlDict.
In OpenFOAM Foundation 14 il sorgente è configurato con type porosityForce, non con i vecchi esempi fvOptions di altre versioni. Un estratto coerente col tutorial è:
porosity
{
type porosityForce;
porosityForce
{
cellZone porousBlockage;
type DarcyForchheimer;
D 1000;
d ($D $D $D);
f (0 0 0);
coordinateSystem porousBlockage;
}
}
createZonesDict definisce la zona mediante una scatola; createZones la trasferisce alla mesh. Il caso ufficiale costruisce il dominio con blockMesh, genera le zone e avvia foamRun. Lo script di questa guida copia il tutorial installato sul server: non modifica l’originale.
Approfondimento · createZones, coordinateSystems e alternative al modello
createZonesleggesystem/createZonesDicte crea lacellZoneselezionando le celle il cui centro cade nel box: controllare inlog.createZonesil numero di celle della zona (64 per l’ostacolo, 256 per lo strato).constant/coordinateSystemsdefinisce la terna in cui sono espressi d e f (axesRotationcon e1 e e2). Con coefficienti isotropi la rotazione è irrilevante; con un mezzo fibroso o a canali conta moltissimo.- Nella cartella porosityModel della versione 14 esistono anche
fixedCoeff(coefficienti α, β con densità di riferimento),powerLaw(ΔP ∝ |U|n) esolidification;src/fvModels/interRegion/interRegionPorosityForceapplica la stessa forza fra regioni.
Il modello generico di etc/caseDicts/fvModels/porosity/porousZone mostra la sintassi completa con sistema di coordinate interno al dizionario, utile come punto di partenza per casi nuovi.
6. Due geometrie per due domande differenti
Configurazione A — Ostacolo aggirabile. Zona porosa −0,5 ≤ x ≤ 0,5 e −0,5 ≤ y ≤ 0,5, all’interno di un canale largo −2 ≤ y ≤ 2. Il fluido può passarvi attorno; i risultati servono per visualizzare redistribuzione, scie e dipendenza da d. Una ΔP complessiva comprende anche bypass e perdite geometriche: non si ricava K con una formula 1D.
Configurazione B — Strato che occupa la sezione. Estendiamo la stessa zona a −2,01 ≤ y ≤ 2,01, mantenendo lunghezza porosa L = 1 m. Le pareti laterali del caso originale sono piani di simmetria. Questa versione riduce il bypass e rende sensato confrontare il gradiente medio di pressione con Darcy–Forchheimer, pur richiedendo ancora verifiche numeriche e di ingresso/uscita.

Approfondimento · Quanto flusso attraversa davvero l’ostacolo
Un ordine di grandezza si ottiene confrontando la resistenza del blocco con la pressione dinamica che lo spinge. Per attraversare il blocco di lunghezza L serve Δp ≈ ν d L up (cinematica), mentre la differenza di pressione disponibile fra fronte e retro di un corpo tozzo è dell’ordine di U²/2 … U². Ne segue up/U ≈ U/(ν d L):
| d (m⁻²) | ν d L (m/s) | up/U stimato |
|---|---|---|
| 100 | 0.5 | ≈ 1 (blocco quasi trasparente) |
| 200 | 1 | ≈ 0.5–1 |
| 500 | 2.5 | ≈ 0.2–0.4 |
| 1000 | 5 | ≈ 0,1–0,2 (quasi impermeabile) |
È una stima grezza, ma spiega perché aumentare d sposta il flusso verso le vie laterali e perché la ΔP globale non cresce in proporzione a d. Il rapporto di bloccaggio geometrico è 1/4 (1 m su 4 m di larghezza): le due vie laterali da 1,5 m accelerano il fluido fino a circa 4/3 U se il blocco è impermeabile. Misurare in ParaView la velocità media dentro la zona (Threshold sulla cellZone o Integrate Variables) e confrontarla con questa tabella è un ottimo esercizio di verifica.
Approfondimento · Lo strato a tutta sezione ha una soluzione esatta
Con ingresso uniforme, pareti symmetryPlane (nessun attrito) e zona estesa a tutta la sezione, la soluzione del modello è un flusso a pistone: U = Uin ovunque, p costante a monte e a valle, lineare nella zona. Con p = 0 all’uscita:
G = nu*d*U + f*U^2/2 (kinematic gradient in the zone, m/s2)
p(x) = G*L for x <= -0.5
p(x) = G*(0.5 - x) for -0.5 <= x <= 0.5
p(x) = 0 for x >= 0.5
Nel metodo ai volumi finiti la caduta fra le facce di ogni cella porosa è G·Δx e le celle libere non hanno gradiente: la caduta fra due celle libere ai due lati della zona è esattamente G·L, indipendentemente dalla mesh. La configurazione B è quindi un test di verifica del codice con soluzione esatta: lo scarto atteso fra misura e formula è dell’ordine della tolleranza dei solutori. Uno scarto di qualche per cento indica un errore di impostazione (zona, d, f, ν o U diversi da quanto si crede), non un errore di discretizzazione. Lo script porous_estimates.py --profile scrive questo profilo in un CSV confrontabile con Plot Over Line.
7. Preparare la campagna sul server Linux
Caricare da Windows la directory scripts dello ZIP mediante FileZilla. Collegarsi in PowerShell:
ssh UTENTE@SERVER_LINUX
Attivare OpenFOAM 14 secondo l’installazione locale, quindi verificare foamVersion, which blockMesh, which createZones e which foamRun. Preparare i casi dentro una nuova directory:
python3 prepare_campaign.py --output "$HOME/cases/guide14_campaign_01"
Se FOAM_TUTORIALS non è impostata, specificare esplicitamente --source /percorso/tutorials/incompressibleFluid/porousBlockage. Lo script non usa sudo, non cancella alcun dato e rifiuta una directory di destinazione già esistente. Lascia una copia del tutorial per ciascun caso, guide14_case.json e manifest.csv.
La tabella contiene 11 casi: quattro ostacoli (d = 100, 200, 500, 1000 m⁻² a U = 1 m/s), sei strati completi (d = 1000 m⁻², f = 0 oppure 20 m⁻¹, tre velocità: 0,25; 0,5; 1 m/s) e un riferimento con d = f = 0.
Approfondimento · Script del pacchetto: prepare_campaign.py
Copia il tutorial installato in 11 cartelle nuove e, per ognuna, sostituisce esattamente una volta: il valore di D e di f in constant/fvModels; per gli strati, il nome della zona (porousSlab) in fvModels, createZonesDict e coordinateSystems e il box (-0.5 -2.01 -1) (0.5 2.01 1); la velocità in 0/U. Scrive guide14_case.json, manifest.csv e README_FIRST.txt. Provato sul tutorial reale di OpenFOAM 14: 11 casi creati; il confronto con l’originale mostra solo le righe attese (per obstacle_D1000 la riga D perde il commento «Fully shedding behavior» e f cambia solo di spaziatura). In caso di errore la cartella parziale viene conservata per la diagnosi.
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Prepare a non-destructive OpenFOAM-14 Darcy-Forchheimer campaign."""
import argparse
import csv
import json
import os
from pathlib import Path
import re
import shutil
import sys
CASES = [
# Original porous obstacle: the lateral bypass invalidates a direct 1D Darcy fit.
('obstacle_D0100', 'obstacle', 100, 0, 1.0),
('obstacle_D0200', 'obstacle', 200, 0, 1.0),
('obstacle_D0500', 'obstacle', 500, 0, 1.0),
('obstacle_D1000', 'obstacle', 1000, 0, 1.0),
# Cross-section-spanning porous slab: use for intrinsic resistance fitting.
('slab_D1000_F00_U025', 'slab', 1000, 0, 0.25),
('slab_D1000_F00_U050', 'slab', 1000, 0, 0.50),
('slab_D1000_F00_U100', 'slab', 1000, 0, 1.00),
('slab_D1000_F20_U025', 'slab', 1000, 20, 0.25),
('slab_D1000_F20_U050', 'slab', 1000, 20, 0.50),
('slab_D1000_F20_U100', 'slab', 1000, 20, 1.00),
('slab_D0000_F00_U100', 'slab', 0, 0, 1.00),
]
def replace_one(s: str, pattern: str, replacement: str, filename: str) -> str:
new, n = re.subn(pattern, replacement, s, flags=re.MULTILINE)
if n != 1:
raise ValueError(f'{filename}: expected 1 editable entry, found {n}')
return new
def modify_case(directory: Path, kind: str, d: int, f: int, speed: float) -> None:
fv = directory / 'constant' / 'fvModels'
src = fv.read_text(encoding='utf-8')
src = replace_one(src, r'^(\s*)D\s+[0-9.eE+\-]+\s*;[^\n]*$', rf'\g<1>D {d};', str(fv))
src = replace_one(src, r'^(\s*)f\s*\(\s*0\s+0\s+0\s*\)\s*;', rf'\g<1>f ({f} {f} {f});', str(fv))
if kind == 'slab':
src = replace_one(src, r'^\s*cellZone\s+porousBlockage\s*;', ' cellZone porousSlab;', str(fv))
src = replace_one(src, r'^\s*coordinateSystem\s+porousBlockage\s*;', ' coordinateSystem porousSlab;', str(fv))
fv.write_text(src, encoding='utf-8')
if kind == 'slab':
zones = directory / 'system' / 'createZonesDict'
z = zones.read_text(encoding='utf-8')
z = replace_one(z, r'^porousBlockage\s*$', 'porousSlab', str(zones))
z = replace_one(z, r'^\s*box\s*\([^\n]+;', ' box (-0.5 -2.01 -1) (0.5 2.01 1);', str(zones))
zones.write_text(z, encoding='utf-8')
systems = directory / 'constant' / 'coordinateSystems'
cs = systems.read_text(encoding='utf-8')
cs = replace_one(cs, r'^porousBlockage\s*$', 'porousSlab', str(systems))
systems.write_text(cs, encoding='utf-8')
ufile = directory / '0' / 'U'
u = ufile.read_text(encoding='utf-8')
u = replace_one(u, r'^internalField\s+uniform\s+\([^\n]*\);', f'internalField uniform ({speed:g} 0 0);', str(ufile))
ufile.write_text(u, encoding='utf-8')
# The official Allrun is present only as provenance; execution uses run_campaign.sh.
metadata = {'kind':kind,'d_m_inv2':d,'f_m_inv':f,'U_m_s':speed,
'nu_m2_s':0.005,'porous_length_m':1.0,
'pressure_units':'kinematic m2/s2','source':'OpenFOAM-14 porousBlockage'}
(directory/'guide14_case.json').write_text(json.dumps(metadata, indent=2)+'\n', encoding='utf-8')
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('--source', type=Path, default=None, help='Source OpenFOAM 14 porousBlockage tutorial directory')
ap.add_argument('--output', type=Path, required=True, help='A NEW, non-existing campaign directory')
args = ap.parse_args()
source = args.source or Path(os.environ.get('FOAM_TUTORIALS',''))/'incompressibleFluid'/'porousBlockage'
source = source.expanduser().resolve()
target = args.output.expanduser().resolve()
if not (source/'constant'/'fvModels').is_file():
ap.error('OpenFOAM-14 porousBlockage tutorial not found; set --source or source OpenFOAM environment')
if target.exists():
ap.error(f'Output already exists (no overwrite): {target}')
# Fail before writing if any required original file is missing.
required = ['0/U','0/p','constant/coordinateSystems','constant/fvModels',
'system/createZonesDict','system/blockMeshDict','system/controlDict']
missing = [x for x in required if not (source/x).is_file()]
if missing:
ap.error('Missing required template files: '+', '.join(missing))
target.mkdir(parents=True)
try:
manifest=[]
for name, kind, d, f, vel in CASES:
dest=target/name
shutil.copytree(source,dest,symlinks=False)
modify_case(dest,kind,d,f,vel)
manifest.append({'case':name, 'kind':kind,'d_m_inv2':d, 'f_m_inv':f,'U_m_s':vel})
with (target/'manifest.csv').open('w',newline='',encoding='utf-8') as h:
w=csv.DictWriter(h,fieldnames=list(manifest[0]));w.writeheader();w.writerows(manifest)
(target/'README_FIRST.txt').write_text('Source: '+str(source)+'\nNo CFD results exist yet. Run cases on OpenFOAM 14.\n',encoding='utf-8')
except Exception:
# Preserve partial output for diagnosis; never delete data automatically.
print(f'ERROR: incomplete campaign preserved at {target}',file=sys.stderr)
raise
print(f'Created {len(manifest)} unique cases in: {target}')
if __name__=='__main__':
main()
Consiglio pratico · Controlli rapidi dopo la preparazione
cd "$HOME/cases/guide14_campaign_01"
cat manifest.csv
for c in obstacle_D1000 slab_D1000_F20_U100; do
echo "== $c"
foamDictionary $c/constant/fvModels -entry porosity/porosityForce/cellZone
foamDictionary $c/constant/fvModels -entry porosity/porosityForce/f
grep -E "^ *box" $c/system/createZonesDict
foamDictionary $c/0/U -entry internalField
done
Lo script scrive manifest.csv con fine riga Windows (CRLF, comportamento predefinito del modulo csv di Python): run_campaign.sh legge correttamente il nome del caso, che è il primo campo, ma se si elabora il file con altri strumenti Bash conviene tenerne conto.
8. Eseguire e controllare una simulazione
Per non perdere la sessione SSH, usare tmux oppure la gestione di sessioni già descritta nella Guida 1. Avviare l’intera campagna (potenzialmente impegnativa) o un singolo caso:
bash run_campaign.sh "$HOME/cases/guide14_campaign_01" slab_D1000_F00_U025
# Per tutti i casi: bash run_campaign.sh "$HOME/cases/guide14_campaign_01"
Ogni caso esegue, in sequenza, blockMesh, createZones, checkMesh, foamRun e conserva rispettivamente log.blockMesh, log.createZones, log.checkMesh, log.foamRun. Il marker guide14.SUCCESS indica che i comandi sono terminati con codice zero, non che il calcolo sia fisicamente validato. I casi con log preesistenti o marker di errore sono saltati: nessun risultato viene cancellato per tentare una ripartenza implicita.
Controllare da Linux tail -n 50 log.foamRun e grep -Ei 'error|fatal|nan|floating|courant' log.foamRun; scaricare poi i log in Windows tramite FileZilla. Valutare Courant, conservazione di massa, soluzione transitoria e stabilità delle grandezze d’interesse.
Approfondimento · ATTENZIONE: run_campaign.sh segna come riuscito un caso fallito
Nello script del pacchetto i comandi sono eseguiti in una subshell scritta a sinistra di ||: in questa posizione Bash disattiva set -e e la trappola ERR all’interno della subshell. Se blockMesh, createZones o foamRun falliscono, lo script continua con i comandi successivi e l’ultimo, touch guide14.SUCCESS, riesce: il caso viene marcato come riuscito e lo script termina con codice 0. Verificato con comandi fittizi che simulano un foamRun in errore: il marcatore guide14.SUCCESS viene creato e non compare guide14.FAILED.
La versione corretta run_campaign_fixed.sh (nel file da scaricare, cartella extra/) concatena i comandi con && e decide il marcatore fuori dalla subshell. Provata negli stessi tre scenari: foamRun in errore → guide14.FAILED, codice 1; blockMesh in errore → si ferma subito, solo log.blockMesh, guide14.FAILED; tutto riuscito → guide14.SUCCESS, codice 0. Lo stesso difetto è presente nello script della Guida 12, corretto allo stesso modo.
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-or-later
# Run the unmodified official OpenFOAM 14 applications in each prepared case.
# Corrected version of run_campaign.sh (Guide 14): failures are no longer marked as success.
set -euo pipefail
if [[ $# -lt 1 || $# -gt 2 ]]; then
echo "Usage: bash run_campaign.sh CAMPAIGN_DIRECTORY [case_name]" >&2
exit 2
fi
command -v blockMesh >/dev/null || { echo "Load OpenFOAM environment first." >&2; exit 2; }
command -v createZones >/dev/null || { echo "Missing createZones (OpenFOAM Foundation 14 required)." >&2; exit 2; }
command -v foamRun >/dev/null || { echo "Missing foamRun (OpenFOAM Foundation 14 required)." >&2; exit 2; }
root=$(realpath "$1")
[[ -f "$root/manifest.csv" ]] || { echo "Invalid campaign directory" >&2; exit 2; }
selected=${2:-}
found=0
while IFS=, read -r name kind d f speed; do
[[ "$name" == 'case' ]] && continue
[[ -n "$name" ]] || continue
[[ -z "$selected" || "$name" == "$selected" ]] || continue
found=1
c="$root/$name"
[[ -f "$c/guide14_case.json" ]] || { echo "Missing case metadata: $c" >&2; exit 1; }
if [[ -f "$c/guide14.SUCCESS" ]]; then
echo "SKIP completed: $name"
continue
fi
if [[ -f "$c/guide14.FAILED" ]]; then
echo "SKIP previously failed: $name (inspect logs; do not silently retry)"
continue
fi
if [[ -e "$c/log.blockMesh" || -e "$c/log.createZones" || -e "$c/log.foamRun" ]]; then
echo "SKIP existing logs: $name (manual inspection necessary)"
continue
fi
echo "RUN $name"
# Fix (Guide 14 article): a subshell on the left of || runs with errexit and the
# ERR trap disabled, so the original version wrote guide14.SUCCESS even when a
# command failed. Each step is now chained with && and checked explicitly.
if ( cd "$c" &&
blockMesh > log.blockMesh 2>&1 &&
createZones > log.createZones 2>&1 &&
checkMesh > log.checkMesh 2>&1 &&
foamRun > log.foamRun 2>&1 ); then
touch "$c/guide14.SUCCESS"
else
touch "$c/guide14.FAILED"
echo "FAILED $name; inspect case logs" >&2
exit 1
fi
done < "$root/manifest.csv"
[[ "$found" == 1 ]] || { echo "No matching case name" >&2; exit 2; }
Approfondimento · Script del pacchetto: run_campaign.sh (originale)
Riportato integralmente come nel pacchetto. Controlla l’ambiente (blockMesh, createZones, foamRun), salta i casi già completati, falliti o con log esistenti e non cancella nulla; per il difetto del marcatore vedere il riquadro precedente.
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-or-later
# Run the unmodified official OpenFOAM 14 applications in each prepared case.
set -euo pipefail
if [[ $# -lt 1 || $# -gt 2 ]]; then
echo "Usage: bash run_campaign.sh CAMPAIGN_DIRECTORY [case_name]" >&2
exit 2
fi
command -v blockMesh >/dev/null || { echo "Load OpenFOAM environment first." >&2; exit 2; }
command -v createZones >/dev/null || { echo "Missing createZones (OpenFOAM Foundation 14 required)." >&2; exit 2; }
command -v foamRun >/dev/null || { echo "Missing foamRun (OpenFOAM Foundation 14 required)." >&2; exit 2; }
root=$(realpath "$1")
[[ -f "$root/manifest.csv" ]] || { echo "Invalid campaign directory" >&2; exit 2; }
selected=${2:-}
found=0
while IFS=, read -r name kind d f speed; do
[[ "$name" == 'case' ]] && continue
[[ -n "$name" ]] || continue
[[ -z "$selected" || "$name" == "$selected" ]] || continue
found=1
c="$root/$name"
[[ -f "$c/guide14_case.json" ]] || { echo "Missing case metadata: $c" >&2; exit 1; }
if [[ -f "$c/guide14.SUCCESS" ]]; then
echo "SKIP completed: $name"
continue
fi
if [[ -f "$c/guide14.FAILED" ]]; then
echo "SKIP previously failed: $name (inspect logs; do not silently retry)"
continue
fi
if [[ -e "$c/log.blockMesh" || -e "$c/log.createZones" || -e "$c/log.foamRun" ]]; then
echo "SKIP existing logs: $name (manual inspection necessary)"
continue
fi
echo "RUN $name"
(
cd "$c"
trap 'touch guide14.FAILED' ERR
blockMesh > log.blockMesh 2>&1
createZones > log.createZones 2>&1
checkMesh > log.checkMesh 2>&1
foamRun > log.foamRun 2>&1
touch guide14.SUCCESS
) || { echo "FAILED $name; inspect case logs" >&2; exit 1; }
done < "$root/manifest.csv"
[[ "$found" == 1 ]] || { echo "No matching case name" >&2; exit 2; }
Approfondimento · Misurare durante il calcolo: un file system/functions per i mezzi porosi
Il tutorial non ha system/functions. Il file seguente (con l’intestazione FoamFile, indispensabile in OpenFOAM 14) registra a ogni passo la pressione e la velocità in cinque sonde (centri di cella, perché y = 0 cade su una faccia), la pressione media sull’ingresso, le portate in ingresso e in uscita per il bilancio di massa e, da t = 20 s, le medie temporali di U e p. Va copiato in system/functions di ciascun caso prima del calcolo.
/*--------------------------------*- C++ -*----------------------------------*\
Guide 14 - monitoring functions for a NEW copy of a porousBlockage case.
Save as <case>/system/functions BEFORE run_campaign.sh.
The FoamFile header is required: without it OpenFOAM 14 ignores the file.
\*---------------------------------------------------------------------------*/
FoamFile
{
format ascii;
class dictionary;
location "system";
object functions;
}
// Pressure taps (kinematic p) and velocity on the channel axis, every time step.
// All points are cell centres of the 0.125 m mesh (y = 0 and y = 0.75 would lie on
// faces): x = -1.5625 far upstream, -0.5625 and +0.5625 first free cells next to
// the porous zone, +1.5625 far downstream; (2.5625 0.8125) lies in the wake and is
// used for the shedding frequency (probe index 4).
#includeFunc probes
(
name=pressureTaps,
points=((-1.5625 0.0625 0) (-0.5625 0.0625 0) (0.5625 0.0625 0) (1.5625 0.0625 0) (2.5625 0.8125 0)),
fields=(p U)
)
// Area-averaged pressure on the inlet (the outlet is fixed at p = 0)
#includeFunc patchAverage(name=pInlet, patch=inlet, fields=(p))
// Volumetric flow rates [m3/s] for the mass balance (inlet negative, outlet positive)
#includeFunc patchFlowRate(name=Qinlet, patch=inlet)
#includeFunc patchFlowRate(name=Qoutlet, patch=outlet)
// Time averages after the initial transient (shedding cases)
#includeFunc fieldAverage(fields=(U p), startTime=20)
// ************************************************************************* //
for c in $(tail -n +2 "$HOME/cases/guide14_campaign_01/manifest.csv" | cut -d, -f1); do
cp -n functions_guide14 "$HOME/cases/guide14_campaign_01/$c/system/functions"
done
Consiglio pratico · Durata, passo temporale e casi stazionari
- 2048 celle e 2000 passi (100 s con Δt = 0,05 s): da pochi secondi a qualche minuto per caso su un core; gli 11 casi in sequenza restano leggeri.
- Il passo è fisso (nessun
adjustTimeStep): Co = U Δt/Δx = 0,4 con U = 1 m/s, fino a circa 0,55 nelle vie laterali accelerate. - Gli strati raggiungono lo stato stazionario in pochi passi (τ ≤ 0,2 s): per loro bastano pochi secondi simulati; tenere comunque lo stesso
endTimeper una campagna omogenea, oppure documentare la riduzione in una copia. - Nel log, la riga
time step continuity errorsdeve restare piccola e le iterazioni diGAMGsu p non devono crescere nel tempo.
9. Esperienza A — Ostacolo poroso e bypass
Aprire il caso in ParaView via pvserver e osservare U, p, sezioni e Stream Tracer al medesimo tempo fisico per tutti i valori di d. Registrare la velocità massima nelle vie di aggiramento, le dimensioni della scia e l’eventuale instazionarietà. L’incremento di resistenza può deviare sempre più flusso lateralmente: perciò la differenza di pressione tra due punti lontani non è proporzionale semplicemente a d.
Consegna: quattro viste con scala colori comune, una tabella delle osservazioni e una breve discussione sulle zone di ricircolo. Se il flusso è periodico o non stazionario, usare medie temporali dopo il transitorio iniziale; non confrontare singoli fotogrammi a tempi arbitrari.
Approfondimento · Medie temporali, frequenza di distacco e Strouhal (script probe_series.py)
Per i casi con scia periodica le grandezze vanno mediate su un numero intero di periodi, dopo il transitorio. Con il file di funzioni del riquadro della sezione 8, fieldAverage scrive UMean e pMean (utili in ParaView), mentre lo script seguente legge le sonde e restituisce media, deviazione standard, minimo e massimo della differenza di pressione fra due prese e la frequenza dominante di una componente di velocità (attraversamenti dello zero della fluttuazione). Il numero di Strouhal St = f D/U per un corpo tozzo a Re ≈ 200 è dell’ordine di 0,15–0,2; un blocco poroso, attraversato da parte del flusso, tende a valori diversi e a una scia più corta.
Provato su serie sintetiche: frequenza imposta 0,21 Hz ritrovata esattamente (16 periodi), media della differenza di pressione 4,9997 m²/s² con oscillazione imposta. Con --fill scrive direttamente il CSV di misure per analyze_pressure.py usando le medie temporali delle prese, in alternativa alla compilazione manuale descritta nella sezione 11.
cd "$HOME/cases/guide14_campaign_01"
python3 probe_series.py obstacle_D*/postProcessing/pressureTaps/0/p --tstart 20 --U --probe-u 4 --component 1 --D 1 --uref 1
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Pressure-tap statistics from OpenFOAM 14 probes files (Guide 14).
Reads postProcessing/<probes name>/<time>/p of one or more cases (scalar
probes, one column per point, comment lines starting with '#') and, for
t >= --tstart, prints mean, standard deviation, minimum and maximum of the
kinematic difference p[up] - p[down] and its value in Pa (declared --rho).
With --U the matching U file is read as well and the dominant frequency of
the chosen component at --probe-u is estimated from upward zero crossings of
the fluctuation (mean removed); the Strouhal number St = freq * D / U_ref
is printed with --D and --uref. With --fill a NEW csv is written with the
columns case,p_up_m2_s2,p_down_m2_s2 expected by analyze_pressure.py,
using the time-averaged tap values.
"""
import argparse
import csv
import math
from pathlib import Path
import statistics as st
import sys
def read_probes(path):
t, rows = [], []
with open(path, encoding='utf-8') as f:
for line in f:
s = line.strip()
if not s or s.startswith('#'):
continue
parts = s.replace('(', ' ').replace(')', ' ').split()
vals = [float(x) for x in parts]
t.append(vals[0])
rows.append(vals[1:])
if not t:
raise ValueError(f'{path}: no data rows')
return t, rows
def zero_cross_freq(t, y):
m = st.fmean(y)
ups = [t[i - 1] + (t[i] - t[i - 1]) * (m - y[i - 1]) / (y[i] - y[i - 1])
for i in range(1, len(y)) if y[i - 1] < m <= y[i]]
if len(ups) < 3:
return None, len(ups)
periods = [b - a for a, b in zip(ups, ups[1:])]
return 1.0 / st.fmean(periods), len(periods)
def main():
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument('pfiles', type=Path, nargs='+', help='probes p files, e.g. CASE/postProcessing/pressureTaps/0/p')
p.add_argument('--up', type=int, default=1, help='index of the upstream tap (0-based)')
p.add_argument('--down', type=int, default=2, help='index of the downstream tap (0-based)')
p.add_argument('--tstart', type=float, default=0.0)
p.add_argument('--rho', type=float, default=1000.0)
p.add_argument('--U', action='store_true', help='also analyse the U probes file next to each p file')
p.add_argument('--probe-u', type=int, default=4, help="probe index for the frequency analysis")
p.add_argument('--component', type=int, default=1, help='0 = Ux, 1 = Uy, 2 = Uz')
p.add_argument('--D', type=float, default=1.0, help='reference length for Strouhal [m]')
p.add_argument('--uref', type=float, default=1.0, help='reference velocity for Strouhal [m/s]')
p.add_argument('--fill', type=Path, help='NEW measurements csv for analyze_pressure.py')
a = p.parse_args()
if a.fill and a.fill.exists():
p.error('--fill output exists; refusing to overwrite')
out = []
for f in a.pfiles:
case = f.parts[-5] if len(f.parts) >= 5 else f.stem
t, rows = read_probes(f)
sel = [(ti, r) for ti, r in zip(t, rows) if ti >= a.tstart]
if len(sel) < 2:
p.error(f'{f}: fewer than 2 records after t = {a.tstart}')
up = [r[a.up] for _, r in sel]
dn = [r[a.down] for _, r in sel]
dp = [u - d for u, d in zip(up, dn)]
m, s = st.fmean(dp), (st.pstdev(dp) if len(dp) > 1 else 0.0)
print(f'{case}: {len(sel)} records, t = {sel[0][0]:g}..{sel[-1][0]:g} s')
print(f' p_up mean {st.fmean(up):.6g}, p_down mean {st.fmean(dn):.6g} m2/s2')
print(f' dp = {m:.6g} +- {s:.3g} m2/s2 (min {min(dp):.6g}, max {max(dp):.6g}) -> {a.rho * m:.6g} +- {a.rho * s:.3g} Pa')
out.append((case, st.fmean(up), st.fmean(dn)))
if a.U:
tu, ru = read_probes(f.with_name('U'))
k = 3 * a.probe_u + a.component
selu = [(ti, r[k]) for ti, r in zip(tu, ru) if ti >= a.tstart]
fr, n = zero_cross_freq([x for x, _ in selu], [y for _, y in selu])
if fr:
print(f' U component {a.component} at probe {a.probe_u}: frequency {fr:.4g} Hz over {n} periods, St = {fr * a.D / a.uref:.4g}')
else:
print(f' U component {a.component} at probe {a.probe_u}: no periodic signal detected (steady or too short)')
if a.fill:
with a.fill.open('x', newline='', encoding='utf-8') as h:
w = csv.writer(h)
w.writerow(['case', 'p_up_m2_s2', 'p_down_m2_s2'])
for c, u, d in out:
w.writerow([c, f'{u:.8g}', f'{d:.8g}'])
print('Written', a.fill)
if __name__ == '__main__':
try:
main()
except (OSError, ValueError) as exc:
sys.exit(f'ERROR: {exc}')
Consiglio pratico · Viste confrontabili in ParaView
- Stessa scala per tutti i casi:
Uda 0 a 1,5 m/s,pcon un intervallo fisso scelto sul caso con resistenza massima. - Per vedere la zona:
Extract Blocko, nel lettore, la selezione dellacellZoneporousBlockage(opzione «Read zones»); in alternativa unContourdel campocellZonese lo si scrive confoamPostProcess -func writeCellZonesquando disponibile. Stream Tracercon sorgente a linea sull’ingresso (da (−2, −1,9) a (−2, 1,9)): mostra la frazione di linee che attraversa il blocco.- Per i casi periodici usare
UMeanepMeandell’ultimo istante invece dei campi istantanei.
10. Esperienza B — Strato completo, Darcy
Aprire i tre casi slab_D1000_F00_U*. Con Plot Over Line, costruire una linea lungo l’asse, da (-1.5,0,0) a (1.5,0,0), usando gli stessi punti per ogni velocità. Salvare i CSV dei profili e controllare che p(x) sia quasi lineare nella cellZone e quasi costante nella parte libera, salvo effetti discreti all’interfaccia.
Per la misura sintetica del salto, usare punti appena all’esterno delle interfacce, ad esempio x = −0,625 m e +0,625 m, y = 0, e registrare p_up e p_down. Si tratta di misure approssimate del salto associato allo strato, non di un metodo metrologico privo di errori. Un’analisi più robusta stima le pendenze di p(x) all’interno della zona e ne valuta l’incertezza; confrontare più mesh e posizioni delle sonde.
Previsione solo teorica: con ν = 0,005 m²/s, ρ = 1000 kg/m³, d = 1000 m⁻², L = 1 m e f = 0, il modello 1D fornisce ΔP ≈ 1250, 2500 e 5000 Pa per U = 0,25, 0,5 e 1 m/s. Questi numeri non sono risultati CFD.
Approfondimento · Che cosa aspettarsi esattamente in questa esperienza
Per la soluzione esatta del riquadro della sezione 6, con p = 0 all’uscita, i valori attesi alle prese sono:
| Caso | p_up (m²/s²) | p_down (m²/s²) | ΔP (Pa, ρ = 1000) |
|---|---|---|---|
| slab_D1000_F00_U025 | 1.25 | 0 | 1250 |
| slab_D1000_F00_U050 | 2.5 | 0 | 2500 |
| slab_D1000_F00_U100 | 5 | 0 | 5000 |
| slab_D1000_F20_U025 | 1.875 | 0 | 1875 |
| slab_D1000_F20_U050 | 5 | 0 | 5000 |
| slab_D1000_F20_U100 | 15 | 0 | 15000 |
| slab_D0000_F00_U100 | 0 | 0 | 0 |
In questo caso specifico, quindi, p_down ≈ 0 e p_up coincide con la caduta attraverso lo strato. I punti x = ±0,625 m suggeriti dalla guida cadono su facce della mesh (le celle hanno centri a ±0,5625 e ±0,6875): ParaView interpola i valori ai punti e, poiché nelle zone libere la pressione è costante, il risultato non cambia. Il file measurement_example_SYNTHETIC.csv del pacchetto, invece, ha p_down diversi da zero: è un esempio matematico per provare lo script, non una previsione della simulazione.
Consiglio pratico · Pendenza dentro la zona invece di due punti
Con Plot Over Line da (−1,5; 0,0625; 0) a (1,5; 0,0625; 0) e risoluzione 1200 si ottiene p(x); una retta ai minimi quadrati sui soli punti con −0,4 < x < 0,4 dà la pendenza G = −dp/dx, indipendente dal livello di riferimento. Il confronto fra G·L e la differenza fra i due punti esterni misura gli effetti di bordo. In alternativa foamPostProcess -func "graphCell(name=axis, start=(-1.5 0.0625 0), end=(1.5 0.0625 0), axis=x, fields=(p U))" scrive un punto per cella, senza interpolazione.
11. Esperienza C — Forchheimer e regressione
Ripetere l’estrazione per slab_D1000_F20_U*, con f = 20 m⁻¹. In questo caso la componente quadratica cresce con la velocità. Nel limite del modello 1D con i parametri assunti, ΔP = 1875, 5000 e 15000 Pa per le tre velocità: anche questi valori sono teorici, non misurati.
Una volta esportati i dati reali, interpolare o campionare la pressione nei due punti fissati; compilare manualmente measurements_template.csv con case,p_up_m2_s2,p_down_m2_s2 e salvare il file con un nuovo nome, ad esempio misure_reali.csv. Lo script non legge CSV raw di ParaView senza prima scegliere consapevolmente punti e colonne. Poi:
python3 analyze_pressure.py \
--manifest "$HOME/cases/guide14_campaign_01/manifest.csv" \
--measurements misure_reali.csv \
--output risultati_pressione.csv --rho 1000
Lo script rifiuta output preesistenti, impedisce duplicati di nome e calcola una regressione ai minimi quadrati, se ci sono almeno tre velocità diverse. Restituisce a, b nella forma ΔP/L = a U + b U² e stima d = a/μ, f = 2b/ρ, con μ = ρν. La regressione è diagnostica, non prova automatica di validità delle ipotesi 1D.

Approfondimento · Script del pacchetto: analyze_pressure.py
Legge il manifesto e il CSV delle misure, rifiuta duplicati, casi sconosciuti, valori non finiti e file di uscita già esistenti; converte in Pa con la ρ dichiarata, confronta con la previsione 1D dello strato e, per gli strati con d = 1000 m⁻², esegue per ciascun valore di f la regressione ai minimi quadrati senza intercetta ΔP/L = aU + bU² risolvendo il sistema 2 × 2 delle equazioni normali. Provato con il file sintetico del pacchetto: ritrova esattamente d = 1000 m⁻² e f = 0 per il primo gruppo, d = 1000 m⁻² e f = 20 m⁻¹ per il secondo, con differenze nulle rispetto alla previsione ideale.
Attenzione: lo script usa valori fissi ν = 0,005 m²/s e L = 1 m (righe d_est_m_inv2 = a/(rho*0.005) e L=1.0; nu=0.005). Se si cambia la viscosità del caso o lo spessore dello strato, vanno modificati anche questi due numeri, altrimenti d stimato e previsione ideale risultano errati.
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Analyze measured ParaView kinetic-pressure taps and Darcy-Forchheimer fit.
Input CSV columns: case,p_up_m2_s2,p_down_m2_s2
This script does NOT read OpenFOAM fields automatically and does NOT fabricate CFD data.
"""
import argparse
import csv
import math
from pathlib import Path
def calc_fit(samples, rho, length):
# Least squares: deltaP/L = a U + b U^2. No intercept by physical model.
if len(samples)<3 or len({u for u,_ in samples})<3:
return None
s2=sum(u*u for u,y in samples)
s3=sum(u**3 for u,y in samples)
s4=sum(u**4 for u,y in samples)
r1=sum(u*y/length for u,y in samples)
r2=sum(u*u*y/length for u,y in samples)
det=s2*s4-s3*s3
if abs(det)<1e-20:
return None
a=(r1*s4-r2*s3)/det
b=(r2*s2-r1*s3)/det
return {'a_Pa_s_m2':a,'b_Pa_s2_m3':b,'d_est_m_inv2':a/(rho*0.005),
'f_est_m_inv':2*b/rho}
def main():
ap=argparse.ArgumentParser(description=__doc__)
ap.add_argument('--manifest',type=Path,required=True)
ap.add_argument('--measurements',type=Path,required=True)
ap.add_argument('--output',type=Path,required=True)
ap.add_argument('--rho',type=float,default=1000., help='Assumed reference density [kg/m3]')
args=ap.parse_args()
if args.rho<=0:ap.error('rho must be positive')
if args.output.exists():ap.error('output already exists; refusal to overwrite')
with args.manifest.open(newline='',encoding='utf-8') as h:
manifest={r['case']:r for r in csv.DictReader(h)}
with args.measurements.open(newline='',encoding='utf-8') as h:
measurements=list(csv.DictReader(h))
if not measurements:ap.error('No real measurement rows. Fill the CSV after ParaView extraction.')
rows=[]; seen=set(); fit_groups={}
for row in measurements:
name=row['case']
if name in seen:ap.error('Duplicate case measurement: '+name)
seen.add(name)
if name not in manifest:ap.error('Unknown case: '+name)
m=manifest[name]
try:
up=float(row['p_up_m2_s2']);down=float(row['p_down_m2_s2'])
except (KeyError,ValueError):ap.error('Missing or invalid kinetic pressures for '+name)
if not all(map(math.isfinite,[up,down])):ap.error('Non-finite pressures: '+name)
u=float(m['U_m_s']); d=float(m['d_m_inv2']); f=float(m['f_m_inv']); L=1.0; nu=0.005
dp=args.rho*(up-down)
theoretical=args.rho*L*(nu*d*u+0.5*f*u*u)
rec={'case':name,'kind':m['kind'],'U_m_s':u,'d_m_inv2':d,'f_m_inv':f,
'deltaP_measured_Pa':dp,'deltaP_ideal_slab_Pa':theoretical,
'difference_Pa':dp-theoretical}
rows.append(rec)
if m['kind']=='slab' and d==1000:
fit_groups.setdefault(f,[]).append((u,dp))
args.output.parent.mkdir(parents=True,exist_ok=True)
with args.output.open('w',newline='',encoding='utf-8') as h:
w=csv.DictWriter(h,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
print('Output:',args.output,'--',len(rows),'real measurements')
for f, vals in sorted(fit_groups.items()):
fit=calc_fit(vals,args.rho,1.0)
if fit:
print(f'Fit for f specified {f:g} 1/m:',', '.join(f'{k}={v:.6g}' for k,v in fit.items()))
else: print(f'Not enough distinct velocities for f={f:g} fit')
print('NOTE: Ideal slab predictions omit entrances, axial development and numerical errors.')
print('NOTE: Obstacle measurements are not eligible for 1D intrinsic K estimation.')
if __name__=='__main__':main()
Approfondimento · Quanto è affidabile una regressione su tre velocità
Con tre punti e due parametri resta un solo grado di libertà per stimare l’errore: la regressione determina a e b ma non la loro incertezza in modo robusto. Consigli: usare 5–7 velocità distribuite in modo che f U/(2 ν d) vada da circa 0,1 a 3; riportare il residuo di ogni punto; verificare che il gruppo con f = 0 restituisca b ≈ 0 (è un controllo interno gratuito); confrontare con la rappresentazione adimensionale ΔP/(L μ U) = d + (ρ f/2μ) U, che è una retta in U e rende visibile un eventuale termine non previsto.
Approfondimento · File di esempio del pacchetto
Testo completo dei due file della cartella examples/: il modello vuoto da compilare e l’esempio sintetico (valori matematici di prova, non risultati CFD).
examples/measurements_template.csv
case,p_up_m2_s2,p_down_m2_s2
examples/measurement_example_SYNTHETIC.csv
case,p_up_m2_s2,p_down_m2_s2
slab_D1000_F00_U025,2.02,0.77
slab_D1000_F00_U050,3.15,0.65
slab_D1000_F00_U100,6.14,1.14
slab_D1000_F20_U025,2.785,0.91
slab_D1000_F20_U050,6.30,1.30
slab_D1000_F20_U100,16.20,1.20
12. Permeabilità, porosità ed Ergun
Una permeabilità K = 1/d è intrinseca al modello Darciano definito. Per correlare d e f a un letto granulare, la correlazione di Ergun (per ipotesi geometriche e regime appropriati) usa diametro equivalente di particella d_p e porosità ε:

Non applicarla automaticamente a reti metalliche espanse, reticoli anisotropi, materiali microstrutturati o fluidi non newtoniani. In caso di anisotropia, i coefficienti sono direzionali e conta l’orientamento di coordinateSystems; qui gli esempi impostano terne isotrope d = (D D D) e f = (F F F).
RICHIAMO FACOLTATIVO — Dimensioni. [d] = m⁻², [f] = m⁻¹, [K] = m². La porosità ε e il rapporto di area elettrochimicamente attiva ECSA sono adimensionali ma non sono equivalenti né consentono da soli di determinare la permeabilità.
Approfondimento · Ergun nella forma di OpenFOAM e inversione per i parametri della campagna
La correlazione di Ergun (Chem. Eng. Prog. 48, 1952) si scrive ΔP/L = 150 μ(1−ε)²U/(ε³dp²) + 1,75 ρ(1−ε)U²/(ε³dp). Confrontandola con μ d U + ρ f U²/2 si ottiene il coefficiente 150 per d e 2 × 1,75 = 3,5 per f: il fattore 3,5 della guida tiene conto dell’½ con cui OpenFOAM moltiplica f. Invertendo le due relazioni:
d / f^2 = (150 / 3.5^2) * eps^3 -> eps^3 = 12.25 d / (150 f^2)
d_p = 3.5 (1 - eps) / (eps^3 f)
Per d = 1000 m⁻² e f = 20 m⁻¹ si ottiene ε ≈ 0,589 e dp ≈ 0,35 m (script porous_estimates.py --ergun): i coefficienti didattici corrispondono a un letto di sfere di 35 cm, non a un filtro. Il calcolo inverso è un buon controllo di plausibilità quando si ricevono d e f da terzi. Varianti note: Macdonald et al. (Ind. Eng. Chem. Fundam. 18, 1979) propongono 180 e 1,8–4; la relazione di Kozeny–Carman coincide con il solo termine viscoso con costante 180.
Consiglio pratico · Coefficienti anisotropi in pratica
// honeycomb or straightener aligned with x: low resistance along the channels,
// almost blocked across them (negative = multiplier of the largest coefficient)
d (1000 -1000 -1000); // -> 1000, 1e6, 1e6 1/m2
f (0 0 0);
coordinateSystem porousBlockage; // e1 = flow direction
Ruotando e1 in constant/coordinateSystems si allinea la direzione «facile» a quella dei canali del materiale. Con rapporti di resistenza molto alti la convergenza richiede spesso un passo più piccolo.
Consiglio pratico · Reynolds di particella: la campagna è nel campo di validità di Ergun?
Il numero di Reynolds modificato del letto è Rep = ρ U dp / (μ (1 − ε)) = U dp / (ν (1 − ε)). Con ε = 0,589 e dp = 0,3524 m ricavati sopra e ν = 0,005 m²/s:
| U [m/s] | Rep | regime |
|---|---|---|
| 0,25 | ≈ 43 | transizione: entrambi i termini contano |
| 0,50 | ≈ 86 | transizione |
| 1,00 | ≈ 171 | inerziale prevalente |
Ergun ha ricavato la correlazione da dati con Rep fino a qualche migliaio; sotto Rep ≈ 10 domina il termine viscoso (Kozeny–Carman), oltre qualche centinaio il termine quadratico. Il rapporto fra i due termini, 1,75 Rep/150, vale 1 a Rep ≈ 86: proprio la velocità intermedia della campagna, ed è il motivo per cui le tre velocità scelte separano bene a e b. Se un mezzo reale lavora a Rep < 1, il termine f diventa invisibile nella regressione: inutile stimarlo.
13. Verifica numerica e incertezza
Non accettare una regressione sulla sola base di R². È necessario: controllare checkMesh e le patch; verificare cellZone visivamente; misurare una portata coerente ingresso/uscita; fare confronto di mesh (ad esempio raddoppiando la risoluzione in almeno due direzioni); confrontare posizioni diverse delle sonde; attendere o mediare un regime quasi stazionario; verificare che p sia cinematica; ripetere la campagna con diverse velocità. Le pressioni su sezioni sono preferibili a un unico punto per geometrie tridimensionali non uniformi.
Il caso dello strato si presta alla calibrazione di K solo se la zona resta effettivamente attraversata senza bypass, e se le perdite esterne vengono stimate o sottratte. Il riferimento D = 0, f = 0 aiuta a stimare il contributo di fondo del dominio, ma non sostituisce l’analisi della discretizzazione.
Approfondimento · Piano di verifica numerica concreto
- Verifica del codice: strato con f = 0 e f = 20 confrontato con la soluzione esatta (sezione 6); scarto atteso dell’ordine della tolleranza.
- Bilancio di massa:
Qinlet+Qoutlet≈ 0 dai file dipostProcessing(portate in m³/s per lo spessore di 0,2 m; Q = 0,8 m³/s con U = 1). - Mesh: in una copia,
(64 32 1)→(128 64 1)inblockMeshDict(i bordi della zona restano su facce) e Δt dimezzato per mantenere il Courant; per l’ostacolo confrontare la ΔP media e la frequenza di distacco. - Tempo: Δt = 0,025 s in una copia; per le medie, finestre diverse (20–100 s e 50–100 s) devono dare lo stesso valore entro l’incertezza.
- Riferimento: il caso
slab_D0000_F00_U100deve dare p ≈ 0 ovunque; un valore diverso da zero indica una perdita di fondo del dominio o un errore di impostazione.
Per l’incertezza di discretizzazione e il Grid Convergence Index si veda la Guida 4.
14. Applicazione ai filtri e ai letti catalitici
La stessa procedura consente un primo modello equivalente di filtro, supporto catalitico o monolite, purché lo spessore e il sistema di coordinate siano definiti. Per una rete metallica espansa, il confronto fra geometria realmente risolta e modello poroso equivalente ha senso soltanto dopo calibrazione su portata e perdita di carico in più condizioni: un rapporto di superficie come SEF o ECSA, da solo, non determina d né f. La predizione di trasferimento di massa e reazione richiede inoltre modelli aggiuntivi, non presenti in questa guida.
Approfondimento · Dalla prova di laboratorio ai coefficienti del modello
- Misurare ΔP su un campione di spessore noto L a 5–7 portate, con prese di pressione lontane dalle facce del campione e una prova a vuoto (senza campione) da sottrarre.
- Calcolare la velocità superficiale U = Q/A con l’area totale della sezione, la stessa definizione usata da OpenFOAM.
- Regressione ΔP/L = aU + bU² con lo stesso fluido e la stessa temperatura; d = a/μ, f = 2b/ρ (proprio come in
analyze_pressure.py). - Inserire d e f nel modello e verificare con un caso a tutta sezione (come lo strato di questa guida) che la simulazione riproduca le misure: è la calibrazione; la validazione richiede poi una geometria diversa da quella di calibrazione.
Per una trattazione completa dei modelli macroscopici: Nield e Bejan, Convection in Porous Media, 5ª ed., Springer 2017, capitolo 1.
Approfondimento · Che cosa il modello non sa: porosità, tempi di permanenza e trasporto
- Il fvModel
porosityForceaggiunge soltanto una forza all’equazione della quantità di moto: la porosità ε non compare in nessun’altra equazione. La velocità calcolata è quella superficiale; la velocità interstiziale reale è U/ε. - Conseguenza per un tracciante (Guida 12) o per un reattore: il tempo di permanenza nella zona porosa è εL/U nel mezzo reale, ma un trasporto scalare che non conosce ε lo fa attraversare in L/U. Con ε = 0,589 l’errore sul tempo di attraversamento è del 70 %.
- Lo stesso vale per la capacità termica del solido, per la dispersione meccanica (molto maggiore della diffusione molecolare nei letti impaccati) e per la superficie specifica a disposizione delle reazioni: sono modelli aggiuntivi da introdurre e validare separatamente.
- Nel modello omogeneizzato il campo di velocità è continuo all’interfaccia libero–poroso; condizioni di salto più accurate (Beavers–Joseph) richiedono modelli dedicati: Beavers e Joseph, J. Fluid Mech. 30 (1967) 197–207.
15. Checklist finale e riproducibilità
- ☐ Versione
foamVersionannotata e sorgente tutorial identificata. - ☐ Directory nuove, nessuna copia esistente sovrascritta.
- ☐
cellZonecontrollata in ParaView, zone coerenti con la domanda scientifica. - ☐
d,f, ν, ρ, U e L annotati con dimensioni e unità. - ☐ Log integri; nessun errore fatale; transitori e residui valutati.
- ☐ Stessi punti, stessi tempi e stessa procedura di campionamento.
- ☐ Conversione corretta della pressione cinematica in pascal.
- ☐ Distinzione esplicita tra ostacolo aggirabile e strato completo.
- ☐ Controlli di mesh, massa e sensibilità soddisfacenti.
- ☐ Grafici, CSV di misura, stati ParaView e file di configurazione archiviati.
Consiglio pratico · Voci aggiuntive per un lavoro di ricerca e archiviazione
- Numero di celle della zona letto in
log.createZones(64 o 256). - Scarto fra misura e soluzione esatta per i sei strati, in percentuale.
- Bilancio di massa Qinlet + Qoutlet e finestra temporale delle medie.
- Uso della versione corretta di
run_campaign.sho controllo manuale di tutti i log. - Valori di ν e L usati in
analyze_pressure.pycoerenti con i casi.
cd "$HOME/cases"
foamVersion > guide14_campaign_01/OPENFOAM_VERSION.txt 2>&1
tar czf guide14_campaign_01_$(date +%Y%m%d).tar.gz guide14_campaign_01
sha256sum guide14_campaign_01_*.tar.gz > guide14_campaign_01_SHA256.txt
16. Riferimenti e limiti della consegna
- OpenFOAM Foundation 14 — caso ufficiale porousBlockage.
- OpenFOAM Foundation 14 — sorgente DarcyForchheimer.
- OpenFOAM — documentazione di porosityForce (documentazione versione precedente consultata per struttura; il riferimento eseguibile è il tutorial della 14).
Stato dei test: i file Python e Bash del pacchetto vengono verificati staticamente e con dati sintetici. Verificare le simulazioni e le condizioni numeriche sul server prima di usare risultati a fini scientifici.
Licenza degli script originali inclusi: GNU GPL v3 o successiva. Il caso ufficiale OpenFOAM viene copiato dall’installazione locale e mantiene i rispettivi avvisi di licenza.
Approfondimento · Letture di riferimento, con collegamenti
- H. Darcy, Les fontaines publiques de la ville de Dijon, Dalmont, Paris 1856.
- P. Forchheimer, «Wasserbewegung durch Boden», Z. Ver. Dtsch. Ing. 45 (1901) 1782–1788.
- S. Ergun, «Fluid flow through packed columns», Chem. Eng. Prog. 48 (1952) 89–94.
- I. F. Macdonald, M. S. El-Sayed, K. Mow, F. A. L. Dullien, «Flow through porous media — the Ergun equation revisited», Ind. Eng. Chem. Fundam. 18 (1979) 199–208.
- S. Whitaker, «Flow in porous media I: a theoretical derivation of Darcy’s law», Transport in Porous Media 1 (1986) 3–25.
- S. Whitaker, «The Forchheimer equation: a theoretical development», Transport in Porous Media 25 (1996) 27–61, ADS.
- D. A. Nield, A. Bejan, Convection in Porous Media, 5th ed., Springer 2017, link.springer.com.
- J. Bear, Dynamics of Fluids in Porous Media, Elsevier 1972 (Dover reprint 1988).
Approfondimento · I file sorgente di OpenFOAM 14 citati nei riquadri
- tutorials/incompressibleFluid/porousBlockage — il caso di partenza
- DarcyForchheimer.H / DarcyForchheimerTemplates.C — legge e trattamento implicito
- src/fvModels/general/porosityForce — il fvModel
- etc/caseDicts/fvModels/porosity/porousZone — modello generico con coefficienti negativi
- porosityModel — DarcyForchheimer, fixedCoeff, powerLaw, solidification
- etc/caseDicts/functions/probes/probes, fieldAverage, patchFlowRate — le funzioni usate nel file di monitoraggio
Approfondimento · Script del pacchetto: patch_media_urls.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 un file nuovo, senza sovrascrivere.
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Create a new WordPress HTML file with updated media URLs, without overwriting."""
import argparse
from pathlib import Path
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('input',type=Path)
p.add_argument('--base-url',required=True, help='WordPress upload URL without a trailing slash')
p.add_argument('--output',type=Path,required=True)
args=p.parse_args()
if args.output.exists():p.error('Output exists; refusing to overwrite')
if not args.base_url.startswith(('https://','http://')):p.error('Expected HTTP(S) media base URL')
source=args.input.read_text(encoding='utf-8')
if 'MEDIA_BASE_URL/' not in source:p.error('No MEDIA_BASE_URL placeholders found')
args.output.write_text(source.replace('MEDIA_BASE_URL/',args.base_url.rstrip('/')+'/'),encoding='utf-8')
print('Created',args.output)
if __name__=='__main__':main()
Approfondimento · File del pacchetto e script aggiuntivi di questo articolo
| File | Dove è riportato | Stato |
|---|---|---|
scripts/prepare_campaign.py | sezione 7 | del pacchetto, provato sul tutorial reale |
scripts/run_campaign.sh | sezione 8 | del pacchetto, difetto del marcatore documentato |
scripts/analyze_pressure.py | sezione 11 | del pacchetto, provato sul CSV sintetico |
scripts/patch_media_urls.py | sezione 16 | del pacchetto |
examples/*.csv | sezione 11 | del pacchetto |
run_campaign_fixed.sh | sezione 8 | aggiuntivo, correzione necessaria, provato |
functions_guide14 | sezione 8 | aggiuntivo |
porous_estimates.py | sezione 3 | aggiuntivo, provato |
probe_series.py | sezione 9 | aggiuntivo, provato |
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, trasferimento termico coniugato, trasporto di specie, diffusione e miscelazione, fluidi multifase e interfacce VOF e il percorso Imparare OpenFOAM 14.
Download: script e file della guida
L’archivio OpenFOAM14_Guida14_script.zip (27 kB compressi, 10 file) contiene tutti gli script e i file di dati riportati o citati in questo articolo, con lo stesso contenuto dei riquadri: nelle cartelle scripts/, examples/ e simili i file del pacchetto della guida, in extra/ gli script e i file aggiuntivi presentati negli approfondimenti. Il file README_IT_EN.txt elenca il contenuto con le somme di controllo SHA-256; i file indicati come SYNTHETIC sono dati illustrativi, non risultati CFD. Licenza GNU GPL v3 o successiva (COPYING_GPLv3.txt).
Importante: eseguire la campagna con bash extra/run_campaign_fixed.sh "$HOME/cases/guide14_campaign_01" al posto di scripts/run_campaign.sh, che segna come riuscito anche un caso fallito, e copiare prima extra/functions_guide14 in system/functions di ciascun caso (sezione 8).
Contenuto dell’archivio:
examples/measurement_example_SYNTHETIC.csv— SYNTHETIC / illustrative data, not CFD results (7 lines)examples/measurements_template.csv— CSV data or template (1 lines)extra/functions_guide14— system/functions file for OpenFOAM 14 (with FoamFile header)extra/porous_estimates.py— Order-of-magnitude checks and exact slab solution for the Guide 14 campaign.extra/probe_series.py— Pressure-tap statistics from OpenFOAM 14 probes files (Guide 14).extra/run_campaign_fixed.sh— corrected run script: stops at the first failed command and writes the success marker only when every step succeedsscripts/analyze_pressure.py— Analyze measured ParaView kinetic-pressure taps and Darcy-Forchheimer fit.scripts/patch_media_urls.py— Create a new WordPress HTML file with updated media URLs, without overwriting.scripts/prepare_campaign.py— Prepare a non-destructive OpenFOAM-14 Darcy-Forchheimer campaign.scripts/run_campaign.sh— Run the unmodified official OpenFOAM 14 applications in each prepared case.