This repository is a Fortran 2008 MPI finite-volume solver (LowMachReact-Hex) for laminar incompressible / low-Mach-style flow on hexahedral meshes.
The solver currently simulates low-Mach flow using a fractional-step projection method:
h; temperature is recovered from h, Y, and p0.T, cp, lambda, and diagnostic rho_thermo from h,Y,p0, using a combined sync path where possible.[!NOTE] Pressure Pinning: In purely periodic or closed (wall-bounded) domains, the pressure matrix has a null space (constant pressure shift). The solver automatically identifies these cases and pins the pressure at cell 1 to ensure convergence. In cases with at least one Dirichlet pressure boundary, pinning is disabled.
Current baseline:
mod_species: Manages the transport of passive scalars (). Supports Scale-on-Demand architecture with dynamic allocation for 0 to 256+ species. Implements a Correction Velocity (diffusive flux correction) to ensure strict mass conservation when using different species diffusivities ().mod_energy: Owns sensible-enthalpy transport, temperature recovery, qrad, cp, thermal conductivity, and diagnostic rho_thermo. The transported energy state is h, not T.mod_transport_properties: Abstracts transport-property evaluation. It provides a bridge to the Cantera 3.x C++ API for dynamic evaluation of viscosity and species diffusivity. Flow/projection density remains the configured constant density in the current solver.h(T,Y,p0), temperature recovery T(h,Y,p0), and combined thermo sync (T, cp, lambda, rho_thermo) = sync(h,Y,p0).mod_profiler: A hierarchical performance profiling module used to track execution time for critical kernels, Cantera thermo sync, output, and MPI communication. It provides a terminal summary at the end of each simulation.mod_bc: A unified boundary condition manager that supports field-specific types (Velocity, Pressure, Species) for every patch.patch_typepatch_velocity_typepatch_pressure_typepatch_species_typepatch_temperature_typeqradThe current solver should be described as a laminar incompressible / low-Mach-style constant-density finite-volume solver, not as a full reacting low-Mach solver yet.
Current energy/thermo rules:
h is the transported thermodynamic state
T is recovered from h, Y, and p0
p0 = background_press
rho_flow = params%rho
rho_thermo = diagnostic only
thermo_update_interval = 1
When species transport updates composition, preserve transported enthalpy and recover the new temperature:
T_new = T(h_transported, Y_new, p0)
Do not preserve old temperature by recomputing:
h_new = h(T_old, Y_new, p0)
The energy path may use a combined Cantera thermo sync:
(T, cp, lambda, rho_thermo) = sync(h, Y, p0)
This sync must preserve h.
Current missing physics:
variable-density low-Mach divergence constraint
reactions and reaction heat release
species-diffusion enthalpy correction: -div(sum_k h_k J_k)
external radiation physics
main: Stable releases and production-ready code.dev: Primary development branch. All feature development and bug fixes start here.mpirun -np 1 and multi-rank runs.-ffast-math.patch_type.The current flow solver represents incompressible, constant-density, laminar Navier-Stokes with species transport:
When energy is enabled, the current passive sensible-enthalpy equation is:
where:
u is velocityp is pressureY_k are species mass fractionsrho is constant densitynu is kinematic viscosity (constant or Cantera-derived)D_k is species diffusivity (constant or Cantera-derived)h is transported sensible enthalpyT = T(h,Y,p0) is recovered temperaturelambda is thermal conductivityqrad is a volumetric source term, currently zero unless a future coupling fills itbody_force is used to drive periodic channel flowThe solver uses a semi-implicit fractional-step method to decouple velocity and pressure:
The current flow method uses:
Important recent numerical improvements:
area / distancediv * cell_volumethis keeps the pressure matrix symmetric on nonuniform cuboid meshes
Distance-weighted face interpolation:
reduces to the old 0.5 * (owner + neighbor) form on uniform grids
Periodic wrapped-distance handling:
requires periodic_face and periodic_neighbor data from periodic.dat
Pressure operator cache:
precomputes pressure diagonal
Persistent projection workspace:
Recommended flow settings for validation:
convection_scheme = "central" for laminar validation once stability is establishedconvection_scheme = "upwind" for first stability tests of new obstacle casesCurrent MPI architecture:
MPI_Allgatherv rather than full-array MPI_AllreduceCurrent decomposition:
flow:
decomposed by owned cell ranges
future radiation:
decomposed by wavenumber / spectral interval
mesh:
replicated on every rank
This architecture is intentional because the future radiation solver needs access to the full geometry on every rank.
Do not assume:
Current rule for matrix-free operators:
The long-term radiation implementation will run every N flow steps and use the same MPI ranks as the flow solver.
Radiation will solve in the wavenumber/spectral domain:
qrad(:) contributionqrad(:) is reduced across ranksTherefore, do not optimize the code by removing full mesh availability from ranks.
Preferred architecture:
mesh:
replicated globally
flow:
owned-cell computation
optimized replicated/global field communication for now
possible future owned/ghost field layout
radiation:
full geometry on each rank
full radiation input fields on each rank
wavenumber-domain decomposition
MPI handled only in radiation driver
radiation kernel is MPI-free
Future flow optimization should distinguish between:
replicated mesh:
keep
unnecessary full-vector communication every CG iteration:
reduce or eliminate
full fields needed only every radiation step:
gather/update only when needed
Debug build:
make clean
make BUILD=debug
Release build:
make clean
make BUILD=release
Recommended release flags:
FFLAGS_RELEASE = -O3 -march=native -fno-omit-frame-pointer
Do not use -ffast-math during validation.
Aggressive performance flags may be tested only after validation:
FFLAGS_RELEASE = -O3 -march=native -funroll-loops -fno-omit-frame-pointer -flto
Only keep aggressive flags if:
Run lid-driven cavity:
mpirun -np 1 ./lowmach_react_hex cases/lid_driven_cavity/case.nml
mpirun -np 8 ./lowmach_react_hex cases/lid_driven_cavity/case.nml
Run periodic channel:
mpirun -np 1 ./lowmach_react_hex cases/channel_flow/case.nml
mpirun -np 8 ./lowmach_react_hex cases/channel_flow/case.nml
If Makefile run targets support NP, prefer:
make cavity-release NP=8
make channel-release NP=8
Run square obstacle cases only after basic cavity/channel tests still pass.
After modifying any of the following:
Run these checks:
diagnostics.csv is produced if enabledmax_div remains boundedIf the change affects pressure, also compare:
pitermax_divrms_divnet_boundary_fluxIf the change affects mesh generation, also check:
mesh%ncell_faces(c) == 6 for all current cuboid cellsFor lid-driven cavity:
Re = U_lid * L / nu.U_lid = 1, L = 1, and nu = 1e-2, the case is Re = 100.max_div should remain bounded and small.Steady-state indicator:
Typical target:
relative_KE_change per output < 1e-5
Stricter validation target:
relative_KE_change per output < 1e-6
The classic Ghia et al. benchmark is for a 2D cavity. For direct comparison, prefer a thin/symmetric/periodic spanwise direction rather than a fully 3D cavity with solid front/back walls.
For periodic channel:
body_force_xFor a channel with walls at y=0 and y=H, the steady analytic solution is:
Derived values:
For channel Reynolds number, report explicitly:
Re_H = U_bulk * H / nu
Re_Dh = U_bulk * 2H / nu
For periodic body-force channel:
piter=0 can be acceptable when the predicted field is already divergence-freeFor obstacle cases:
piter, max_div, rms_div, net_boundary_flux, and kinetic energyRecommended square-obstacle validation ladder:
Re = 20-40:
steady symmetric wake
Re = 100:
periodic shedding
Re = 250:
stronger shedding after Re=100 is stable/validated
For square side length D = 0.25 and nu = 1e-2:
U(Re=40) = 1.6
U(Re=100) = 4.0
U(Re=250) = 10.0
Current boundary-condition input supports:
patch_typepatch_velocity_typepatch_pressure_typeDo not remove legacy patch_type.
Example lid-driven cavity:
&boundary_input
n_patches = 6
patch_name = "xmin", "xmax", "ymin", "ymax", "zmin", "zmax"
patch_type = "wall", "wall", "wall", "wall", "wall", "wall"
patch_velocity_type = "no_slip", "no_slip", "no_slip", "moving_wall", "no_slip", "no_slip"
patch_pressure_type = "zero_gradient", "zero_gradient", "zero_gradient", "zero_gradient", "zero_gradient", "zero_gradient"
patch_u = 0.0, 0.0, 0.0, 1.0, 0.0, 0.0
patch_v = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
patch_w = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
patch_p = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
patch_dpdn = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
/
Example periodic channel:
&boundary_input
n_patches = 6
patch_name = "xmin", "xmax", "ymin", "ymax", "zmin", "zmax"
patch_type = "periodic", "periodic", "wall", "wall", "periodic", "periodic"
patch_velocity_type = "periodic", "periodic", "no_slip", "no_slip", "periodic", "periodic"
patch_pressure_type = "periodic", "periodic", "zero_gradient", "zero_gradient", "periodic", "periodic"
patch_u = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
patch_v = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
patch_w = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
patch_p = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
patch_dpdn = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
/
dpdn means:
dpdn = dp/dn = grad(p) dot n
For zero-gradient pressure boundaries:
patch_dpdn = 0.0
Current solver mesh assumptions:
Native mesh files:
points.dat
cells.dat
faces.dat
patches.dat
periodic.dat
Possible future explicit connectivity files:
face_nodes.dat
cell_faces.dat
The current Fortran mesh reader reconstructs mesh%ncell_faces and mesh%cell_faces from faces.dat.
For current cuboid cells:
mesh%ncell_faces(c) = 6
Manual .geo files are acceptable for small tests but should not be the long-term workflow.
Recommended Level 1 mesh workflow:
mesh_config.py
-> native block mesh generator
-> mesh_native/
-> solver
or:
mesh_config.json
-> generated .geo
-> gmsh
-> convert_gmsh_hex.py
-> mesh_native/
For the current solver, the direct native block generator is preferred for structured cuboid meshes.
The generator should:
Keep Gmsh GUI as an inspection/debugging tool, not the only source of truth for production meshes.
Current optimized pressure path:
MPI_AllgathervMPI_AllreduceThis is more efficient than the earlier full-vector MPI_Allreduce after each pressure matvec.
Do not regress to full-array MPI_Allreduce for pressure matvec assembly unless explicitly debugging.
Near-term performance direction:
keep full mesh replicated
keep radiation compatibility
reduce repeated allocation
cache mesh-dependent coefficients
avoid repeated MPI metadata setup
avoid full-vector reductions when owned-cell gather is sufficient
Long-term pressure options:
Radiation must be structured as:
mod_radiation_api # shared radiation data types
mod_radiation_driver # MPI-aware wrapper
mod_radiation_kernel # MPI-free implementation owned by radiation developer
The radiation developer should implement only:
radiation_compute_qrad_kernel(mesh, inputs, spectrum, k_first, k_last, qrad)
The kernel must:
mpi_f08qrad(:)The driver handles:
qradMPI_Allreduce or future MPI_Reduce_scattervqrad to flow/outputRadiation decomposition:
mesh:
replicated
fields needed by radiation:
globally available at radiation update time
radiation work:
decomposed by wavenumber
qrad:
partial qrad per rank
reduced to total qrad
Radiation call pattern:
if (params%enable_radiation) then
if (mod(step, params%radiation_interval) == 0) then
call update_radiation_inputs(...)
call radiation_update(...)
end if
end if
Radiation should not affect the flow until the passive enthalpy path and manufactured qrad tests are validated. Until then, it may be computed and output for diagnostics/postprocessing.
Cantera should first be used as a property backend, not as a reaction source.
Do not call Cantera directly from:
mod_flow_projectionmod_species_transportPreferred future modules:
mod_transport_propertiesmod_chemistry_canteramod_thermo_stateSuggested transport type:
type transport_properties_t
real(rk), allocatable :: rho(:)
real(rk), allocatable :: mu(:)
real(rk), allocatable :: nu(:)
real(rk), allocatable :: lambda(:)
real(rk), allocatable :: diffusivity(:,:) ! (nspecies,ncells)
end type transport_properties_t
Development path:
T/cp/lambda/rho_thermo updates (Done)Note: Calling Cantera cell-by-cell at every step introduces significant overhead. The current energy thermo path uses a combined sync and conservative cache. Future work may add diagnostics for cache hit/miss behavior, tabulation, or stronger tolerance-based update strategies after validation.
Add passive transported species:
Initial model:
fields%face_fluxsum(Y_k)=1Y_CO2Y_H2OY_COsum_YRecommended species equation:
dY_k/dt + div(u Y_k) = div(D_k grad(Y_k))
Use upwind advection for the first implementation to maintain boundedness. A limited second-order scheme can be added later.
Mixing is represented by the species diffusion term:
div(D_k grad(Y_k))
For the first implementation:
Species validation tests:
sum(Y_k) should remain near 1.Energy transport is now available as passive sensible enthalpy in the constant-density solver. The current projection constraint remains:
div(u) = 0
Variable-density low-Mach reacting flow will generally require:
This is a major architecture change and should come after:
qrad is validated with manufactured sourcesDo not patch variable-density behavior into the current incompressible projection without a written formulation.
Short term:
Long term:
Recommended long-term output hierarchy:
results.h5
/mesh
points
cells
cell_centers
cell_volume
/time
step_000000
time
velocity
pressure
divergence
Y_CO2
Y_H2O
Y_CO
qrad
step_000200
...
/diagnostics
step
time
kinetic_energy
max_divergence
rms_divergence
pressure_iterations
Ideal workflow:
solver writes:
diagnostics.csv
probes.csv
results.h5
results.xdmf
optional flow_*.vtu for debugging/visualization
postprocessing reads:
results.h5 for Python analysis
results.xdmf or VTU/PVD for ParaView
Do not remove VTU output. It is still useful for debugging and ParaView.
implicit none.fatal_error for unrecoverable input/configuration errors.All new or significantly modified public modules, public types, and public procedures must have FORD-compatible documentation comments.
Use !> for documentation comments.
!> MPI-aware radiation driver.
!!
!! This module owns all MPI logic for radiation. The radiation kernel must remain
!! MPI-free and should only compute local spectral contributions to qrad.
module mod_radiation_driver
!> Radiation input fields available on every rank.
!!
!! The arrays in this type are full-cell arrays, not owned-cell-only arrays,
!! because the radiation kernel needs full mesh visibility.
type, public :: radiation_inputs_t
real(rk), allocatable :: temperature(:) !< Cell temperature [K].
real(rk), allocatable :: pressure(:) !< Cell pressure [Pa].
end type radiation_inputs_t
!> Compute total radiative source term qrad.
!!
!! The driver partitions the spectral grid by MPI rank, calls the MPI-free
!! radiation kernel for the local wavenumber range, and then reduces the partial
!! qrad contribution across ranks.
!!
!! @param mesh Full replicated mesh.
!! @param inputs Full replicated radiation input fields.
!! @param spectrum Spectral grid and quadrature data.
!! @param qrad Total cell-centered radiative source term.
subroutine radiation_update(mesh, inputs, spectrum, qrad)
When adding a module, document:
When adding a public routine, document:
When adding a derived type, document:
Codex should add comments that improve generated FORD documentation, not decorative comments that repeat obvious code.
FORD/MathJax renders display equations correctly, but inline math in Markdown files and
Fortran documentation/source comments may be emitted literally when single-dollar
delimiters are used. Do not wrap inline math in single-dollar delimiters in documentation or source
comments. Use \(...\) for all inline math instead.
Examples of the required inline form:
\(V_c\)
\(f \in \partial c\)
\(\rho_f\)
\(\psi_{\text{other}}\)
For Fortran source comments, write documentation like this:
!> Cell control volume \(V_c\).
!! Face \(f \in \partial c\) uses density \(\rho_f\).
!! Other-side interpolation weight is \(\psi_{\text{other}}\).
Display equations may continue to use display-math delimiters where FORD/MathJax
renders them correctly. This rule is specifically about inline math. Before
committing documentation, grep Markdown and Fortran comments for single-dollar
inline math and replace those instances with \(...\).
Completed or active staged capabilities:
h <-> T, cp, lambda, and diagnostic rho_thermo.Near-term future work:
qrad tests.qrad.