diff --git a/CMakeLists.txt b/CMakeLists.txt index b7bc6ffd..97f68010 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -173,6 +173,9 @@ if(BUILD_EXAMPLES) add_executable(lid_driven_cavity_direct examples/lid_driven_cavity_direct.c) target_link_libraries(lid_driven_cavity_direct PRIVATE CFD::Library) + add_executable(turbulent_channel examples/turbulent_channel.c) + target_link_libraries(turbulent_channel PRIVATE CFD::Library) + # Poiseuille flow with stretched grids add_executable(poiseuille_stretched_grid examples/poiseuille_stretched_grid.c) target_link_libraries(poiseuille_stretched_grid PRIVATE CFD::Library) @@ -309,6 +312,14 @@ if(BUILD_TESTS) # Energy equation tests add_executable(test_energy_solver tests/solvers/energy/test_energy_solver.c) add_executable(test_natural_convection tests/validation/test_natural_convection.c) + + # Turbulence model tests + add_executable(test_turbulence_kepsilon tests/solvers/turbulence/test_turbulence_kepsilon.c) + add_executable(test_turbulence_sa tests/solvers/turbulence/test_turbulence_sa.c) + add_executable(test_turbulence_wall_functions tests/solvers/turbulence/test_turbulence_wall_functions.c) + add_executable(test_turbulence_disabled_regression tests/solvers/turbulence/test_turbulence_disabled_regression.c) + add_executable(test_turbulent_channel tests/validation/test_turbulent_channel.c) + add_executable(test_turbulence_cross_backend tests/solvers/turbulence/test_turbulence_cross_backend.c) # Release-only: also run the Ra=1e4 de Vahl Davis tier (finer grid, slower) if(CAVITY_FULL_VALIDATION) target_compile_definitions(test_natural_convection PRIVATE CAVITY_FULL_VALIDATION=1) @@ -429,6 +440,12 @@ if(BUILD_TESTS) target_link_libraries(test_poiseuille_3d PRIVATE CFD::Library unity) target_link_libraries(test_energy_solver PRIVATE CFD::Library unity) target_link_libraries(test_natural_convection PRIVATE CFD::Library unity) + target_link_libraries(test_turbulence_kepsilon PRIVATE CFD::Library unity) + target_link_libraries(test_turbulence_sa PRIVATE CFD::Library unity) + target_link_libraries(test_turbulence_wall_functions PRIVATE CFD::Library unity) + target_link_libraries(test_turbulence_disabled_regression PRIVATE CFD::Library unity) + target_link_libraries(test_turbulent_channel PRIVATE CFD::Library unity) + target_link_libraries(test_turbulence_cross_backend PRIVATE CFD::Library unity) target_link_libraries(test_finite_differences PRIVATE unity $<$>:m>) target_include_directories(test_finite_differences PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/lib/include) target_link_libraries(test_finite_differences_3d PRIVATE unity $<$>:m>) @@ -580,6 +597,16 @@ if(BUILD_TESTS) add_test(NAME EnergySolverTest COMMAND test_energy_solver) add_test(NAME NaturalConvectionTest COMMAND test_natural_convection) + # Turbulence model tests + add_test(NAME TurbulenceKEpsilonTest COMMAND test_turbulence_kepsilon) + add_test(NAME TurbulenceSpalartAllmarasTest COMMAND test_turbulence_sa) + add_test(NAME TurbulenceWallFunctionsTest COMMAND test_turbulence_wall_functions) + add_test(NAME TurbulenceDisabledRegressionTest COMMAND test_turbulence_disabled_regression) + add_test(NAME TurbulentChannelTest COMMAND test_turbulent_channel) + set_tests_properties(TurbulentChannelTest PROPERTIES LABELS "validation" TIMEOUT 600) + add_test(NAME TurbulenceCrossBackendTest COMMAND test_turbulence_cross_backend) + set_tests_properties(TurbulenceCrossBackendTest PROPERTIES LABELS "cross-arch") + # Mathematical accuracy tests add_test(NAME FiniteDifferencesTest COMMAND test_finite_differences) add_test(NAME FiniteDifferences3DTest COMMAND test_finite_differences_3d) diff --git a/README.md b/README.md index 7d142abd..09bdfa69 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ A production-grade computational fluid dynamics (CFD) library in C for solving 2 - 🔧 **Pluggable Solvers**: Explicit Euler, RK2 (Heun), Projection Method (Chorin's algorithm) - 📊 **Linear Solvers**: Jacobi, SOR, Red-Black SOR, CG/PCG, BiCGSTAB - 🌡️ **Heat Transfer**: Energy equation (advection–diffusion) + Boussinesq buoyancy + thermal BCs -- 🎯 **Validated**: Ghia lid-driven cavity, Taylor-Green vortex, Poiseuille flow, natural convection benchmarks +- 🌀 **Turbulence (RANS)**: Standard k-ε and Spalart-Allmaras with log-law wall functions; validated vs turbulent channel flow at Re_τ = 395 +- 🎯 **Validated**: Ghia lid-driven cavity, Taylor-Green vortex, Poiseuille flow, natural convection, turbulent channel flow - 📈 **VTK/CSV Output**: Ready for ParaView, VisIt visualization - 💾 **Restart/Checkpoint**: Portable, versioned binary save/restore of complete simulation state - ⚡ **Performance**: SIMD-optimized with runtime CPU detection @@ -153,6 +154,15 @@ int main(void) { | `rk4_omp` | OpenMP | Multi-threaded RK4 | | `rk4_gpu` | GPU | CUDA-accelerated RK4 | +### Turbulence backend coverage + +| Feature | Scalar | AVX2 | NEON | OMP | GPU | +| ------- | ------ | ---- | ---- | --- | --- | +| k-ε + SA + wall functions | done | done | — | done | — | + +GPU NS solvers return `CFD_ERROR_UNSUPPORTED` when a turbulence model is enabled. +Turbulence is limited to 2D uniform grids in this release. + ## Project Structure ```text diff --git a/ROADMAP.md b/ROADMAP.md index 78de2480..8a5242f4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -47,6 +47,7 @@ The single source of truth for backend gaps. Each algorithm targets scalar (CPU) | | RK2 (Heun) | done | done | — | done | done | | | RK4 (classical)| done | done | — | done | done | | **Energy Eq.** | Advec-diff + Boussinesq + thermal BCs | done | done | — | done | done | +| **Turbulence** | k-ε / SA + wall functions | done | done | — | done | — | | **Linear Solvers** | Jacobi | done | done | done | — | done | | | SOR | done | done | done | — | done | | | Red-Black SOR | done | done | done | done | done | @@ -84,7 +85,7 @@ Genuine constraints to be aware of (not backlog items): | Phase | Theme | Priority | Status — what remains | | ----- | ----- | -------- | --------------------- | | 1 | Core Solver Improvements | P0–P3 | GMRES, multigrid, implicit integrators, SIMPLE/PISO, nonlinear & eigenvalue solvers | -| 2 | Physics Extensions | P1–P3 | Turbulence (RANS), compressible, species, multiphase; energy-eq. extensions | +| 2 | Physics Extensions | P1–P3 | RANS k-ε/SA ✅; realizable k-ε, k-ω SST remain; compressible, species, multiphase; energy-eq. extensions | | 3 | Geometry & Mesh | P1–P2 | Unstructured meshes, mesh I/O, adaptive refinement (3D ✅) | | 4 | Scalability & Performance | P1–P2 | MPI, GPU improvements, profiling tools (modular libs ✅) | | 5 | I/O & Post-processing | P1–P3 | HDF5, modern VTK XML, in-situ viz (CSV ✅) | @@ -226,13 +227,17 @@ source via host callback). See CHANGELOG. ### 2.2 Turbulence Models (P1) -- [ ] Spalart-Allmaras (1-equation) -- [ ] k-epsilon standard +- [x] Spalart-Allmaras (1-equation) +- [x] k-epsilon standard - [ ] k-epsilon realizable - [ ] k-omega SST -- [ ] Wall functions +- [x] Wall functions - [ ] Low-Reynolds number treatment +**Done (2D, uniform grids):** standard k-ε and Spalart-Allmaras with log-law wall functions +on scalar/OMP/AVX2 backends; validated against turbulent channel flow at Re_τ = 395 +(k-ε: u_τ error 2.9%; SA: 3.1%). GPU turbulence not yet implemented. + ### 2.3 Compressible Flow (P2) - [ ] Density-based solver @@ -507,7 +512,7 @@ easier to support new architectures. | Milestone | Target | | --------- | ------ | -| **v0.4.0 — Turbulence** | At least one RANS model (k-ε or SA), wall functions, turbulent channel-flow validation | +| ✅ **v0.4.0 — Turbulence** | *(complete: k-ε + SA, log-law wall functions, turbulent channel-flow validation at Re_τ = 395)* | | **v0.5.0 — Parallel Computing** | MPI parallelization, scalability benchmarks, HDF5 parallel I/O | | **v0.6.0 — Unstructured Meshes** | Unstructured mesh support, Gmsh import, complex-geometry examples | | **v1.0.0 — Production Ready** | All Phase 1–6 features, comprehensive validation, complete docs, stable API, performance optimized | diff --git a/docs/guides/examples.md b/docs/guides/examples.md index 196522e4..51032d1e 100644 --- a/docs/guides/examples.md +++ b/docs/guides/examples.md @@ -828,6 +828,56 @@ Part 3: Grid Refinement (Explicit Euler, dt=5e-04, T=0.5) --- +### 17. turbulent_channel.c + +**Purpose:** RANS turbulence model demonstration — turbulent channel flow at Re_τ = 395 + +**What it demonstrates:** + +- Enabling k-ε or Spalart-Allmaras turbulence via `params.turb_model` +- Configuring wall-function walls with `BC_TYPE_NOSLIP` faces in `params.turb_bc` +- Calling `turbulence_init_uniform()` before time-stepping +- Recovering the friction velocity u_τ via `turbulence_wall_u_tau()` +- Comparing the computed u+ profile against the log law +- VTK output with the four turbulence scalar fields (`turbulent_kinetic_energy`, + `dissipation_rate`, `nu_tilde`, `turbulent_viscosity`) + +**Problem setup:** + +- Domain: 4 × 2 (channel half-height δ = 1), flow is streamwise-uniform +- Re_τ = 395, ν = 1/395, ρ = 1; constant body force f_x = u_τ²/δ = 1, so the + exact steady friction velocity is u_τ = 1 +- Grid: 16×21 uniform, first-node y+ ≈ 40 (wall-function window 30–100) +- Bottom/top faces: `BC_TYPE_NOSLIP` (wall functions); left/right periodic +- Direct solver interface (registry + `solver_step`), marched to a + kinetic-energy residual below 1e-6 + +**Run:** +```bash +./turbulent_channel # k-epsilon (default) +./turbulent_channel ke # k-epsilon explicitly +./turbulent_channel sa # Spalart-Allmaras +``` + +**Expected output (k-ε):** +``` + Converged at step 24183 (KE residual 9.98e-07) + +Recovered u_tau = 0.9712 (exact force balance: 1.0000) + + y+ u+ log-law err + 39.5 14.10 14.17 0.5% + 79.0 16.24 15.86 2.4% + 118.5 17.26 16.85 2.5% + ... + 395.0 20.10 19.78 1.6% + +Wrote turbulent_channel.vtk (open in ParaView to inspect nu_t/k). +``` +The SA variant converges similarly (u_τ ≈ 0.969, u+ within ~4% of the log law). + +--- + ## Visualization ### VTK Files (ParaView/VisIt) diff --git a/docs/reference/api-reference.md b/docs/reference/api-reference.md index 088eb19d..a444a125 100644 --- a/docs/reference/api-reference.md +++ b/docs/reference/api-reference.md @@ -260,11 +260,33 @@ typedef struct { double rho; // Density int max_iterations; // Maximum iterations double tolerance; // Convergence tolerance + // Turbulence + turbulence_model_t turb_model; // TURB_MODEL_NONE (default), K_EPSILON, SPALART_ALLMARAS + ns_turbulence_bc_config_t turb_bc; // Per-face turbulence BC types } ns_solver_params_t; ns_solver_params_t ns_solver_params_default(void); ``` +`turbulence_model_t` is defined in `cfd/solvers/navier_stokes_solver.h`: + +```c +typedef enum { + TURB_MODEL_NONE = 0, // Laminar (default, zero overhead) + TURB_MODEL_K_EPSILON = 1, // Standard k-epsilon (Launder-Spalding) + TURB_MODEL_SPALART_ALLMARAS = 2, // Spalart-Allmaras 1-equation (no-ft2) +} turbulence_model_t; +``` + +`ns_turbulence_bc_config_t` holds one BC type per domain face. Available types: + +| Constant | Meaning | +|----------|---------| +| `BC_TYPE_PERIODIC` | Periodic (default for all faces) | +| `BC_TYPE_NEUMANN` | Zero-gradient (outlet) | +| `BC_TYPE_DIRICHLET` | Fixed inlet values via per-face `k_values` / `eps_values` / `nu_tilde_values` | +| `BC_TYPE_NOSLIP` | Log-law wall-function treatment | + ### Solver Statistics ```c @@ -273,6 +295,7 @@ typedef struct { double residual; // Final residual double elapsed_time; // Execution time (seconds) cfd_status_t status; // Solver status + double max_nu_t; // Maximum turbulent viscosity (0.0 when laminar) } ns_solver_stats_t; ns_solver_stats_t ns_solver_stats_default(void); @@ -358,13 +381,18 @@ void flow_field_destroy(flow_field* field); ```c typedef struct { - size_t nx, ny, nz; // Grid dimensions (nz=1 for 2D) - double* u; // x-velocity [nx * ny * nz] - double* v; // y-velocity [nx * ny * nz] - double* w; // z-velocity [nx * ny * nz] (zero for 2D) - double* p; // Pressure [nx * ny * nz] - double* rho; // Density [nx * ny * nz] - double* T; // Temperature [nx * ny * nz] + size_t nx, ny, nz; // Grid dimensions (nz=1 for 2D) + double* u; // x-velocity [nx * ny * nz] + double* v; // y-velocity [nx * ny * nz] + double* w; // z-velocity [nx * ny * nz] (zero for 2D) + double* p; // Pressure [nx * ny * nz] + double* rho; // Density [nx * ny * nz] + double* T; // Temperature [nx * ny * nz] + // Turbulence fields (always allocated; zero when TURB_MODEL_NONE) + double* turb_k; // Turbulent kinetic energy k [nx * ny * nz] + double* turb_eps; // Dissipation rate epsilon [nx * ny * nz] + double* turb_nu_tilde;// SA transported variable ν̃ [nx * ny * nz] + double* nu_t; // Turbulent viscosity ν_t [nx * ny * nz] } flow_field; ``` @@ -465,6 +493,97 @@ typedef struct { poisson_solver_stats_t poisson_solver_stats_default(void); ``` +## Turbulence API + +```c +#include "cfd/solvers/turbulence_solver.h" +``` + +### turbulence_init_uniform + +```c +cfd_status_t turbulence_init_uniform(flow_field* field, + const ns_solver_params_t* params, + double k0, double eps0, double nu_tilde0); +``` + +Initialize turbulence fields to spatially uniform values. **Call this once before +time-stepping whenever `params->turb_model != TURB_MODEL_NONE`.** + +- `k0` — initial turbulent kinetic energy (k-ε only; ignored for SA) +- `eps0` — initial dissipation rate (k-ε only; ignored for SA) +- `nu_tilde0` — initial SA transported variable (SA only; ignored for k-ε) + +Returns `CFD_SUCCESS`, or `CFD_ERROR_INVALID` if `field` or `params` is NULL. + +### turbulence_step_explicit + +```c +cfd_status_t turbulence_step_explicit(flow_field* field, + const grid* g, + const ns_solver_params_t* params, + double dt, double time); +``` + +Advance the turbulence transport equations by one explicit time step. This function is +**called automatically** by all CPU/OMP/AVX2 NS solvers after the velocity and energy steps +— users normally do not call it directly. + +Returns `CFD_SUCCESS` (no-op when `TURB_MODEL_NONE`), `CFD_ERROR_UNSUPPORTED` on a 3D grid +or non-uniform grid spacing, or `CFD_ERROR_INVALID` for NULL arguments. + +### turbulence_apply_bcs + +```c +cfd_status_t turbulence_apply_bcs(flow_field* field, + const grid* g, + const ns_solver_params_t* params); +``` + +Apply turbulence boundary conditions (periodic, Neumann, Dirichlet, or wall-function) to all +faces as configured in `params->turb_bc`. Also called automatically by the NS solvers. + +### turbulence_wall_u_tau + +```c +double turbulence_wall_u_tau(double u_p, double y_p, double nu); +``` + +Compute the friction velocity u_τ from the first-node parallel velocity `u_p`, wall-normal +distance `y_p`, and kinematic viscosity `nu` using the log-law (`u+ = ln(y+)/κ + B`) via +Newton iteration. Below y+ = 11.63 the linear viscous-sublayer law is used instead. Used +internally by the wall-function BC; exposed for testing and post-processing. + +### VTK and CSV turbulence output + +When a turbulence model is active, `write_vtk_flow_field()` automatically includes four +additional point-data scalars: + +- `turbulent_kinetic_energy` (k) +- `dissipation_rate` (ε) +- `nu_tilde` (ν̃) +- `turbulent_viscosity` (ν_t) + +CSV centerline files written by `write_centerline_to_csv()` gain three additional columns: +`turb_k`, `turb_eps`, `nu_t`. + +### Minimal usage example + +```c +sim->params.mu = 1.0 / 395.0; +sim->params.turb_model = TURB_MODEL_K_EPSILON; +sim->params.turb_bc.bottom = BC_TYPE_NOSLIP; /* wall-function walls */ +sim->params.turb_bc.top = BC_TYPE_NOSLIP; /* other faces stay PERIODIC */ + +turbulence_init_uniform(sim->field, &sim->params, k0, eps0, 0.0); + +/* run_simulation_step advances k/eps/nu_t automatically */ +for (int step = 0; step < n_steps; step++) { + cfd_status_t st = run_simulation_step(sim); + if (st != CFD_SUCCESS) { /* handle */ break; } +} +``` + ## I/O API ### VTK Output diff --git a/docs/reference/solvers.md b/docs/reference/solvers.md index 428fb3aa..c7ebf083 100644 --- a/docs/reference/solvers.md +++ b/docs/reference/solvers.md @@ -520,6 +520,157 @@ w(x,y,z,t) = 0 - Velocity decays as exp(-3νt), kinetic energy as exp(-6νt) - Validated on 16×16×16 grid with ν=0.01 +## Turbulence Models (RANS) + +Two Reynolds-Averaged Navier-Stokes (RANS) turbulence models are implemented for 2D simulations +on uniform grids. Set `params.turb_model` before calling `solver_init`; the default +(`TURB_MODEL_NONE = 0`) reproduces the laminar path bit-exactly with zero overhead. + +### Turbulence Model Overview + +| Model | Type | Transported variables | Extra cost | +|-------|----|----|----| +| `TURB_MODEL_K_EPSILON` | 2-equation | k, ε | ~2× laminar step | +| `TURB_MODEL_SPALART_ALLMARAS` | 1-equation | ν̃ | ~1.5× laminar step | + +The turbulent viscosity ν_t is computed each step and stored in `flow_field->nu_t`. All NS +solvers (scalar, OMP, AVX2) apply the effective viscosity + +``` +div((ν + ν_t) grad u) +``` + +in the momentum equation; ν_t = 0 when `TURB_MODEL_NONE`. + +**Not modelled (standard practice):** Boussinesq-stress transpose term and the -(2/3)k δ_ij +isotropic stress (absorbed into modified pressure). No turbulent Prandtl heat-flux model; the +energy equation is independent of turbulence in this release. + +### Standard k-ε Model + +**Transport equations:** + +``` +∂k/∂t + (u·∇)k = div((ν + ν_t/σ_k) grad k) + P_k - ε +∂ε/∂t + (u·∇)ε = div((ν + ν_t/σ_ε) grad ε) + C1 (ε/k) P_k - C2 ε²/k +``` + +**Turbulent viscosity:** + +``` +ν_t = C_μ k² / ε (clipped at 1e5 ν) +``` + +**Constants (Launder & Spalding):** + +| C_μ | C1 | C2 | σ_k | σ_ε | +|------|------|------|-----|-----| +| 0.09 | 1.44 | 1.92 | 1.0 | 1.3 | + +### Spalart-Allmaras Model + +**Transport equation:** + +``` +∂ν̃/∂t + (u·∇)ν̃ = cb1 S̃ ν̃ + (1/σ) div((ν + ν̃) grad ν̃) + cb2/σ |grad ν̃|² - cw1 fw (ν̃/d)² +``` + +**Turbulent viscosity:** + +``` +ν_t = ν̃ fv1, fv1 = χ³ / (χ³ + cv1³), χ = ν̃/ν +``` + +**Constants (no-ft2 fully turbulent variant):** + +| cb1 | cb2 | σ | κ | cw2 | cw3 | cv1 | +|--------|-------|-----|------|-----|-----|-----| +| 0.1355 | 0.622 | 2/3 | 0.41 | 0.3 | 2 | 7.1 | + +Wall distance d is computed on the fly from faces marked `BC_TYPE_NOSLIP` in `turb_bc`. + +### Discretization + +- **Advection:** first-order upwind (guarantees positivity of k, ε, ν̃) +- **Diffusion:** conservative face-averaged effective viscosity +- **Source terms:** semi-implicit Patankar treatment — the destruction term is linearized so k, + ε, and ν̃ can never be driven negative +- **Production limiter:** P_k ≤ 10 ε +- **Positivity floors:** k, ε, ν̃ ≥ 1e-10 enforced after each step +- **Time step:** the adaptive `compute_time_step` includes the viscous limit + `dt < dx² / (2 ν_eff n_dim)` using max(ν_t) when turbulence is active + +### Wall Functions + +Log-law wall treatment is applied at every face marked `BC_TYPE_NOSLIP` in `turb_bc`. + +**Log law:** + +``` +u+ = (1/κ) ln(y+) + B, κ = 0.41, B = 5.2 +``` + +Below y+ = 11.63 the linear viscous-sublayer law is used. The friction velocity u_τ is +recovered by Newton iteration on the log-law residual (`turbulence_wall_u_tau()`). + +**Equilibrium values at the first interior node (distance y_p from the wall):** + +``` +k = u_τ² / sqrt(C_μ) +ε = u_τ³ / (κ y_p) [k-ε] +ν̃ = κ u_τ y_p [SA] +``` + +The wall-face effective viscosity is set to `max(2*(u_τ² y_p / u_p - ν), 0)` so that the +discrete wall shear equals u_τ² exactly and reduces to pure laminar shear in the viscous +sublayer. + +**y+ guideline:** place the first interior node at 30 ≤ y+ ≤ 100. + +### Backend Coverage + +| Backend | k-ε | SA | Notes | +|---------|-----|----|-------| +| Scalar | done | done | reference implementation | +| AVX2 | done | done | k-ε: i-vectorized 4-wide with blendv upwind + scalar tail; SA: OMP-parallel scalar rows (pow-heavy closures do not vectorize profitably; numerics identical) | +| NEON | — | — | not yet implemented | +| OMP | done | done | j-loop parallel, identical numerics to scalar | +| GPU | — | — | returns `CFD_ERROR_UNSUPPORTED` when a turbulence model is enabled | + +Cross-backend consistency is verified by a unit test asserting kernel-level L∞ agreement +between all available backends. + +### Limitations + +- **2D only** — 3D grids return `CFD_ERROR_UNSUPPORTED` +- **Uniform grid only** — non-uniform spacing returns `CFD_ERROR_UNSUPPORTED` +- **No GPU turbulence** — GPU NS solvers return `CFD_ERROR_UNSUPPORTED` when + `turb_model != TURB_MODEL_NONE` +- **No turbulent Prandtl model** — the energy equation is not coupled to turbulence + +### Turbulent Channel-Flow Validation + +**Setup:** Re_τ = 395, δ = 1, 16×21 uniform grid, body-force-driven +(f_x = u_τ²/δ = 1 so exact steady u_τ = 1), first-node y+ ≈ 40. + +**k-ε results:** + +| Quantity | Value | Error | +|----------|-------|-------| +| u_τ (recovered) | 0.971 | 2.9% | +| u+ at y+ = 39.5 | — | 0.5% vs log law | +| u+ at y+ = 79 | — | 2.4% vs log law | + +**SA results:** + +| Quantity | Value | Error | +|----------|-------|-------| +| u_τ (recovered) | 0.969 | 3.1% | +| u+ at y+ ≈ 39.5 | — | 0.5% vs log law | +| u+ at y+ ≈ 79 | — | 3.7% vs log law | + +Source: `tests/validation/test_turbulent_channel.c` (ctest label `validation`). + ## References ### Numerical Methods @@ -531,12 +682,19 @@ w(x,y,z,t) = 0 ### Validation - **Ghia, U., Ghia, K.N., Shin, C.T.** (1982). "High-Re Solutions for Incompressible Flow Using the Navier-Stokes Equations and a Multigrid Method". Journal of Computational Physics. +- **Kim, J., Moin, P., Moser, R.** (1987). "Turbulence statistics in fully developed channel flow at low Reynolds number". Journal of Fluid Mechanics. ### Linear Solvers - **Saad, Y.** - "Iterative Methods for Sparse Linear Systems" - **Barrett et al.** - "Templates for the Solution of Linear Systems" +### Turbulence Modeling + +- **Launder, B.E. & Spalding, D.B.** (1974). "The numerical computation of turbulent flows". Computer Methods in Applied Mechanics and Engineering. +- **Spalart, P.R. & Allmaras, S.R.** (1992). "A one-equation turbulence model for aerodynamic flows". AIAA Paper 92-0439. +- **Wilcox, D.C.** - "Turbulence Modeling for CFD" + ## Next Steps - [Examples](../guides/examples.md) - See solvers in action diff --git a/examples/turbulent_channel.c b/examples/turbulent_channel.c new file mode 100644 index 00000000..81a2dd33 --- /dev/null +++ b/examples/turbulent_channel.c @@ -0,0 +1,208 @@ +/** + * Turbulent Channel Flow Example (RANS + wall functions) + * + * Fully-developed turbulent channel flow at a prescribed friction Reynolds + * number Re_tau, driven by a constant streamwise body force f_x = u_tau^2/delta + * (so the exact steady friction velocity is u_tau = 1 in these units): + * - periodic in x, log-law wall-function walls at y = 0 and y = 2*delta + * - RANS closure: standard k-epsilon or Spalart-Allmaras + * + * This example demonstrates: + * - Enabling a turbulence model via params.turb_model + * - Configuring wall-function walls via params.turb_bc (BC_TYPE_NOSLIP faces) + * - Initializing the turbulence fields with turbulence_init_uniform + * - Comparing the computed u+ profile against the log law + * + * Uses the direct solver interface (registry + solver_step) with a fixed time + * step; run_simulation_step is not used because it overrides params.dt. + * + * Usage: turbulent_channel [model] + * model = "ke" (k-epsilon, default) or "sa" (Spalart-Allmaras) + */ + +#include "cfd/boundary/boundary_conditions.h" +#include "cfd/core/cfd_init.h" +#include "cfd/core/grid.h" +#include "cfd/io/vtk_output.h" +#include "cfd/solvers/navier_stokes_solver.h" +#include "cfd/solvers/turbulence_solver.h" + +#include +#include +#include +#include + +#define KAPPA 0.41 +#define LOG_B 5.2 + +/* Periodic in x, no-slip walls in y (boundary values set directly; the + * projection solver preserves caller-set boundary values). */ +static void apply_channel_bc(flow_field* field) { + size_t nx = field->nx, ny = field->ny; + for (size_t j = 0; j < ny; j++) { + field->u[j * nx] = field->u[j * nx + (nx - 2)]; + field->v[j * nx] = field->v[j * nx + (nx - 2)]; + field->u[j * nx + (nx - 1)] = field->u[j * nx + 1]; + field->v[j * nx + (nx - 1)] = field->v[j * nx + 1]; + } + for (size_t i = 0; i < nx; i++) { + field->u[i] = 0.0; + field->v[i] = 0.0; + field->u[(ny - 1) * nx + i] = 0.0; + field->v[(ny - 1) * nx + i] = 0.0; + } +} + +/* Constant streamwise body force f_x = u_tau^2/delta = 1 */ +static void channel_body_force(double x, double y, double z, double t, void* ctx, + double* su, double* sv, double* sw) { + (void)x; (void)y; (void)z; (void)t; (void)ctx; + *su = 1.0; + *sv = 0.0; + *sw = 0.0; +} + +static double compute_ke(const flow_field* field) { + double ke = 0.0; + size_t total = field->nx * field->ny; + for (size_t n = 0; n < total; n++) { + ke += field->u[n] * field->u[n] + field->v[n] * field->v[n]; + } + return 0.5 * ke; +} + +int main(int argc, char* argv[]) { + /* Parameters (see tests/validation/test_turbulent_channel.c) */ + size_t nx = 16, ny = 21; + double Lx = 4.0, H = 2.0, delta = 1.0; + double Re_tau = 395.0; + double nu = 1.0 / Re_tau; + double dt = 0.002; + int max_steps = 40000; + int min_steps = 5000; + int print_interval = 2000; + double steady_tol = 1e-6; + + turbulence_model_t model = TURB_MODEL_K_EPSILON; + const char* model_name = "k-epsilon"; + if (argc > 1 && strcmp(argv[1], "sa") == 0) { + model = TURB_MODEL_SPALART_ALLMARAS; + model_name = "Spalart-Allmaras"; + } + + printf("Turbulent Channel Flow (RANS + wall functions)\n"); + printf("===============================================\n"); + printf("Grid: %zu x %zu\n", nx, ny); + printf("Re_tau: %.1f\n", Re_tau); + printf("Model: %s\n", model_name); + printf("First-node y+: %.1f (wall-function target: 30-100)\n\n", + (H / (double)(ny - 1)) / nu); + + cfd_init(); + + grid* g = grid_create(nx, ny, 1, 0.0, Lx, 0.0, H, 0.0, 0.0); + flow_field* field = flow_field_create(nx, ny, 1); + if (!g || !field) { + fprintf(stderr, "Failed to create grid/field\n"); + return 1; + } + grid_initialize_uniform(g); + + /* Initial condition: plug profile near the expected bulk velocity */ + double u_bulk0 = 15.0; + for (size_t j = 0; j < ny; j++) { + for (size_t i = 0; i < nx; i++) { + size_t idx = j * nx + i; + double y = g->y[j]; + int at_wall = (j == 0 || j == ny - 1); + field->u[idx] = at_wall ? 0.0 + : u_bulk0 * (1.0 - pow(fabs(y - delta), 8.0)); + field->v[idx] = 0.0; + field->p[idx] = 1.0; + field->rho[idx] = 1.0; + field->T[idx] = 300.0; + } + } + + ns_solver_params_t params = ns_solver_params_default(); + params.dt = dt; + params.max_iter = 1; + params.mu = nu; /* rho = 1: dynamic == kinematic viscosity */ + params.source_func = channel_body_force; + params.turb_model = model; + params.turb_bc.bottom = BC_TYPE_NOSLIP; /* wall-function wall */ + params.turb_bc.top = BC_TYPE_NOSLIP; /* left/right stay PERIODIC */ + + /* Turbulence initial condition: ~5% intensity of the bulk velocity */ + double k0 = 1.5 * pow(0.05 * u_bulk0, 2.0); + double eps0 = pow(0.09, 0.75) * pow(k0, 1.5) / (0.07 * delta); + if (turbulence_init_uniform(field, ¶ms, k0, eps0, 3.0 * nu) != CFD_SUCCESS) { + fprintf(stderr, "Failed to initialize turbulence fields\n"); + return 1; + } + + ns_solver_registry_t* registry = cfd_registry_create(); + cfd_registry_register_defaults(registry); + ns_solver_t* slv = cfd_solver_create(registry, NS_SOLVER_TYPE_PROJECTION); + if (!slv || solver_init(slv, g, ¶ms) != CFD_SUCCESS) { + fprintf(stderr, "Failed to create projection solver\n"); + return 1; + } + + /* March to steady state (kinetic-energy residual) */ + printf("Running simulation...\n"); + double prev_ke = compute_ke(field); + int step = 0; + for (step = 0; step < max_steps; step++) { + apply_channel_bc(field); + + ns_solver_stats_t stats; + cfd_status_t status = solver_step(slv, field, g, ¶ms, &stats); + if (status != CFD_SUCCESS) { + fprintf(stderr, "Solver failed at step %d (status=%d)\n", step, status); + break; + } + + double ke = compute_ke(field); + double residual = fabs(ke - prev_ke) / (prev_ke + 1e-10); + prev_ke = ke; + + if (step % print_interval == 0) { + printf(" Step %6d: KE residual = %.2e, time = %.2f s\n", + step, residual, step * dt); + } + if (residual < steady_tol && step > min_steps) { + printf(" Converged at step %d (KE residual %.2e)\n", step, residual); + break; + } + } + apply_channel_bc(field); + + /* Report u+ vs the log law along the bottom half of the channel */ + size_t i_mid = nx / 2; + double y_p = g->y[1] - g->y[0]; + double u_p = fabs(field->u[nx + i_mid]); + double u_tau = turbulence_wall_u_tau(u_p, y_p, nu); + + printf("\nRecovered u_tau = %.4f (exact force balance: 1.0000)\n\n", u_tau); + printf(" %8s %8s %8s %8s\n", "y+", "u+", "log-law", "err"); + for (size_t j = 1; j <= (ny - 1) / 2; j++) { + double yplus = g->y[j] / nu; /* u_tau = 1 in these units */ + double u_plus = field->u[j * nx + i_mid] / u_tau; + double u_log = log(yplus) / KAPPA + LOG_B; + printf(" %8.1f %8.2f %8.2f %7.1f%%\n", + yplus, u_plus, u_log, 100.0 * fabs(u_plus - u_log) / u_log); + } + + /* Write a final VTK snapshot (includes k, eps, nu_tilde, nu_t scalars) */ + write_vtk_flow_field("turbulent_channel.vtk", field, nx, ny, 1, + 0.0, Lx, 0.0, H, 0.0, 0.0); + printf("\nWrote turbulent_channel.vtk (open in ParaView to inspect nu_t/k).\n"); + + solver_destroy(slv); + cfd_registry_destroy(registry); + flow_field_destroy(field); + grid_destroy(g); + cfd_finalize(); + return 0; +} diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 444a320b..665d2ddb 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -122,6 +122,10 @@ set(CFD_SCALAR_SOURCES src/solvers/navier_stokes/cpu/solver_projection.c # Energy equation solver - scalar src/solvers/energy/cpu/energy_solver.c + # Turbulence models (RANS) - scalar + src/solvers/turbulence/cpu/turbulence_solver.c + src/solvers/turbulence/cpu/turbulence_kepsilon.c + src/solvers/turbulence/cpu/turbulence_sa.c # Linear solvers - scalar implementations only src/solvers/linear/cpu/linear_solver_jacobi.c src/solvers/linear/cpu/linear_solver_sor.c @@ -146,6 +150,8 @@ set(CFD_SIMD_SOURCES src/solvers/navier_stokes/avx2/solver_rk4_avx2.c # Energy equation solver - SIMD src/solvers/energy/avx2/energy_solver_avx2.c + # Turbulence models (RANS) - SIMD + src/solvers/turbulence/avx2/turbulence_solver_avx2.c # Linear solvers - SIMD src/solvers/linear/simd/linear_solver_simd_dispatch.c src/solvers/linear/avx2/linear_solver_jacobi_avx2.c @@ -174,6 +180,8 @@ set(CFD_OMP_SOURCES src/solvers/navier_stokes/omp/solver_rk4_omp.c # Energy equation solver - OMP src/solvers/energy/omp/energy_solver_omp.c + # Turbulence models (RANS) - OMP + src/solvers/turbulence/omp/turbulence_solver_omp.c # Linear solvers - OMP src/solvers/linear/omp/linear_solver_redblack_omp.c src/solvers/linear/omp/linear_solver_cg_omp.c @@ -330,11 +338,17 @@ if(OpenMP_C_FOUND) set(CFD_OMP_BUILD_SOURCES ${CFD_OMP_SOURCES}) set(CFD_HAS_OMP TRUE) else() - # Only boundary condition files (they have #ifdef CFD_ENABLE_OPENMP guards) - # Solver files require omp.h and can't be compiled without OpenMP + # OpenMP not found: compile only files that degrade to a correct serial + # implementation without omp.h. The boundary-condition files have + # #ifdef CFD_ENABLE_OPENMP guards; the turbulence kernel guards its omp.h + # include behind #ifdef _OPENMP and uses only #pragma omp directives (which + # no-op), so it stays correct serially — mirroring the always-compiled AVX2 + # turbulence file and keeping its OMP-named symbol present. The NS and linear + # OMP solvers unconditionally require omp.h, so they remain dropped. set(CFD_OMP_BUILD_SOURCES src/boundary/omp/boundary_conditions_omp.c src/boundary/omp/boundary_conditions_outlet_omp.c + src/solvers/turbulence/omp/turbulence_solver_omp.c ) set(CFD_HAS_OMP FALSE) endif() diff --git a/lib/include/cfd/boundary/boundary_conditions.h b/lib/include/cfd/boundary/boundary_conditions.h index 53a8fbdf..b24224e5 100644 --- a/lib/include/cfd/boundary/boundary_conditions.h +++ b/lib/include/cfd/boundary/boundary_conditions.h @@ -693,11 +693,15 @@ CFD_LIBRARY_EXPORT bc_inlet_config_t bc_inlet_config_uniform(double u_velocity, * Create a default inlet configuration for parabolic velocity profile. * * Creates a parabolic velocity inlet (fully-developed laminar flow). - * For left/right inlets: u is parabolic, v is zero. - * For top/bottom inlets: v is parabolic, u is zero. + * The returned config defaults to left edge with u = max_velocity, v = 0. + * Use bc_inlet_set_edge() to change the edge, or modify spec.velocity + * directly to set different base velocity components. + * + * Note: The parabolic profile multiplies the base velocity by + * 4*position*(1-position), where position is 0 to 1 along the inlet. * * @param max_velocity Maximum velocity at center of inlet - * @return Configured inlet structure + * @return Configured inlet structure (default: left edge, +x direction) */ CFD_LIBRARY_EXPORT bc_inlet_config_t bc_inlet_config_parabolic(double max_velocity); diff --git a/lib/include/cfd/solvers/navier_stokes_solver.h b/lib/include/cfd/solvers/navier_stokes_solver.h index fd5292a0..6e12b869 100644 --- a/lib/include/cfd/solvers/navier_stokes_solver.h +++ b/lib/include/cfd/solvers/navier_stokes_solver.h @@ -58,6 +58,10 @@ typedef struct { double* p; /**< pressure */ double* rho; /**< density */ double* T; /**< temperature */ + double* turb_k; /**< turbulent kinetic energy k (always allocated, zero when turbulence disabled) */ + double* turb_eps; /**< turbulent dissipation rate epsilon (always allocated, zero when disabled) */ + double* turb_nu_tilde; /**< Spalart-Allmaras working variable (always allocated, zero when disabled) */ + double* nu_t; /**< turbulent (eddy) viscosity (always allocated, zero when disabled) */ size_t nx; /**< number of points in x-direction */ size_t ny; /**< number of points in y-direction */ size_t nz; /**< number of points in z-direction (1 for 2D) */ @@ -115,6 +119,47 @@ typedef struct { bc_dirichlet_values_t dirichlet_values; /**< Fixed values for Dirichlet faces */ } ns_thermal_bc_config_t; +/** + * RANS turbulence model selection. + * + * TURB_MODEL_NONE (0) disables turbulence entirely: the momentum equations use + * the laminar viscosity only and the turbulence transport step is a no-op. + * Zero-initialization is therefore fully backward compatible. + */ +typedef enum { + TURB_MODEL_NONE = 0, /**< Laminar (no turbulence model) */ + TURB_MODEL_K_EPSILON = 1, /**< Standard k-epsilon (Launder-Spalding) with wall functions */ + TURB_MODEL_SPALART_ALLMARAS = 2, /**< Spalart-Allmaras one-equation model (no-ft2 variant) */ +} turbulence_model_t; + +/** + * Per-face turbulence boundary condition configuration. + * + * Face type meanings for the turbulence fields (k/epsilon or nu_tilde): + * - BC_TYPE_PERIODIC (0, default): wrap-around, matching periodic velocity BCs + * - BC_TYPE_NEUMANN: zero-gradient (typical outlet) + * - BC_TYPE_DIRICHLET: fixed values from k_values/eps_values/nu_tilde_values + * (typical inlet) + * - BC_TYPE_NOSLIP: wall-function wall — standard log-law treatment: friction + * velocity from the log law at the first interior node, equilibrium k and + * epsilon (or nu_tilde) there, and a wall-node eddy viscosity chosen so the + * discrete wall shear stress matches the log law. + * + * Zero-initialization produces an all-PERIODIC configuration, mirroring + * ns_thermal_bc_config_t semantics. + */ +typedef struct { + bc_type_t left; /**< BC type for x=0 face */ + bc_type_t right; /**< BC type for x=Lx face */ + bc_type_t bottom; /**< BC type for y=0 face */ + bc_type_t top; /**< BC type for y=Ly face */ + bc_type_t front; /**< BC type for z=Lz face (3D only) */ + bc_type_t back; /**< BC type for z=0 face (3D only) */ + bc_dirichlet_values_t k_values; /**< Fixed k per Dirichlet face */ + bc_dirichlet_values_t eps_values; /**< Fixed epsilon per Dirichlet face */ + bc_dirichlet_values_t nu_tilde_values; /**< Fixed nu_tilde per Dirichlet face */ +} ns_turbulence_bc_config_t; + /** * Navier-Stokes solver parameters */ @@ -155,6 +200,13 @@ typedef struct { /* Thermal boundary conditions (zero-initialized = all PERIODIC = no change) */ ns_thermal_bc_config_t thermal_bc; + + /* RANS turbulence model (TURB_MODEL_NONE = disabled). Implemented on the + * scalar CPU backend for 2D uniform grids; OMP/AVX2 turbulence kernels + * follow the same numerics. GPU solvers return CFD_ERROR_UNSUPPORTED when + * a turbulence model is enabled. */ + turbulence_model_t turb_model; /**< Turbulence model selection */ + ns_turbulence_bc_config_t turb_bc; /**< Per-face turbulence BCs (zero-init = all PERIODIC) */ } ns_solver_params_t; @@ -201,6 +253,7 @@ typedef struct { double max_velocity; /**< Maximum velocity magnitude */ double max_pressure; /**< Maximum pressure */ double max_temperature; /**< Maximum temperature (when energy equation active) */ + double max_nu_t; /**< Maximum eddy viscosity (when turbulence model active) */ double cfl_number; /**< Actual CFL number used */ double elapsed_time_ms; /**< Wall clock time for solve */ cfd_status_t status; /**< Status of the solve */ @@ -401,6 +454,7 @@ static inline ns_solver_stats_t ns_solver_stats_default(void) { stats.max_velocity = 0.0; stats.max_pressure = 0.0; stats.max_temperature = 0.0; + stats.max_nu_t = 0.0; stats.cfl_number = 0.0; stats.elapsed_time_ms = 0.0; stats.status = CFD_SUCCESS; diff --git a/lib/include/cfd/solvers/turbulence_solver.h b/lib/include/cfd/solvers/turbulence_solver.h new file mode 100644 index 00000000..e0ec23c6 --- /dev/null +++ b/lib/include/cfd/solvers/turbulence_solver.h @@ -0,0 +1,109 @@ +/** + * @file turbulence_solver.h + * @brief RANS turbulence models: standard k-epsilon and Spalart-Allmaras + * + * Solves the turbulence transport equations as a post-step after velocity + * advancement (mirroring the energy equation), using the updated velocity + * field. The resulting eddy viscosity nu_t enters the momentum equations + * through a conservative face-averaged effective viscosity nu + nu_t. + * + * k-epsilon (Launder-Spalding, high-Reynolds form, wall functions required): + * dk/dt + u*grad(k) = div((nu + nu_t/sigma_k) grad k) + P_k - eps + * deps/dt + u*grad(eps) = div((nu + nu_t/sigma_e) grad eps) + (eps/k)(C1*P_k - C2*eps) + * nu_t = C_mu * k^2 / eps + * + * Spalart-Allmaras (no-ft2 variant, fully turbulent): + * dnt/dt + u*grad(nt) = (1/sigma)[div((nu+nt) grad nt) + cb2*|grad nt|^2] + * + cb1*S_tilde*nt - cw1*fw*(nt/d)^2 + * nu_t = nt * fv1 + * + * Discretization: first-order upwind advection (positivity), conservative + * face-averaged diffusion, semi-implicit (Patankar) sink treatment so the + * destruction terms can never drive k/eps/nt negative. Sources use central + * gradients. 2D uniform grids only in this version; the momentum-equation + * Boussinesq-stress transpose term and the -(2/3)k*delta_ij term are omitted + * (the latter is absorbed into a modified pressure, standard practice). + * + * Wall functions (faces marked BC_TYPE_NOSLIP in params->turb_bc): standard + * log-law treatment, u+ = ln(y+)/kappa + B above y+ = 11.63, linear below. + */ + +#ifndef CFD_TURBULENCE_SOLVER_H +#define CFD_TURBULENCE_SOLVER_H + +#include "cfd/cfd_export.h" +#include "cfd/core/cfd_status.h" +#include "cfd/core/grid.h" +#include "cfd/solvers/navier_stokes_solver.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Advance the turbulence transport equations by one explicit step. + * + * Model chosen by params->turb_model; no-op returning CFD_SUCCESS when + * TURB_MODEL_NONE. Updates turb_k/turb_eps (k-epsilon) or turb_nu_tilde + * (Spalart-Allmaras) in-place using the current velocity field, then + * recomputes nu_t (clipped for realizability). + * + * @param field Flow field (turbulence arrays updated, u/v read-only) + * @param grid Computational grid (2D uniform spacing required) + * @param params Solver parameters (turb_model, turb_bc, mu) + * @param dt Time step size + * @param time Current physical time + * @return CFD_SUCCESS; CFD_ERROR_UNSUPPORTED for 3D or non-uniform grids; + * CFD_ERROR_DIVERGED if NaN/Inf detected + */ +CFD_LIBRARY_EXPORT cfd_status_t turbulence_step_explicit(flow_field* field, const grid* grid, + const ns_solver_params_t* params, + double dt, double time); + +/** + * Apply per-face turbulence boundary conditions, including wall functions. + * + * Face types (params->turb_bc): PERIODIC (default), NEUMANN (zero-gradient), + * DIRICHLET (fixed values), NOSLIP (log-law wall function: sets equilibrium + * k/eps or nu_tilde at the first interior node and a wall-node nu_t chosen so + * the discrete wall shear matches the log law). + * + * Faces are applied in the order left, right, bottom, top; later faces + * overwrite shared corner cells (same precedence as energy_apply_thermal_bcs). + * + * No-op returning CFD_SUCCESS when params->turb_model == TURB_MODEL_NONE. + * + * @return CFD_SUCCESS, CFD_ERROR_INVALID for NULL args / unsupported face + * types / too-small grids, CFD_ERROR_UNSUPPORTED for 3D grids. + */ +CFD_LIBRARY_EXPORT cfd_status_t turbulence_apply_bcs(flow_field* field, const grid* grid, + const ns_solver_params_t* params); + +/** + * Initialize the active turbulence fields uniformly and set nu_t consistently. + * + * k-epsilon: k0 and eps0 are used (nu_tilde0 ignored); nu_t = C_mu*k0^2/eps0. + * Spalart-Allmaras: nu_tilde0 is used (k0/eps0 ignored); nu_t = nu_tilde0*fv1. + * No-op returning CFD_SUCCESS when TURB_MODEL_NONE. + */ +CFD_LIBRARY_EXPORT cfd_status_t turbulence_init_uniform(flow_field* field, + const ns_solver_params_t* params, + double k0, double eps0, + double nu_tilde0); + +/** + * Solve the log law for the friction velocity u_tau. + * + * Given the wall-parallel speed u_p at wall distance y_p and kinematic + * viscosity nu, returns u_tau such that u_p/u_tau = ln(u_tau*y_p/nu)/kappa + B + * (Newton iteration), or the linear-law value sqrt(nu*u_p/y_p) when the + * resulting y+ is below 11.63. Returns 0.0 for non-positive inputs. + * Exposed for testing and diagnostics. + */ +CFD_LIBRARY_EXPORT double turbulence_wall_u_tau(double u_p, double y_p, double nu); + +#ifdef __cplusplus +} +#endif + +#endif /* CFD_TURBULENCE_SOLVER_H */ diff --git a/lib/src/api/solver_registry.c b/lib/src/api/solver_registry.c index 504535bf..e0dff077 100644 --- a/lib/src/api/solver_registry.c +++ b/lib/src/api/solver_registry.c @@ -61,6 +61,18 @@ static double compute_max_temperature(const flow_field* field) { return max_t; } +/* Maximum eddy viscosity; nu_t is non-negative, so 0.0 means laminar. */ +static double compute_max_nu_t(const flow_field* field) { + double max_nt = 0.0; + if (field->nu_t) { + size_t n = field->nx * field->ny * field->nz; + for (size_t i = 0; i < n; i++) { + if (field->nu_t[i] > max_nt) max_nt = field->nu_t[i]; + } + } + return max_nt; +} + // Forward declarations for internal solver implementations // These are not part of the public API cfd_status_t explicit_euler_impl(flow_field* field, const grid* grid, const ns_solver_params_t* params); @@ -590,6 +602,7 @@ static cfd_status_t explicit_euler_step(ns_solver_t* solver, flow_field* field, stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return CFD_SUCCESS; @@ -614,6 +627,7 @@ static cfd_status_t explicit_euler_solve(ns_solver_t* solver, flow_field* field, stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return CFD_SUCCESS; @@ -667,6 +681,7 @@ static cfd_status_t rk2_step(ns_solver_t* solver, flow_field* field, const grid* stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return status; @@ -690,6 +705,7 @@ static cfd_status_t rk2_solve(ns_solver_t* solver, flow_field* field, const grid stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return status; @@ -766,6 +782,7 @@ static cfd_status_t rk4_step(ns_solver_t* solver, flow_field* field, const grid* stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return status; @@ -789,6 +806,7 @@ static cfd_status_t rk4_solve(ns_solver_t* solver, flow_field* field, const grid stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return status; @@ -865,6 +883,7 @@ static cfd_status_t explicit_euler_simd_solve(ns_solver_t* solver, flow_field* f stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return CFD_SUCCESS; } @@ -942,6 +961,7 @@ static cfd_status_t projection_step(ns_solver_t* solver, flow_field* field, cons stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return CFD_SUCCESS; } @@ -967,6 +987,7 @@ static cfd_status_t projection_solve(ns_solver_t* solver, flow_field* field, con stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return CFD_SUCCESS; } @@ -1022,6 +1043,7 @@ static cfd_status_t projection_simd_solve(ns_solver_t* solver, flow_field* field stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return CFD_SUCCESS; } @@ -1069,6 +1091,7 @@ static cfd_status_t gpu_euler_step(ns_solver_t* solver, flow_field* field, const cfd_status_t rc = solve_explicit_euler_method_gpu(field, grid, &step_params, &cfg); if (rc == CFD_SUCCESS && stats) { stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return rc; } @@ -1082,6 +1105,7 @@ static cfd_status_t gpu_euler_solve(ns_solver_t* solver, flow_field* field, cons cfd_status_t rc = solve_explicit_euler_method_gpu(field, grid, params, &cfg); if (rc == CFD_SUCCESS && stats) { stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return rc; } @@ -1134,6 +1158,7 @@ static cfd_status_t gpu_projection_step(ns_solver_t* solver, flow_field* field, cfd_status_t rc = solve_projection_method_gpu(field, grid, &step_params, &cfg); if (rc == CFD_SUCCESS && stats) { stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return rc; } @@ -1148,6 +1173,7 @@ static cfd_status_t gpu_projection_solve(ns_solver_t* solver, flow_field* field, cfd_status_t rc = solve_projection_method_gpu(field, grid, params, &cfg); if (rc == CFD_SUCCESS && stats) { stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return rc; } @@ -1199,6 +1225,7 @@ static cfd_status_t gpu_rk2_step(ns_solver_t* solver, flow_field* field, const g cfd_status_t rc = solve_rk2_method_gpu(field, grid, &step_params, &cfg); if (rc == CFD_SUCCESS && stats) { stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return rc; } @@ -1212,6 +1239,7 @@ static cfd_status_t gpu_rk2_solve(ns_solver_t* solver, flow_field* field, const cfd_status_t rc = solve_rk2_method_gpu(field, grid, params, &cfg); if (rc == CFD_SUCCESS && stats) { stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return rc; } @@ -1227,6 +1255,7 @@ static cfd_status_t gpu_rk4_step(ns_solver_t* solver, flow_field* field, const g cfd_status_t rc = solve_rk4_method_gpu(field, grid, &step_params, &cfg); if (rc == CFD_SUCCESS && stats) { stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return rc; } @@ -1240,6 +1269,7 @@ static cfd_status_t gpu_rk4_solve(ns_solver_t* solver, flow_field* field, const cfd_status_t rc = solve_rk4_method_gpu(field, grid, params, &cfg); if (rc == CFD_SUCCESS && stats) { stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return rc; } @@ -1314,6 +1344,7 @@ static cfd_status_t explicit_euler_omp_step(ns_solver_t* solver, flow_field* fie stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return CFD_SUCCESS; } @@ -1333,6 +1364,7 @@ static cfd_status_t explicit_euler_omp_solve(ns_solver_t* solver, flow_field* fi stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return CFD_SUCCESS; } @@ -1381,6 +1413,7 @@ static cfd_status_t rk2_omp_step(ns_solver_t* solver, flow_field* field, const g stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return status; } @@ -1400,6 +1433,7 @@ static cfd_status_t rk2_omp_solve(ns_solver_t* solver, flow_field* field, const stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return status; } @@ -1448,6 +1482,7 @@ static cfd_status_t rk4_omp_step(ns_solver_t* solver, flow_field* field, const g stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return status; } @@ -1467,6 +1502,7 @@ static cfd_status_t rk4_omp_solve(ns_solver_t* solver, flow_field* field, const stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return status; } @@ -1544,6 +1580,7 @@ static cfd_status_t projection_omp_step(ns_solver_t* solver, flow_field* field, stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return CFD_SUCCESS; } @@ -1566,6 +1603,7 @@ static cfd_status_t projection_omp_solve(ns_solver_t* solver, flow_field* field, stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = compute_max_temperature(field); + stats->max_nu_t = compute_max_nu_t(field); } return CFD_SUCCESS; } diff --git a/lib/src/io/csv_output.c b/lib/src/io/csv_output.c index aa1b579e..5eb365ca 100644 --- a/lib/src/io/csv_output.c +++ b/lib/src/io/csv_output.c @@ -187,7 +187,7 @@ void write_csv_centerline(const char* filename, const flow_field* field, // Horizontal centerline: along x at y = ny/2 size_t j_mid = ny / 2; - fprintf(fp, "x,u,v,w,p,rho,T"); + fprintf(fp, "x,u,v,w,p,rho,T,turb_k,turb_eps,nu_t"); if (has_vel_mag) { fprintf(fp, ",vel_mag"); } @@ -198,6 +198,10 @@ void write_csv_centerline(const char* filename, const flow_field* field, double w_val = field->w ? field->w[idx] : 0.0; fprintf(fp, "%.6e,%.6e,%.6e,%.6e,%.6e,%.6e,%.6e", x_coords[i], field->u[idx], field->v[idx], w_val, field->p[idx], field->rho[idx], field->T[idx]); + fprintf(fp, ",%.6e,%.6e,%.6e", + field->turb_k ? field->turb_k[idx] : 0.0, + field->turb_eps ? field->turb_eps[idx] : 0.0, + field->nu_t ? field->nu_t[idx] : 0.0); if (has_vel_mag) { fprintf(fp, ",%.6e", derived->velocity_magnitude[idx]); } @@ -207,7 +211,7 @@ void write_csv_centerline(const char* filename, const flow_field* field, // Vertical centerline: along y at x = nx/2 size_t i_mid = nx / 2; - fprintf(fp, "y,u,v,w,p,rho,T"); + fprintf(fp, "y,u,v,w,p,rho,T,turb_k,turb_eps,nu_t"); if (has_vel_mag) { fprintf(fp, ",vel_mag"); } @@ -218,6 +222,10 @@ void write_csv_centerline(const char* filename, const flow_field* field, double w_val = field->w ? field->w[idx] : 0.0; fprintf(fp, "%.6e,%.6e,%.6e,%.6e,%.6e,%.6e,%.6e", y_coords[j], field->u[idx], field->v[idx], w_val, field->p[idx], field->rho[idx], field->T[idx]); + fprintf(fp, ",%.6e,%.6e,%.6e", + field->turb_k ? field->turb_k[idx] : 0.0, + field->turb_eps ? field->turb_eps[idx] : 0.0, + field->nu_t ? field->nu_t[idx] : 0.0); if (has_vel_mag) { fprintf(fp, ",%.6e", derived->velocity_magnitude[idx]); } diff --git a/lib/src/io/vtk_output.c b/lib/src/io/vtk_output.c index dd44043d..76531e23 100644 --- a/lib/src/io/vtk_output.c +++ b/lib/src/io/vtk_output.c @@ -270,6 +270,31 @@ void write_vtk_flow_field(const char* filename, const flow_field* field, size_t } } + // Turbulence scalar fields (zero when no turbulence model is active; + // NULL-guarded for manually constructed flow_field structs) + const struct { + const char* name; + const double* data; + } turb_fields[] = { + {"turbulent_kinetic_energy", field->turb_k}, + {"dissipation_rate", field->turb_eps}, + {"nu_tilde", field->turb_nu_tilde}, + {"turbulent_viscosity", field->nu_t}, + }; + for (size_t f = 0; f < sizeof(turb_fields) / sizeof(turb_fields[0]); f++) { + fprintf(fp, "\nSCALARS %s float 1\n", turb_fields[f].name); + fprintf(fp, "LOOKUP_TABLE default\n"); + for (size_t k = 0; k < nz; k++) { + for (size_t j = 0; j < ny; j++) { + for (size_t i = 0; i < nx; i++) { + size_t idx = (k * nx * ny) + IDX_2D(i, j, nx); + double val = turb_fields[f].data ? turb_fields[f].data[idx] : 0.0; + fprintf(fp, "%g\n", val); + } + } + } + } + fclose(fp); } diff --git a/lib/src/solvers/gpu/solver_projection_gpu.cu b/lib/src/solvers/gpu/solver_projection_gpu.cu index 96174839..21c92cfd 100644 --- a/lib/src/solvers/gpu/solver_projection_gpu.cu +++ b/lib/src/solvers/gpu/solver_projection_gpu.cu @@ -591,6 +591,13 @@ cfd_status_t solve_navier_stokes_gpu(flow_field* field, const grid* grid, const ns_solver_params_t* params, const gpu_config_t* config) { if (!field || !grid || !params) return CFD_ERROR_INVALID; + // RANS turbulence models have no GPU kernels. + if (params->turb_model != TURB_MODEL_NONE) { + cfd_set_error(CFD_ERROR_UNSUPPORTED, + "GPU NS solver does not support turbulence models; " + "use a CPU, OMP, or AVX2 solver"); + return CFD_ERROR_UNSUPPORTED; + } gpu_config_t cfg = config ? *config : gpu_config_default(); if (!gpu_should_use(&cfg, field->nx, field->ny, field->nz, params->max_iter)) return CFD_ERROR; @@ -618,6 +625,13 @@ cfd_status_t solve_projection_method_gpu(flow_field* field, const grid* grid, const ns_solver_params_t* params, const gpu_config_t* config) { if (!field || !grid || !params) return CFD_ERROR_INVALID; + // RANS turbulence models have no GPU kernels. + if (params->turb_model != TURB_MODEL_NONE) { + cfd_set_error(CFD_ERROR_UNSUPPORTED, + "GPU projection solver does not support turbulence models; " + "use a CPU, OMP, or AVX2 solver"); + return CFD_ERROR_UNSUPPORTED; + } gpu_config_t cfg = config ? *config : gpu_config_default(); size_t nx = field->nx, ny = field->ny, nz = field->nz; if (!gpu_should_use(&cfg, nx, ny, nz, params->max_iter)) diff --git a/lib/src/solvers/gpu/solver_rk_gpu.cu b/lib/src/solvers/gpu/solver_rk_gpu.cu index cee77cc3..36e83d19 100644 --- a/lib/src/solvers/gpu/solver_rk_gpu.cu +++ b/lib/src/solvers/gpu/solver_rk_gpu.cu @@ -277,6 +277,13 @@ static cfd_status_t solve_rk_gpu(flow_field* field, const grid* g, "use a CPU, OMP, or AVX2 solver"); return CFD_ERROR_UNSUPPORTED; } + // RANS turbulence models have no GPU kernels. + if (params->turb_model != TURB_MODEL_NONE) { + cfd_set_error(CFD_ERROR_UNSUPPORTED, + "GPU RK solver does not support turbulence models; " + "use a CPU, OMP, or AVX2 solver"); + return CFD_ERROR_UNSUPPORTED; + } // Energy-equation support (heat_source_func + thermal BC types/grid). { cfd_status_t e = gpu_check_energy_support(params, nx, ny, nz); diff --git a/lib/src/solvers/navier_stokes/avx2/solver_explicit_euler_avx2.c b/lib/src/solvers/navier_stokes/avx2/solver_explicit_euler_avx2.c index f038b322..807c791a 100644 --- a/lib/src/solvers/navier_stokes/avx2/solver_explicit_euler_avx2.c +++ b/lib/src/solvers/navier_stokes/avx2/solver_explicit_euler_avx2.c @@ -24,7 +24,9 @@ #include "cfd/core/memory.h" #include "cfd/solvers/navier_stokes_solver.h" #include "cfd/solvers/energy_solver.h" +#include "cfd/solvers/turbulence_solver.h" #include "../../energy/energy_solver_internal.h" +#include "../../turbulence/turbulence_solver_internal.h" #include "../boundary_copy_utils.h" @@ -66,7 +68,8 @@ typedef struct { double* v_new; double* w_new; double* p_new; - double* T_ws; /* Reusable scratch for the energy step (avoids per-step alloc) */ + double* T_ws; /* Reusable scratch for the energy step (avoids per-step alloc) */ + double* turb_ws; /* Reusable scratch for the turbulence step; always allocated */ double* dx_inv; double* dy_inv; size_t nx; @@ -126,37 +129,26 @@ cfd_status_t explicit_euler_simd_init(struct NSSolver* solver, const grid* grid, ctx->inv_2dz = (grid->nz > 1 && grid->dz) ? 1.0 / (2.0 * grid->dz[0]) : 0.0; ctx->inv_dz2 = (grid->nz > 1 && grid->dz) ? 1.0 / (grid->dz[0] * grid->dz[0]) : 0.0; - ctx->u_new = (double*)cfd_aligned_malloc(field_size); - ctx->v_new = (double*)cfd_aligned_malloc(field_size); - ctx->w_new = (double*)cfd_aligned_malloc(field_size); - ctx->p_new = (double*)cfd_aligned_malloc(field_size); - ctx->T_ws = (double*)cfd_aligned_malloc(field_size); - ctx->dx_inv = (double*)cfd_aligned_malloc(ctx->nx * sizeof(double)); - ctx->dy_inv = (double*)cfd_aligned_malloc(ctx->ny * sizeof(double)); + size_t n_total = ctx->nx * ctx->ny * ctx->nz; + ctx->u_new = (double*)cfd_aligned_malloc(field_size); + ctx->v_new = (double*)cfd_aligned_malloc(field_size); + ctx->w_new = (double*)cfd_aligned_malloc(field_size); + ctx->p_new = (double*)cfd_aligned_malloc(field_size); + ctx->T_ws = (double*)cfd_aligned_malloc(field_size); + ctx->turb_ws = (double*)cfd_calloc(TURB_WORKSPACE_SIZE(n_total), sizeof(double)); + ctx->dx_inv = (double*)cfd_aligned_malloc(ctx->nx * sizeof(double)); + ctx->dy_inv = (double*)cfd_aligned_malloc(ctx->ny * sizeof(double)); if (!ctx->u_new || !ctx->v_new || !ctx->w_new || !ctx->p_new || !ctx->T_ws || - !ctx->dx_inv || !ctx->dy_inv) { - if (ctx->u_new) { - cfd_aligned_free(ctx->u_new); - } - if (ctx->v_new) { - cfd_aligned_free(ctx->v_new); - } - if (ctx->w_new) { - cfd_aligned_free(ctx->w_new); - } - if (ctx->p_new) { - cfd_aligned_free(ctx->p_new); - } - if (ctx->T_ws) { - cfd_aligned_free(ctx->T_ws); - } - if (ctx->dx_inv) { - cfd_aligned_free(ctx->dx_inv); - } - if (ctx->dy_inv) { - cfd_aligned_free(ctx->dy_inv); - } + !ctx->turb_ws || !ctx->dx_inv || !ctx->dy_inv) { + if (ctx->u_new) { cfd_aligned_free(ctx->u_new); } + if (ctx->v_new) { cfd_aligned_free(ctx->v_new); } + if (ctx->w_new) { cfd_aligned_free(ctx->w_new); } + if (ctx->p_new) { cfd_aligned_free(ctx->p_new); } + if (ctx->T_ws) { cfd_aligned_free(ctx->T_ws); } + if (ctx->turb_ws) { cfd_free(ctx->turb_ws); } + if (ctx->dx_inv) { cfd_aligned_free(ctx->dx_inv); } + if (ctx->dy_inv) { cfd_aligned_free(ctx->dy_inv); } cfd_free(ctx); return CFD_ERROR_NOMEM; } @@ -198,6 +190,7 @@ void explicit_euler_simd_destroy(struct NSSolver* solver) { cfd_aligned_free(ctx->w_new); cfd_aligned_free(ctx->p_new); cfd_aligned_free(ctx->T_ws); + cfd_free(ctx->turb_ws); cfd_aligned_free(ctx->dx_inv); cfd_aligned_free(ctx->dy_inv); } @@ -433,10 +426,10 @@ static void process_simd_row(explicit_euler_simd_context* ctx, flow_field* field } #endif -#if !USE_AVX -static void process_scalar_row(explicit_euler_simd_context* ctx, flow_field* field, - const grid* grid, const ns_solver_params_t* params, size_t j, - double conservative_dt, double t, size_t stride_z, size_t k_offset) { +static void process_scalar_row_turb(explicit_euler_simd_context* ctx, flow_field* field, + const grid* grid, const ns_solver_params_t* params, size_t j, + double conservative_dt, double t, size_t stride_z, + size_t k_offset, int turb_on_flag) { for (size_t i = 1; i < ctx->nx - 1; i++) { size_t idx = k_offset + IDX_2D(i, j, ctx->nx); @@ -474,10 +467,6 @@ static void process_scalar_row(explicit_euler_simd_context* ctx, flow_field* fie ctx->inv_dz2; double rho = fmax(field->rho[idx], 1e-10); - // Using manual define for M_PI just in case it is missed in fallback -#ifndef M_PI -#define M_PI 3.14159265358979323846 -#endif double nu = fmin(params->mu / rho, 1.0); du_dx = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, du_dx)); @@ -512,12 +501,47 @@ static void process_scalar_row(explicit_euler_simd_context* ctx, flow_field* fie double v_c = field->v[idx]; double w_c = field->w[idx]; + double visc_u, visc_v, visc_w; + if (!turb_on_flag) { + visc_u = nu * (d2u_dx2 + d2u_dy2 + d2u_dz2); + visc_v = nu * (d2v_dx2 + d2v_dy2 + d2v_dz2); + visc_w = nu * (d2w_dx2 + d2w_dy2 + d2w_dz2); + } else { + const double* nu_t = field->nu_t; + double dx2 = grid->dx[i] * grid->dx[i]; + double dy2 = grid->dy[j] * grid->dy[j]; + /* Face-averaged nu_eff = nu + nu_t (nu_t is bounded by the + * realizability clamp) */ + double nu_xp = nu + 0.5 * (nu_t[idx] + nu_t[idx + 1]); + double nu_xm = nu + 0.5 * (nu_t[idx] + nu_t[idx - 1]); + double nu_yp = nu + 0.5 * (nu_t[idx] + nu_t[idx + ctx->nx]); + double nu_ym = nu + 0.5 * (nu_t[idx] + nu_t[idx - ctx->nx]); + visc_u = (nu_xp * (field->u[idx + 1] - u_c) - + nu_xm * (u_c - field->u[idx - 1])) / dx2 + + (nu_yp * (field->u[idx + ctx->nx] - u_c) - + nu_ym * (u_c - field->u[idx - ctx->nx])) / dy2 + + nu * d2u_dz2; + visc_v = (nu_xp * (field->v[idx + 1] - v_c) - + nu_xm * (v_c - field->v[idx - 1])) / dx2 + + (nu_yp * (field->v[idx + ctx->nx] - v_c) - + nu_ym * (v_c - field->v[idx - ctx->nx])) / dy2 + + nu * d2v_dz2; + visc_w = (nu_xp * (field->w[idx + 1] - w_c) - + nu_xm * (w_c - field->w[idx - 1])) / dx2 + + (nu_yp * (field->w[idx + ctx->nx] - w_c) - + nu_ym * (w_c - field->w[idx - ctx->nx])) / dy2 + + nu * d2w_dz2; + visc_u = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, visc_u)); + visc_v = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, visc_v)); + visc_w = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, visc_w)); + } + double du = conservative_dt * (-u_c * du_dx - v_c * du_dy - w_c * du_dz - - dp_dx / rho + nu * (d2u_dx2 + d2u_dy2 + d2u_dz2) + source_u); + dp_dx / rho + visc_u + source_u); double dv = conservative_dt * (-u_c * dv_dx - v_c * dv_dy - w_c * dv_dz - - dp_dy / rho + nu * (d2v_dx2 + d2v_dy2 + d2v_dz2) + source_v); + dp_dy / rho + visc_v + source_v); double dw = conservative_dt * (-u_c * dw_dx - v_c * dw_dy - w_c * dw_dz - - dp_dz / rho + nu * (d2w_dx2 + d2w_dy2 + d2w_dz2) + source_w); + dp_dz / rho + visc_w + source_w); du = fmax(-UPDATE_LIMIT, fmin(UPDATE_LIMIT, du)); dv = fmax(-UPDATE_LIMIT, fmin(UPDATE_LIMIT, dv)); @@ -534,7 +558,6 @@ static void process_scalar_row(explicit_euler_simd_context* ctx, flow_field* fie ctx->p_new[idx] = field->p[idx] + dp; } } -#endif cfd_status_t explicit_euler_simd_step(struct NSSolver* solver, flow_field* field, const grid* grid, const ns_solver_params_t* params, ns_solver_stats_t* stats) { @@ -543,6 +566,7 @@ cfd_status_t explicit_euler_simd_step(struct NSSolver* solver, flow_field* field } explicit_euler_simd_context* ctx = (explicit_euler_simd_context*)solver->context; + const int turb_on = (params->turb_model != TURB_MODEL_NONE); if (field->nx < 3 || field->ny < 3 || (field->nz > 1 && field->nz < 3)) { return CFD_ERROR_INVALID; @@ -565,32 +589,42 @@ cfd_status_t explicit_euler_simd_step(struct NSSolver* solver, flow_field* field size_t nx = ctx->nx; size_t ny = ctx->ny; -#if USE_AVX - simd_constants sc; - init_simd_constants(&sc, params, conservative_dt, ctx->inv_2dz, ctx->inv_dz2); - int ny_int = (int)(ctx->ny); int j; - for (size_t k = ctx->k_start; k < ctx->k_end; k++) { - size_t k_offset = k * ctx->stride_z; +#if USE_AVX + if (!turb_on) { + simd_constants sc; + init_simd_constants(&sc, params, conservative_dt, ctx->inv_2dz, ctx->inv_dz2); + for (size_t k = ctx->k_start; k < ctx->k_end; k++) { + size_t k_offset = k * ctx->stride_z; #ifdef _OPENMP - #pragma omp parallel for schedule(static) + #pragma omp parallel for schedule(static) #endif - for (j = 1; j < ny_int - 1; j++) { - process_simd_row(ctx, field, grid, (size_t)j, &sc, ctx->stride_z, k_offset); + for (j = 1; j < ny_int - 1; j++) { + process_simd_row(ctx, field, grid, (size_t)j, &sc, ctx->stride_z, k_offset); + } + } + } else { + for (size_t k = ctx->k_start; k < ctx->k_end; k++) { + size_t k_offset = k * ctx->stride_z; +#ifdef _OPENMP + #pragma omp parallel for schedule(static) +#endif + for (j = 1; j < ny_int - 1; j++) { + process_scalar_row_turb(ctx, field, grid, params, (size_t)j, conservative_dt, + 0.0, ctx->stride_z, k_offset, 1); + } } } #else - int ny_int = (int)(ctx->ny); - int j; for (size_t k = ctx->k_start; k < ctx->k_end; k++) { size_t k_offset = k * ctx->stride_z; #ifdef _OPENMP #pragma omp parallel for schedule(static) #endif for (j = 1; j < ny_int - 1; j++) { - process_scalar_row(ctx, field, grid, params, (size_t)j, conservative_dt, 0.0, - ctx->stride_z, k_offset); + process_scalar_row_turb(ctx, field, grid, params, (size_t)j, conservative_dt, 0.0, + ctx->stride_z, k_offset, turb_on); } } #endif @@ -625,6 +659,21 @@ cfd_status_t explicit_euler_simd_step(struct NSSolver* solver, flow_field* field } } + /* Turbulence transport: advance k-eps/SA with the updated velocity, + * then apply turbulence BCs. nu_t is updated once per step. */ + { + cfd_status_t turb_status = turbulence_step_explicit_avx2_with_workspace( + field, grid, params, conservative_dt, + ctx->iter_count * conservative_dt, ctx->turb_ws, + turb_on ? TURB_WORKSPACE_SIZE(size) : 0); + if (turb_status == CFD_SUCCESS) { + turb_status = turbulence_apply_bcs(field, grid, params); + } + if (turb_status != CFD_SUCCESS) { + return turb_status; + } + } + if (stats) { stats->iterations = 1; } diff --git a/lib/src/solvers/navier_stokes/avx2/solver_projection_avx2.c b/lib/src/solvers/navier_stokes/avx2/solver_projection_avx2.c index 44ce850f..e9412ee0 100644 --- a/lib/src/solvers/navier_stokes/avx2/solver_projection_avx2.c +++ b/lib/src/solvers/navier_stokes/avx2/solver_projection_avx2.c @@ -18,7 +18,9 @@ #include "cfd/solvers/navier_stokes_solver.h" #include "cfd/solvers/poisson_solver.h" #include "cfd/solvers/energy_solver.h" +#include "cfd/solvers/turbulence_solver.h" #include "../../energy/energy_solver_internal.h" +#include "../../turbulence/turbulence_solver_internal.h" #include "../boundary_copy_utils.h" @@ -55,7 +57,8 @@ typedef struct { double* p_new; double* rhs; double* u_new; /* used as p_temp for Poisson solver */ - double* T_ws; /* Reusable scratch for the energy step (avoids per-step alloc) */ + double* T_ws; /* Reusable scratch for the energy step (avoids per-step alloc) */ + double* turb_ws; /* Reusable scratch for the turbulence step; always allocated */ size_t nx; size_t ny; size_t nz; @@ -123,37 +126,26 @@ cfd_status_t projection_simd_init(struct NSSolver* solver, const grid* grid, ctx->inv_2dz = (grid->nz > 1 && grid->dz) ? 1.0 / (2.0 * dz) : 0.0; ctx->inv_dz2 = (grid->nz > 1 && grid->dz) ? 1.0 / (dz * dz) : 0.0; - ctx->u_star = (double*)cfd_aligned_malloc(size); - ctx->v_star = (double*)cfd_aligned_malloc(size); - ctx->w_star = (double*)cfd_aligned_malloc(size); - ctx->p_new = (double*)cfd_aligned_malloc(size); - ctx->rhs = (double*)cfd_aligned_malloc(size); - ctx->u_new = (double*)cfd_aligned_malloc(size); - ctx->T_ws = (double*)cfd_aligned_malloc(size); + size_t n_total = ctx->nx * ctx->ny * ctx->nz; + ctx->u_star = (double*)cfd_aligned_malloc(size); + ctx->v_star = (double*)cfd_aligned_malloc(size); + ctx->w_star = (double*)cfd_aligned_malloc(size); + ctx->p_new = (double*)cfd_aligned_malloc(size); + ctx->rhs = (double*)cfd_aligned_malloc(size); + ctx->u_new = (double*)cfd_aligned_malloc(size); + ctx->T_ws = (double*)cfd_aligned_malloc(size); + ctx->turb_ws = (double*)cfd_calloc(TURB_WORKSPACE_SIZE(n_total), sizeof(double)); if (!ctx->u_star || !ctx->v_star || !ctx->w_star || !ctx->p_new || - !ctx->rhs || !ctx->u_new || !ctx->T_ws) { - if (ctx->u_star) { - cfd_aligned_free(ctx->u_star); - } - if (ctx->v_star) { - cfd_aligned_free(ctx->v_star); - } - if (ctx->w_star) { - cfd_aligned_free(ctx->w_star); - } - if (ctx->p_new) { - cfd_aligned_free(ctx->p_new); - } - if (ctx->rhs) { - cfd_aligned_free(ctx->rhs); - } - if (ctx->u_new) { - cfd_aligned_free(ctx->u_new); - } - if (ctx->T_ws) { - cfd_aligned_free(ctx->T_ws); - } + !ctx->rhs || !ctx->u_new || !ctx->T_ws || !ctx->turb_ws) { + if (ctx->u_star) { cfd_aligned_free(ctx->u_star); } + if (ctx->v_star) { cfd_aligned_free(ctx->v_star); } + if (ctx->w_star) { cfd_aligned_free(ctx->w_star); } + if (ctx->p_new) { cfd_aligned_free(ctx->p_new); } + if (ctx->rhs) { cfd_aligned_free(ctx->rhs); } + if (ctx->u_new) { cfd_aligned_free(ctx->u_new); } + if (ctx->T_ws) { cfd_aligned_free(ctx->T_ws); } + if (ctx->turb_ws) { cfd_free(ctx->turb_ws); } cfd_free(ctx); return CFD_ERROR_NOMEM; } @@ -174,6 +166,7 @@ void projection_simd_destroy(struct NSSolver* solver) { cfd_aligned_free(ctx->rhs); cfd_aligned_free(ctx->u_new); cfd_aligned_free(ctx->T_ws); + cfd_free(ctx->turb_ws); } cfd_free(ctx); solver->context = NULL; @@ -206,6 +199,8 @@ cfd_status_t projection_simd_step(struct NSSolver* solver, flow_field* field, co double dt = params->dt; double nu = params->mu; // Viscosity (treated as kinematic for ρ=1) + const int turb_on = (params->turb_model != TURB_MODEL_NONE); + double* u_star = ctx->u_star; double* v_star = ctx->v_star; double* w_star = ctx->w_star; @@ -278,9 +273,35 @@ cfd_status_t projection_simd_step(struct NSSolver* solver, flow_field* field, co double d2w_dz2 = (field->w[idx + ctx->stride_z] - 2.0 * w + field->w[idx - ctx->stride_z]) * ctx->inv_dz2; - double visc_u = nu * (d2u_dx2 + d2u_dy2 + d2u_dz2); - double visc_v = nu * (d2v_dx2 + d2v_dy2 + d2v_dz2); - double visc_w = nu * (d2w_dx2 + d2w_dy2 + d2w_dz2); + double visc_u, visc_v, visc_w; + if (!turb_on) { + visc_u = nu * (d2u_dx2 + d2u_dy2 + d2u_dz2); + visc_v = nu * (d2v_dx2 + d2v_dy2 + d2v_dz2); + visc_w = nu * (d2w_dx2 + d2w_dy2 + d2w_dz2); + } else { + const double* nu_t = field->nu_t; + double dx2 = dx * dx; + double dy2 = dy * dy; + double nu_xp = nu + 0.5 * (nu_t[idx] + nu_t[idx + 1]); + double nu_xm = nu + 0.5 * (nu_t[idx] + nu_t[idx - 1]); + double nu_yp = nu + 0.5 * (nu_t[idx] + nu_t[idx + nx]); + double nu_ym = nu + 0.5 * (nu_t[idx] + nu_t[idx - nx]); + visc_u = (nu_xp * (field->u[idx + 1] - u) - + nu_xm * (u - field->u[idx - 1])) / dx2 + + (nu_yp * (field->u[idx + nx] - u) - + nu_ym * (u - field->u[idx - nx])) / dy2 + + nu * d2u_dz2; + visc_v = (nu_xp * (field->v[idx + 1] - v) - + nu_xm * (v - field->v[idx - 1])) / dx2 + + (nu_yp * (field->v[idx + nx] - v) - + nu_ym * (v - field->v[idx - nx])) / dy2 + + nu * d2v_dz2; + visc_w = (nu_xp * (field->w[idx + 1] - w) - + nu_xm * (w - field->w[idx - 1])) / dx2 + + (nu_yp * (field->w[idx + nx] - w) - + nu_ym * (w - field->w[idx - nx])) / dy2 + + nu * d2w_dz2; + } // Source terms double source_u = 0.0; @@ -467,6 +488,20 @@ cfd_status_t projection_simd_step(struct NSSolver* solver, flow_field* field, co copy_boundary_velocities_3d(field->u, field->v, field->w, u_star, v_star, w_star, nx, ny, ctx->nz); + // Turbulence transport: advance k-eps/SA with the corrected velocity, + // then apply turbulence BCs (including wall functions). + { + cfd_status_t turb_status = turbulence_step_explicit_avx2_with_workspace( + field, grid, params, dt, ctx->iter_count * dt, ctx->turb_ws, + turb_on ? TURB_WORKSPACE_SIZE(size) : 0); + if (turb_status == CFD_SUCCESS) { + turb_status = turbulence_apply_bcs(field, grid, params); + } + if (turb_status != CFD_SUCCESS) { + return turb_status; + } + } + // Check for NaN for (size_t n = 0; n < size; n++) { if (!isfinite(field->u[n]) || !isfinite(field->v[n]) || diff --git a/lib/src/solvers/navier_stokes/avx2/solver_rk2_avx2.c b/lib/src/solvers/navier_stokes/avx2/solver_rk2_avx2.c index c1249271..578f147e 100644 --- a/lib/src/solvers/navier_stokes/avx2/solver_rk2_avx2.c +++ b/lib/src/solvers/navier_stokes/avx2/solver_rk2_avx2.c @@ -33,7 +33,9 @@ #include "cfd/core/memory.h" #include "cfd/solvers/navier_stokes_solver.h" #include "cfd/solvers/energy_solver.h" +#include "cfd/solvers/turbulence_solver.h" #include "../../energy/energy_solver_internal.h" +#include "../../turbulence/turbulence_solver_internal.h" #include #include @@ -68,6 +70,7 @@ typedef struct { double* k2_u; double* k2_v; double* k2_w; double* k2_p; double* u0; double* v0; double* w0; double* p0; double* T_ws; /* Reusable scratch for the energy step (avoids per-step alloc) */ + double* turb_ws; /* Reusable scratch for the turbulence step; always allocated */ double* dx_inv; /* 1/(2*dx[i]) for i = 0..nx-1 */ double* dy_inv; /* 1/(2*dy[j]) for j = 0..ny-1 */ size_t nx, ny, nz; @@ -126,6 +129,7 @@ static cfd_status_t rk2_avx2_impl(flow_field* field, rk2_avx2_context_t* ctx, memset(ctx->k1_w, 0, bytes); memset(ctx->k1_p, 0, bytes); compute_rhs_avx2(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, ctx->k1_u, ctx->k1_v, ctx->k1_w, ctx->k1_p, ctx, g, params, iter, dt); @@ -160,6 +164,7 @@ static cfd_status_t rk2_avx2_impl(flow_field* field, rk2_avx2_context_t* ctx, memset(ctx->k2_w, 0, bytes); memset(ctx->k2_p, 0, bytes); compute_rhs_avx2(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, ctx->k2_u, ctx->k2_v, ctx->k2_w, ctx->k2_p, ctx, g, params, iter, dt); @@ -201,6 +206,23 @@ static cfd_status_t rk2_avx2_impl(flow_field* field, rk2_avx2_context_t* ctx, goto cleanup; } + /* Turbulence transport: advance k-eps/SA with the updated velocity, + * then apply turbulence BCs. nu_t is frozen across RK stages and + * updated once per full step. */ + { + const int turb_on = (params->turb_model != TURB_MODEL_NONE); + cfd_status_t turb_status = turbulence_step_explicit_avx2_with_workspace( + field, g, params, dt, iter * dt, ctx->turb_ws, + turb_on ? TURB_WORKSPACE_SIZE(n) : 0); + if (turb_status == CFD_SUCCESS) { + turb_status = turbulence_apply_bcs(field, g, params); + } + if (turb_status != CFD_SUCCESS) { + status = turb_status; + goto cleanup; + } + } + /* NaN / Inf check (parallelized) */ { int has_nan = 0; @@ -278,6 +300,7 @@ cfd_status_t rk2_avx2_init(ns_solver_t* solver, const grid* g, ctx->inv_2dz = (g->nz > 1 && g->dz) ? 1.0 / (2.0 * g->dz[0]) : 0.0; ctx->inv_dz2 = (g->nz > 1 && g->dz) ? 1.0 / (g->dz[0] * g->dz[0]) : 0.0; + size_t n_total = ctx->nx * ctx->ny * ctx->nz; ctx->k1_u = (double*)cfd_aligned_malloc(bytes); ctx->k1_v = (double*)cfd_aligned_malloc(bytes); ctx->k1_w = (double*)cfd_aligned_malloc(bytes); @@ -291,13 +314,14 @@ cfd_status_t rk2_avx2_init(ns_solver_t* solver, const grid* g, ctx->w0 = (double*)cfd_aligned_malloc(bytes); ctx->p0 = (double*)cfd_aligned_malloc(bytes); ctx->T_ws = (double*)cfd_aligned_malloc(bytes); + ctx->turb_ws = (double*)cfd_calloc(TURB_WORKSPACE_SIZE(n_total), sizeof(double)); ctx->dx_inv = (double*)cfd_aligned_malloc(ctx->nx * sizeof(double)); ctx->dy_inv = (double*)cfd_aligned_malloc(ctx->ny * sizeof(double)); if (!ctx->k1_u || !ctx->k1_v || !ctx->k1_w || !ctx->k1_p || !ctx->k2_u || !ctx->k2_v || !ctx->k2_w || !ctx->k2_p || !ctx->u0 || !ctx->v0 || !ctx->w0 || !ctx->p0 || - !ctx->T_ws || !ctx->dx_inv || !ctx->dy_inv) { + !ctx->T_ws || !ctx->turb_ws || !ctx->dx_inv || !ctx->dy_inv) { cfd_aligned_free(ctx->k1_u); cfd_aligned_free(ctx->k1_v); cfd_aligned_free(ctx->k1_w); cfd_aligned_free(ctx->k1_p); cfd_aligned_free(ctx->k2_u); cfd_aligned_free(ctx->k2_v); @@ -305,6 +329,7 @@ cfd_status_t rk2_avx2_init(ns_solver_t* solver, const grid* g, cfd_aligned_free(ctx->u0); cfd_aligned_free(ctx->v0); cfd_aligned_free(ctx->w0); cfd_aligned_free(ctx->p0); cfd_aligned_free(ctx->T_ws); + cfd_free(ctx->turb_ws); cfd_aligned_free(ctx->dx_inv); cfd_aligned_free(ctx->dy_inv); cfd_free(ctx); @@ -345,6 +370,7 @@ void rk2_avx2_destroy(ns_solver_t* solver) cfd_aligned_free(ctx->u0); cfd_aligned_free(ctx->v0); cfd_aligned_free(ctx->w0); cfd_aligned_free(ctx->p0); cfd_aligned_free(ctx->T_ws); + cfd_free(ctx->turb_ws); cfd_aligned_free(ctx->dx_inv); cfd_aligned_free(ctx->dy_inv); } @@ -381,9 +407,10 @@ cfd_status_t rk2_avx2_step(ns_solver_t* solver, flow_field* field, const grid* g double max_vel = 0.0, max_p = 0.0; ptrdiff_t n_s = (ptrdiff_t)(field->nx * field->ny * field->nz); double max_t = (field->T && n_s > 0) ? field->T[0] : 0.0; + double max_nt = 0.0; ptrdiff_t ks; #if defined(_OPENMP) && (_OPENMP >= 201107) - #pragma omp parallel for reduction(max: max_vel, max_p, max_t) schedule(static) + #pragma omp parallel for reduction(max: max_vel, max_p, max_t, max_nt) schedule(static) #endif for (ks = 0; ks < n_s; ks++) { double vel = sqrt(field->u[ks] * field->u[ks] + @@ -393,10 +420,12 @@ cfd_status_t rk2_avx2_step(ns_solver_t* solver, flow_field* field, const grid* g double ap = fabs(field->p[ks]); if (ap > max_p) max_p = ap; if (field->T && field->T[ks] > max_t) max_t = field->T[ks]; + if (field->nu_t && field->nu_t[ks] > max_nt) max_nt = field->nu_t[ks]; } stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = max_t; + stats->max_nu_t = max_nt; } return status; @@ -431,9 +460,10 @@ cfd_status_t rk2_avx2_solve(ns_solver_t* solver, flow_field* field, const grid* double max_vel = 0.0, max_p = 0.0; ptrdiff_t n_s = (ptrdiff_t)(field->nx * field->ny * field->nz); double max_t = (field->T && n_s > 0) ? field->T[0] : 0.0; + double max_nt = 0.0; ptrdiff_t ks; #if defined(_OPENMP) && (_OPENMP >= 201107) - #pragma omp parallel for reduction(max: max_vel, max_p, max_t) schedule(static) + #pragma omp parallel for reduction(max: max_vel, max_p, max_t, max_nt) schedule(static) #endif for (ks = 0; ks < n_s; ks++) { double vel = sqrt(field->u[ks] * field->u[ks] + @@ -443,10 +473,12 @@ cfd_status_t rk2_avx2_solve(ns_solver_t* solver, flow_field* field, const grid* double ap = fabs(field->p[ks]); if (ap > max_p) max_p = ap; if (field->T && field->T[ks] > max_t) max_t = field->T[ks]; + if (field->nu_t && field->nu_t[ks] > max_nt) max_nt = field->nu_t[ks]; } stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = max_t; + stats->max_nu_t = max_nt; } return status; #endif diff --git a/lib/src/solvers/navier_stokes/avx2/solver_rk4_avx2.c b/lib/src/solvers/navier_stokes/avx2/solver_rk4_avx2.c index 614c5441..107067a2 100644 --- a/lib/src/solvers/navier_stokes/avx2/solver_rk4_avx2.c +++ b/lib/src/solvers/navier_stokes/avx2/solver_rk4_avx2.c @@ -34,7 +34,9 @@ #include "cfd/core/memory.h" #include "cfd/solvers/navier_stokes_solver.h" #include "cfd/solvers/energy_solver.h" +#include "cfd/solvers/turbulence_solver.h" #include "../../energy/energy_solver_internal.h" +#include "../../turbulence/turbulence_solver_internal.h" #include #include @@ -71,6 +73,7 @@ typedef struct { double* k4_u; double* k4_v; double* k4_w; double* k4_p; double* u0; double* v0; double* w0; double* p0; double* T_ws; /* Reusable scratch for the energy step (avoids per-step alloc) */ + double* turb_ws; /* Reusable scratch for the turbulence step; always allocated */ double* dx_inv; /* 1/(2*dx[i]) for i = 0..nx-1 */ double* dy_inv; /* 1/(2*dy[j]) for j = 0..ny-1 */ size_t nx, ny, nz; @@ -155,6 +158,7 @@ static cfd_status_t rk4_avx2_impl(flow_field* field, rk4_avx2_context_t* ctx, memset(ctx->k1_w, 0, bytes); memset(ctx->k1_p, 0, bytes); compute_rhs_avx2(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, ctx->k1_u, ctx->k1_v, ctx->k1_w, ctx->k1_p, ctx, g, params, iter, dt); @@ -166,6 +170,7 @@ static cfd_status_t rk4_avx2_impl(flow_field* field, rk4_avx2_context_t* ctx, memset(ctx->k2_w, 0, bytes); memset(ctx->k2_p, 0, bytes); compute_rhs_avx2(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, ctx->k2_u, ctx->k2_v, ctx->k2_w, ctx->k2_p, ctx, g, params, iter, dt); @@ -177,6 +182,7 @@ static cfd_status_t rk4_avx2_impl(flow_field* field, rk4_avx2_context_t* ctx, memset(ctx->k3_w, 0, bytes); memset(ctx->k3_p, 0, bytes); compute_rhs_avx2(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, ctx->k3_u, ctx->k3_v, ctx->k3_w, ctx->k3_p, ctx, g, params, iter, dt); @@ -188,6 +194,7 @@ static cfd_status_t rk4_avx2_impl(flow_field* field, rk4_avx2_context_t* ctx, memset(ctx->k4_w, 0, bytes); memset(ctx->k4_p, 0, bytes); compute_rhs_avx2(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, ctx->k4_u, ctx->k4_v, ctx->k4_w, ctx->k4_p, ctx, g, params, iter, dt); @@ -234,6 +241,23 @@ static cfd_status_t rk4_avx2_impl(flow_field* field, rk4_avx2_context_t* ctx, goto cleanup; } + /* Turbulence transport: advance k-eps/SA with the updated velocity, + * then apply turbulence BCs. nu_t is frozen across RK stages and + * updated once per full step. */ + { + const int turb_on = (params->turb_model != TURB_MODEL_NONE); + cfd_status_t turb_status = turbulence_step_explicit_avx2_with_workspace( + field, g, params, dt, iter * dt, ctx->turb_ws, + turb_on ? TURB_WORKSPACE_SIZE(n) : 0); + if (turb_status == CFD_SUCCESS) { + turb_status = turbulence_apply_bcs(field, g, params); + } + if (turb_status != CFD_SUCCESS) { + status = turb_status; + goto cleanup; + } + } + /* NaN / Inf check (parallelized) */ { int has_nan = 0; @@ -311,6 +335,7 @@ cfd_status_t rk4_avx2_init(ns_solver_t* solver, const grid* g, ctx->inv_2dz = (g->nz > 1 && g->dz) ? 1.0 / (2.0 * g->dz[0]) : 0.0; ctx->inv_dz2 = (g->nz > 1 && g->dz) ? 1.0 / (g->dz[0] * g->dz[0]) : 0.0; + size_t n_total = ctx->nx * ctx->ny * ctx->nz; ctx->k1_u = (double*)cfd_aligned_malloc(bytes); ctx->k1_v = (double*)cfd_aligned_malloc(bytes); ctx->k1_w = (double*)cfd_aligned_malloc(bytes); @@ -332,6 +357,7 @@ cfd_status_t rk4_avx2_init(ns_solver_t* solver, const grid* g, ctx->w0 = (double*)cfd_aligned_malloc(bytes); ctx->p0 = (double*)cfd_aligned_malloc(bytes); ctx->T_ws = (double*)cfd_aligned_malloc(bytes); + ctx->turb_ws = (double*)cfd_calloc(TURB_WORKSPACE_SIZE(n_total), sizeof(double)); ctx->dx_inv = (double*)cfd_aligned_malloc(ctx->nx * sizeof(double)); ctx->dy_inv = (double*)cfd_aligned_malloc(ctx->ny * sizeof(double)); @@ -340,7 +366,7 @@ cfd_status_t rk4_avx2_init(ns_solver_t* solver, const grid* g, !ctx->k3_u || !ctx->k3_v || !ctx->k3_w || !ctx->k3_p || !ctx->k4_u || !ctx->k4_v || !ctx->k4_w || !ctx->k4_p || !ctx->u0 || !ctx->v0 || !ctx->w0 || !ctx->p0 || - !ctx->T_ws || !ctx->dx_inv || !ctx->dy_inv) { + !ctx->T_ws || !ctx->turb_ws || !ctx->dx_inv || !ctx->dy_inv) { cfd_aligned_free(ctx->k1_u); cfd_aligned_free(ctx->k1_v); cfd_aligned_free(ctx->k1_w); cfd_aligned_free(ctx->k1_p); cfd_aligned_free(ctx->k2_u); cfd_aligned_free(ctx->k2_v); @@ -352,6 +378,7 @@ cfd_status_t rk4_avx2_init(ns_solver_t* solver, const grid* g, cfd_aligned_free(ctx->u0); cfd_aligned_free(ctx->v0); cfd_aligned_free(ctx->w0); cfd_aligned_free(ctx->p0); cfd_aligned_free(ctx->T_ws); + cfd_free(ctx->turb_ws); cfd_aligned_free(ctx->dx_inv); cfd_aligned_free(ctx->dy_inv); cfd_free(ctx); @@ -396,6 +423,7 @@ void rk4_avx2_destroy(ns_solver_t* solver) cfd_aligned_free(ctx->u0); cfd_aligned_free(ctx->v0); cfd_aligned_free(ctx->w0); cfd_aligned_free(ctx->p0); cfd_aligned_free(ctx->T_ws); + cfd_free(ctx->turb_ws); cfd_aligned_free(ctx->dx_inv); cfd_aligned_free(ctx->dy_inv); } @@ -432,9 +460,10 @@ cfd_status_t rk4_avx2_step(ns_solver_t* solver, flow_field* field, const grid* g double max_vel = 0.0, max_p = 0.0; ptrdiff_t n_s = (ptrdiff_t)(field->nx * field->ny * field->nz); double max_t = (field->T && n_s > 0) ? field->T[0] : 0.0; + double max_nt = 0.0; ptrdiff_t ks; #if defined(_OPENMP) && (_OPENMP >= 201107) - #pragma omp parallel for reduction(max: max_vel, max_p, max_t) schedule(static) + #pragma omp parallel for reduction(max: max_vel, max_p, max_t, max_nt) schedule(static) #endif for (ks = 0; ks < n_s; ks++) { double vel = sqrt(field->u[ks] * field->u[ks] + @@ -444,10 +473,12 @@ cfd_status_t rk4_avx2_step(ns_solver_t* solver, flow_field* field, const grid* g double ap = fabs(field->p[ks]); if (ap > max_p) max_p = ap; if (field->T && field->T[ks] > max_t) max_t = field->T[ks]; + if (field->nu_t && field->nu_t[ks] > max_nt) max_nt = field->nu_t[ks]; } stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = max_t; + stats->max_nu_t = max_nt; } return status; @@ -482,9 +513,10 @@ cfd_status_t rk4_avx2_solve(ns_solver_t* solver, flow_field* field, const grid* double max_vel = 0.0, max_p = 0.0; ptrdiff_t n_s = (ptrdiff_t)(field->nx * field->ny * field->nz); double max_t = (field->T && n_s > 0) ? field->T[0] : 0.0; + double max_nt = 0.0; ptrdiff_t ks; #if defined(_OPENMP) && (_OPENMP >= 201107) - #pragma omp parallel for reduction(max: max_vel, max_p, max_t) schedule(static) + #pragma omp parallel for reduction(max: max_vel, max_p, max_t, max_nt) schedule(static) #endif for (ks = 0; ks < n_s; ks++) { double vel = sqrt(field->u[ks] * field->u[ks] + @@ -494,10 +526,12 @@ cfd_status_t rk4_avx2_solve(ns_solver_t* solver, flow_field* field, const grid* double ap = fabs(field->p[ks]); if (ap > max_p) max_p = ap; if (field->T && field->T[ks] > max_t) max_t = field->T[ks]; + if (field->nu_t && field->nu_t[ks] > max_nt) max_nt = field->nu_t[ks]; } stats->max_velocity = max_vel; stats->max_pressure = max_p; stats->max_temperature = max_t; + stats->max_nu_t = max_nt; } return status; #endif diff --git a/lib/src/solvers/navier_stokes/cpu/solver_explicit_euler.c b/lib/src/solvers/navier_stokes/cpu/solver_explicit_euler.c index 61b83422..7aa9320e 100644 --- a/lib/src/solvers/navier_stokes/cpu/solver_explicit_euler.c +++ b/lib/src/solvers/navier_stokes/cpu/solver_explicit_euler.c @@ -8,8 +8,10 @@ #include "cfd/solvers/energy_solver.h" #include "cfd/solvers/navier_stokes_solver.h" +#include "cfd/solvers/turbulence_solver.h" #include "../../energy/energy_solver_internal.h" +#include "../../turbulence/turbulence_solver_internal.h" #include "../boundary_copy_utils.h" #include @@ -73,7 +75,9 @@ ns_solver_params_t ns_solver_params_default(void) { .gravity = {0.0, 0.0, 0.0}, .heat_source_func = NULL, .heat_source_context = NULL, - .thermal_bc = {0}}; + .thermal_bc = {0}, + .turb_model = TURB_MODEL_NONE, + .turb_bc = {0}}; return params; } flow_field* flow_field_create(size_t nx, size_t ny, size_t nz) { @@ -100,8 +104,13 @@ flow_field* flow_field_create(size_t nx, size_t ny, size_t nz) { field->p = (double*)cfd_aligned_calloc(total, sizeof(double)); field->rho = (double*)cfd_aligned_calloc(total, sizeof(double)); field->T = (double*)cfd_aligned_calloc(total, sizeof(double)); + field->turb_k = (double*)cfd_aligned_calloc(total, sizeof(double)); + field->turb_eps = (double*)cfd_aligned_calloc(total, sizeof(double)); + field->turb_nu_tilde = (double*)cfd_aligned_calloc(total, sizeof(double)); + field->nu_t = (double*)cfd_aligned_calloc(total, sizeof(double)); - if (!field->u || !field->v || !field->w || !field->p || !field->rho || !field->T) { + if (!field->u || !field->v || !field->w || !field->p || !field->rho || !field->T || + !field->turb_k || !field->turb_eps || !field->turb_nu_tilde || !field->nu_t) { flow_field_destroy(field); return NULL; } @@ -117,6 +126,10 @@ void flow_field_destroy(flow_field* field) { cfd_aligned_free(field->p); cfd_aligned_free(field->rho); cfd_aligned_free(field->T); + cfd_aligned_free(field->turb_k); + cfd_aligned_free(field->turb_eps); + cfd_aligned_free(field->turb_nu_tilde); + cfd_aligned_free(field->nu_t); cfd_free(field); } } @@ -221,6 +234,30 @@ void compute_time_step(flow_field* field, const grid* grid, ns_solver_params_t* double dt_stable = min_double(dt_cfl, dt_thermal); + // Viscous diffusion stability constraint with turbulent eddy viscosity: + // dt < dx^2 / (2 * nu_eff * ndim). Only relevant when a turbulence model + // is active (nu_t can exceed the laminar viscosity by orders of magnitude). + if (params->turb_model != TURB_MODEL_NONE && field->nu_t) { + size_t total = field->nx * field->ny * field->nz; + double nu_t_max = 0.0; + double rho_min = field->rho[0]; + for (size_t n = 0; n < total; n++) { + nu_t_max = max_double(nu_t_max, field->nu_t[n]); + rho_min = min_double(rho_min, field->rho[n]); + } + /* Minimum density gives the largest (most restrictive) local nu; + * floor it like the kernels do (local_nu) rather than substituting + * 1.0, which would overestimate the stable dt at low densities. */ + double rho_ref = max_double(rho_min, 1e-10); + double nu_eff_max = params->mu / rho_ref + nu_t_max; + if (nu_eff_max > 0.0) { + int ndim = (grid->nz > 1) ? 3 : 2; + double dt_visc = (dmin * dmin) / (2.0 * nu_eff_max * ndim); + dt_visc *= params->cfl; // Apply safety factor + dt_stable = min_double(dt_stable, dt_visc); + } + } + // Limit time step to reasonable bounds double dt_max = DT_MAX_LIMIT; // Maximum allowed time step double dt_min = DT_MIN_LIMIT; // Minimum allowed time step @@ -372,11 +409,15 @@ cfd_status_t explicit_euler_impl(flow_field* field, const grid* grid, const ns_s int needs_T_ws = (params->alpha > 0.0 || params->beta != 0.0); double* T_energy_ws = needs_T_ws ? (double*)cfd_calloc(total, sizeof(double)) : NULL; + const int turb_on = (params->turb_model != TURB_MODEL_NONE); + double* turb_ws = turb_on + ? (double*)cfd_calloc(TURB_WORKSPACE_SIZE(total), sizeof(double)) : NULL; if (!u_new || !v_new || !w_new || !p_new || !rho_new || - (needs_T_ws && !T_energy_ws)) { + (needs_T_ws && !T_energy_ws) || (turb_on && !turb_ws)) { cfd_free(u_new); cfd_free(v_new); cfd_free(w_new); cfd_free(p_new); cfd_free(rho_new); cfd_free(T_energy_ws); + cfd_free(turb_ws); return CFD_ERROR_NOMEM; } @@ -483,25 +524,71 @@ cfd_status_t explicit_euler_impl(flow_field* field, const grid* grid, const ns_s energy_compute_buoyancy(field->T[idx], params, &source_u, &source_v, &source_w); + /* Viscous terms: laminar constant-nu Laplacian, or conservative + * face-averaged effective viscosity div((nu + nu_t) grad u) + * when a turbulence model is active. The z-term keeps the + * laminar nu (turbulence is 2D-only; vanishes when nz == 1). */ + double visc_u, visc_v, visc_w; + if (!turb_on) { + visc_u = nu * (d2u_dx2 + d2u_dy2 + d2u_dz2); + visc_v = nu * (d2v_dx2 + d2v_dy2 + d2v_dz2); + visc_w = nu * (d2w_dx2 + d2w_dy2 + d2w_dz2); + } else { + const double* nu_t = field->nu_t; + /* Face-averaged nu_eff = nu + nu_t (nu_t already bounded by + * the realizability clamp; the flux clamp below caps it) */ + double nu_xp = nu + 0.5 * (nu_t[idx] + nu_t[idx + 1]); + double nu_xm = nu + 0.5 * (nu_t[idx] + nu_t[idx - 1]); + double nu_yp = nu + 0.5 * (nu_t[idx] + nu_t[idx + nx]); + double nu_ym = nu + 0.5 * (nu_t[idx] + nu_t[idx - nx]); + double inv_dxi2 = 1.0 / (grid->dx[i] * grid->dx[i]); + double inv_dyj2 = 1.0 / (grid->dy[j] * grid->dy[j]); + + double t_u_x = (nu_xp * (field->u[idx + 1] - u_c) - + nu_xm * (u_c - field->u[idx - 1])) * inv_dxi2; + double t_u_y = (nu_yp * (field->u[idx + nx] - u_c) - + nu_ym * (u_c - field->u[idx - nx])) * inv_dyj2; + double t_v_x = (nu_xp * (field->v[idx + 1] - v_c) - + nu_xm * (v_c - field->v[idx - 1])) * inv_dxi2; + double t_v_y = (nu_yp * (field->v[idx + nx] - v_c) - + nu_ym * (v_c - field->v[idx - nx])) * inv_dyj2; + double t_w_x = (nu_xp * (field->w[idx + 1] - w_c) - + nu_xm * (w_c - field->w[idx - 1])) * inv_dxi2; + double t_w_y = (nu_yp * (field->w[idx + nx] - w_c) - + nu_ym * (w_c - field->w[idx - nx])) * inv_dyj2; + + /* Same safety clamp as the laminar second derivatives */ + t_u_x = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_u_x)); + t_u_y = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_u_y)); + t_v_x = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_v_x)); + t_v_y = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_v_y)); + t_w_x = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_w_x)); + t_w_y = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_w_y)); + + visc_u = t_u_x + t_u_y + nu * d2u_dz2; + visc_v = t_v_x + t_v_y + nu * d2v_dz2; + visc_w = t_w_x + t_w_y + nu * d2w_dz2; + } + /* u-momentum */ double du = conservative_dt * (-u_c * du_dx - v_c * du_dy - w_c * du_dz - dp_dx / field->rho[idx] - + nu * (d2u_dx2 + d2u_dy2 + d2u_dz2) + + visc_u + source_u); /* v-momentum */ double dv = conservative_dt * (-u_c * dv_dx - v_c * dv_dy - w_c * dv_dz - dp_dy / field->rho[idx] - + nu * (d2v_dx2 + d2v_dy2 + d2v_dz2) + + visc_v + source_v); /* w-momentum */ double dw = conservative_dt * (-u_c * dw_dx - v_c * dw_dy - w_c * dw_dz - dp_dz / field->rho[idx] - + nu * (d2w_dx2 + d2w_dy2 + d2w_dz2) + + visc_w + source_w); du = fmax(-UPDATE_LIMIT, fmin(UPDATE_LIMIT, du)); @@ -539,6 +626,7 @@ cfd_status_t explicit_euler_impl(flow_field* field, const grid* grid, const ns_s if (energy_status != CFD_SUCCESS) { cfd_free(u_new); cfd_free(v_new); cfd_free(w_new); cfd_free(p_new); cfd_free(rho_new); cfd_free(T_energy_ws); + cfd_free(turb_ws); return energy_status; } } @@ -554,9 +642,27 @@ cfd_status_t explicit_euler_impl(flow_field* field, const grid* grid, const ns_s if (bc_status != CFD_SUCCESS) { cfd_free(u_new); cfd_free(v_new); cfd_free(w_new); cfd_free(p_new); cfd_free(rho_new); cfd_free(T_energy_ws); + cfd_free(turb_ws); return bc_status; } + /* Turbulence transport: advance k-eps/SA with the updated velocity, + * then apply turbulence BCs (including wall functions) */ + { + cfd_status_t turb_status = turbulence_step_explicit_with_workspace( + field, grid, params, conservative_dt, iter * conservative_dt, + turb_ws, turb_on ? TURB_WORKSPACE_SIZE(total) : 0); + if (turb_status == CFD_SUCCESS) { + turb_status = turbulence_apply_bcs(field, grid, params); + } + if (turb_status != CFD_SUCCESS) { + cfd_free(u_new); cfd_free(v_new); cfd_free(w_new); + cfd_free(p_new); cfd_free(rho_new); cfd_free(T_energy_ws); + cfd_free(turb_ws); + return turb_status; + } + } + /* NaN/Inf check */ int has_nan = 0; for (size_t n = 0; n < total; n++) { @@ -569,6 +675,7 @@ cfd_status_t explicit_euler_impl(flow_field* field, const grid* grid, const ns_s if (has_nan) { cfd_free(u_new); cfd_free(v_new); cfd_free(w_new); cfd_free(p_new); cfd_free(rho_new); cfd_free(T_energy_ws); + cfd_free(turb_ws); cfd_set_error(CFD_ERROR_DIVERGED, "NaN/Inf detected in explicit_euler step"); return CFD_ERROR_DIVERGED; @@ -577,6 +684,7 @@ cfd_status_t explicit_euler_impl(flow_field* field, const grid* grid, const ns_s cfd_free(u_new); cfd_free(v_new); cfd_free(w_new); cfd_free(p_new); cfd_free(rho_new); cfd_free(T_energy_ws); + cfd_free(turb_ws); return CFD_SUCCESS; } diff --git a/lib/src/solvers/navier_stokes/cpu/solver_projection.c b/lib/src/solvers/navier_stokes/cpu/solver_projection.c index 3f7573e7..b479dda5 100644 --- a/lib/src/solvers/navier_stokes/cpu/solver_projection.c +++ b/lib/src/solvers/navier_stokes/cpu/solver_projection.c @@ -24,8 +24,10 @@ #include "cfd/solvers/energy_solver.h" #include "cfd/solvers/navier_stokes_solver.h" #include "cfd/solvers/poisson_solver.h" +#include "cfd/solvers/turbulence_solver.h" #include "../../energy/energy_solver_internal.h" +#include "../../turbulence/turbulence_solver_internal.h" #include "../boundary_copy_utils.h" #include @@ -93,12 +95,15 @@ cfd_status_t solve_projection_method(flow_field* field, const grid* grid, int needs_T_ws = (params->alpha > 0.0 || params->beta != 0.0); double* T_energy_ws = needs_T_ws ? (double*)cfd_calloc(total, sizeof(double)) : NULL; + const int turb_on = (params->turb_model != TURB_MODEL_NONE); + double* turb_ws = turb_on + ? (double*)cfd_calloc(TURB_WORKSPACE_SIZE(total), sizeof(double)) : NULL; if (!u_star || !v_star || !w_star || !p_new || !p_temp || !rhs || - (needs_T_ws && !T_energy_ws)) { + (needs_T_ws && !T_energy_ws) || (turb_on && !turb_ws)) { cfd_free(u_star); cfd_free(v_star); cfd_free(w_star); cfd_free(p_new); cfd_free(p_temp); cfd_free(rhs); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return CFD_ERROR_NOMEM; } @@ -140,25 +145,57 @@ cfd_status_t solve_projection_method(flow_field* field, const grid* grid, double conv_v = u * dv_dx + v * dv_dy + w * dv_dz; double conv_w = u * dw_dx + v * dw_dy + w * dw_dz; - /* Second derivatives (viscous) */ - double d2u_dx2 = (field->u[idx + 1] - 2.0 * u + field->u[idx - 1]) / (dx * dx); - double d2u_dy2 = (field->u[idx + nx] - 2.0 * u + field->u[idx - nx]) / (dy * dy); - double d2u_dz2 = (field->u[idx + stride_z] - 2.0 * u + - field->u[idx - stride_z]) * inv_dz2; - - double d2v_dx2 = (field->v[idx + 1] - 2.0 * v + field->v[idx - 1]) / (dx * dx); - double d2v_dy2 = (field->v[idx + nx] - 2.0 * v + field->v[idx - nx]) / (dy * dy); - double d2v_dz2 = (field->v[idx + stride_z] - 2.0 * v + - field->v[idx - stride_z]) * inv_dz2; - - double d2w_dx2 = (field->w[idx + 1] - 2.0 * w + field->w[idx - 1]) / (dx * dx); - double d2w_dy2 = (field->w[idx + nx] - 2.0 * w + field->w[idx - nx]) / (dy * dy); - double d2w_dz2 = (field->w[idx + stride_z] - 2.0 * w + - field->w[idx - stride_z]) * inv_dz2; - - double visc_u = nu * (d2u_dx2 + d2u_dy2 + d2u_dz2); - double visc_v = nu * (d2v_dx2 + d2v_dy2 + d2v_dz2); - double visc_w = nu * (d2w_dx2 + d2w_dy2 + d2w_dz2); + /* Viscous terms */ + double visc_u, visc_v, visc_w; + if (!turb_on) { + /* Laminar: constant-viscosity Laplacian */ + double d2u_dx2 = (field->u[idx + 1] - 2.0 * u + field->u[idx - 1]) / (dx * dx); + double d2u_dy2 = (field->u[idx + nx] - 2.0 * u + field->u[idx - nx]) / (dy * dy); + double d2u_dz2 = (field->u[idx + stride_z] - 2.0 * u + + field->u[idx - stride_z]) * inv_dz2; + + double d2v_dx2 = (field->v[idx + 1] - 2.0 * v + field->v[idx - 1]) / (dx * dx); + double d2v_dy2 = (field->v[idx + nx] - 2.0 * v + field->v[idx - nx]) / (dy * dy); + double d2v_dz2 = (field->v[idx + stride_z] - 2.0 * v + + field->v[idx - stride_z]) * inv_dz2; + + double d2w_dx2 = (field->w[idx + 1] - 2.0 * w + field->w[idx - 1]) / (dx * dx); + double d2w_dy2 = (field->w[idx + nx] - 2.0 * w + field->w[idx - nx]) / (dy * dy); + double d2w_dz2 = (field->w[idx + stride_z] - 2.0 * w + + field->w[idx - stride_z]) * inv_dz2; + + visc_u = nu * (d2u_dx2 + d2u_dy2 + d2u_dz2); + visc_v = nu * (d2v_dx2 + d2v_dy2 + d2v_dz2); + visc_w = nu * (d2w_dx2 + d2w_dy2 + d2w_dz2); + } else { + /* Turbulent: conservative face-averaged effective viscosity + * div((nu + nu_t) grad u). The z-term keeps the laminar nu + * (turbulence is 2D-only; it vanishes when nz == 1). */ + const double* nu_t = field->nu_t; + double nu_xp = nu + 0.5 * (nu_t[idx] + nu_t[idx + 1]); + double nu_xm = nu + 0.5 * (nu_t[idx] + nu_t[idx - 1]); + double nu_yp = nu + 0.5 * (nu_t[idx] + nu_t[idx + nx]); + double nu_ym = nu + 0.5 * (nu_t[idx] + nu_t[idx - nx]); + + visc_u = (nu_xp * (field->u[idx + 1] - u) - + nu_xm * (u - field->u[idx - 1])) / (dx * dx) + + (nu_yp * (field->u[idx + nx] - u) - + nu_ym * (u - field->u[idx - nx])) / (dy * dy) + + nu * (field->u[idx + stride_z] - 2.0 * u + + field->u[idx - stride_z]) * inv_dz2; + visc_v = (nu_xp * (field->v[idx + 1] - v) - + nu_xm * (v - field->v[idx - 1])) / (dx * dx) + + (nu_yp * (field->v[idx + nx] - v) - + nu_ym * (v - field->v[idx - nx])) / (dy * dy) + + nu * (field->v[idx + stride_z] - 2.0 * v + + field->v[idx - stride_z]) * inv_dz2; + visc_w = (nu_xp * (field->w[idx + 1] - w) - + nu_xm * (w - field->w[idx - 1])) / (dx * dx) + + (nu_yp * (field->w[idx + nx] - w) - + nu_ym * (w - field->w[idx - nx])) / (dy * dy) + + nu * (field->w[idx + stride_z] - 2.0 * w + + field->w[idx - stride_z]) * inv_dz2; + } /* Source terms */ double source_u = 0.0, source_v = 0.0, source_w = 0.0; @@ -220,6 +257,7 @@ cfd_status_t solve_projection_method(flow_field* field, const grid* grid, if (poisson_iters < 0) { cfd_free(u_star); cfd_free(v_star); cfd_free(w_star); cfd_free(p_new); cfd_free(p_temp); cfd_free(rhs); + cfd_free(T_energy_ws); cfd_free(turb_ws); return CFD_ERROR_MAX_ITER; } @@ -259,7 +297,7 @@ cfd_status_t solve_projection_method(flow_field* field, const grid* grid, if (energy_status != CFD_SUCCESS) { cfd_free(u_star); cfd_free(v_star); cfd_free(w_star); cfd_free(p_new); cfd_free(p_temp); cfd_free(rhs); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return energy_status; } } @@ -269,7 +307,7 @@ cfd_status_t solve_projection_method(flow_field* field, const grid* grid, if (bc_status != CFD_SUCCESS) { cfd_free(u_star); cfd_free(v_star); cfd_free(w_star); cfd_free(p_new); cfd_free(p_temp); cfd_free(rhs); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return bc_status; } @@ -277,13 +315,30 @@ cfd_status_t solve_projection_method(flow_field* field, const grid* grid, copy_boundary_velocities_3d(field->u, field->v, field->w, u_star, v_star, w_star, nx, ny, nz); + /* Turbulence transport: advance k-eps/SA with the corrected velocity, + * then apply turbulence BCs (including wall functions) */ + { + cfd_status_t turb_status = turbulence_step_explicit_with_workspace( + field, grid, params, dt, iter * dt, turb_ws, + turb_on ? TURB_WORKSPACE_SIZE(total) : 0); + if (turb_status == CFD_SUCCESS) { + turb_status = turbulence_apply_bcs(field, grid, params); + } + if (turb_status != CFD_SUCCESS) { + cfd_free(u_star); cfd_free(v_star); cfd_free(w_star); + cfd_free(p_new); cfd_free(p_temp); cfd_free(rhs); + cfd_free(T_energy_ws); cfd_free(turb_ws); + return turb_status; + } + } + /* NaN/Inf check */ for (size_t n = 0; n < total; n++) { if (!isfinite(field->u[n]) || !isfinite(field->v[n]) || !isfinite(field->w[n]) || !isfinite(field->p[n])) { cfd_free(u_star); cfd_free(v_star); cfd_free(w_star); cfd_free(p_new); cfd_free(p_temp); cfd_free(rhs); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return CFD_ERROR_DIVERGED; } } @@ -291,7 +346,7 @@ cfd_status_t solve_projection_method(flow_field* field, const grid* grid, cfd_free(u_star); cfd_free(v_star); cfd_free(w_star); cfd_free(p_new); cfd_free(p_temp); cfd_free(rhs); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return CFD_SUCCESS; } diff --git a/lib/src/solvers/navier_stokes/cpu/solver_rk2.c b/lib/src/solvers/navier_stokes/cpu/solver_rk2.c index c15bd6bf..d457581a 100644 --- a/lib/src/solvers/navier_stokes/cpu/solver_rk2.c +++ b/lib/src/solvers/navier_stokes/cpu/solver_rk2.c @@ -22,8 +22,10 @@ #include "cfd/core/memory.h" #include "cfd/solvers/energy_solver.h" #include "cfd/solvers/navier_stokes_solver.h" +#include "cfd/solvers/turbulence_solver.h" #include "../../energy/energy_solver_internal.h" +#include "../../turbulence/turbulence_solver_internal.h" #include #include @@ -95,15 +97,18 @@ cfd_status_t rk2_impl(flow_field* field, const grid* grid, int needs_T_ws = (params->alpha > 0.0 || params->beta != 0.0); double* T_energy_ws = needs_T_ws ? (double*)cfd_calloc(total, sizeof(double)) : NULL; + const int turb_on = (params->turb_model != TURB_MODEL_NONE); + double* turb_ws = turb_on + ? (double*)cfd_calloc(TURB_WORKSPACE_SIZE(total), sizeof(double)) : NULL; if (!k1_u || !k1_v || !k1_w || !k1_p || !k2_u || !k2_v || !k2_w || !k2_p || !u0 || !v0 || !w0 || !p0 || - (needs_T_ws && !T_energy_ws)) { + (needs_T_ws && !T_energy_ws) || (turb_on && !turb_ws)) { cfd_free(k1_u); cfd_free(k1_v); cfd_free(k1_w); cfd_free(k1_p); cfd_free(k2_u); cfd_free(k2_v); cfd_free(k2_w); cfd_free(k2_p); cfd_free(u0); cfd_free(v0); cfd_free(w0); cfd_free(p0); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return CFD_ERROR_NOMEM; } @@ -124,6 +129,7 @@ cfd_status_t rk2_impl(flow_field* field, const grid* grid, memset(k1_p, 0, bytes); compute_rhs(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, k1_u, k1_v, k1_w, k1_p, grid, params, nx, ny, nz, stride_z, k_start, k_end, inv_2dz, inv_dz2, @@ -154,6 +160,7 @@ cfd_status_t rk2_impl(flow_field* field, const grid* grid, memset(k2_p, 0, bytes); compute_rhs(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, k2_u, k2_v, k2_w, k2_p, grid, params, nx, ny, nz, stride_z, k_start, k_end, inv_2dz, inv_dz2, @@ -191,6 +198,19 @@ cfd_status_t rk2_impl(flow_field* field, const grid* grid, goto cleanup; } + /* Turbulence transport: advance k-eps/SA with the updated velocity, + * then apply turbulence BCs (including wall functions). nu_t is + * frozen across RK stages and updated once per full step. */ + status = turbulence_step_explicit_with_workspace( + field, grid, params, dt, iter * dt, turb_ws, + turb_on ? TURB_WORKSPACE_SIZE(total) : 0); + if (status == CFD_SUCCESS) { + status = turbulence_apply_bcs(field, grid, params); + } + if (status != CFD_SUCCESS) { + goto cleanup; + } + /* NaN / Inf check */ for (size_t n = 0; n < total; n++) { if (!isfinite(field->u[n]) || !isfinite(field->v[n]) || @@ -205,7 +225,7 @@ cfd_status_t rk2_impl(flow_field* field, const grid* grid, cfd_free(k1_u); cfd_free(k1_v); cfd_free(k1_w); cfd_free(k1_p); cfd_free(k2_u); cfd_free(k2_v); cfd_free(k2_w); cfd_free(k2_p); cfd_free(u0); cfd_free(v0); cfd_free(w0); cfd_free(p0); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return status; } diff --git a/lib/src/solvers/navier_stokes/cpu/solver_rk4.c b/lib/src/solvers/navier_stokes/cpu/solver_rk4.c index e21715f9..c68b4087 100644 --- a/lib/src/solvers/navier_stokes/cpu/solver_rk4.c +++ b/lib/src/solvers/navier_stokes/cpu/solver_rk4.c @@ -23,8 +23,10 @@ #include "cfd/core/memory.h" #include "cfd/solvers/energy_solver.h" #include "cfd/solvers/navier_stokes_solver.h" +#include "cfd/solvers/turbulence_solver.h" #include "../../energy/energy_solver_internal.h" +#include "../../turbulence/turbulence_solver_internal.h" #include #include @@ -123,19 +125,22 @@ cfd_status_t rk4_impl(flow_field* field, const grid* grid, int needs_T_ws = (params->alpha > 0.0 || params->beta != 0.0); double* T_energy_ws = needs_T_ws ? (double*)cfd_calloc(total, sizeof(double)) : NULL; + const int turb_on = (params->turb_model != TURB_MODEL_NONE); + double* turb_ws = turb_on + ? (double*)cfd_calloc(TURB_WORKSPACE_SIZE(total), sizeof(double)) : NULL; if (!k1_u || !k1_v || !k1_w || !k1_p || !k2_u || !k2_v || !k2_w || !k2_p || !k3_u || !k3_v || !k3_w || !k3_p || !k4_u || !k4_v || !k4_w || !k4_p || !u0 || !v0 || !w0 || !p0 || - (needs_T_ws && !T_energy_ws)) { + (needs_T_ws && !T_energy_ws) || (turb_on && !turb_ws)) { cfd_free(k1_u); cfd_free(k1_v); cfd_free(k1_w); cfd_free(k1_p); cfd_free(k2_u); cfd_free(k2_v); cfd_free(k2_w); cfd_free(k2_p); cfd_free(k3_u); cfd_free(k3_v); cfd_free(k3_w); cfd_free(k3_p); cfd_free(k4_u); cfd_free(k4_v); cfd_free(k4_w); cfd_free(k4_p); cfd_free(u0); cfd_free(v0); cfd_free(w0); cfd_free(p0); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return CFD_ERROR_NOMEM; } @@ -155,6 +160,7 @@ cfd_status_t rk4_impl(flow_field* field, const grid* grid, memset(k1_w, 0, bytes); memset(k1_p, 0, bytes); compute_rhs(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, k1_u, k1_v, k1_w, k1_p, grid, params, nx, ny, nz, stride_z, k_start, k_end, inv_2dz, inv_dz2, @@ -168,6 +174,7 @@ cfd_status_t rk4_impl(flow_field* field, const grid* grid, memset(k2_w, 0, bytes); memset(k2_p, 0, bytes); compute_rhs(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, k2_u, k2_v, k2_w, k2_p, grid, params, nx, ny, nz, stride_z, k_start, k_end, inv_2dz, inv_dz2, @@ -181,6 +188,7 @@ cfd_status_t rk4_impl(flow_field* field, const grid* grid, memset(k3_w, 0, bytes); memset(k3_p, 0, bytes); compute_rhs(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, k3_u, k3_v, k3_w, k3_p, grid, params, nx, ny, nz, stride_z, k_start, k_end, inv_2dz, inv_dz2, @@ -194,6 +202,7 @@ cfd_status_t rk4_impl(flow_field* field, const grid* grid, memset(k4_w, 0, bytes); memset(k4_p, 0, bytes); compute_rhs(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, k4_u, k4_v, k4_w, k4_p, grid, params, nx, ny, nz, stride_z, k_start, k_end, inv_2dz, inv_dz2, @@ -237,6 +246,19 @@ cfd_status_t rk4_impl(flow_field* field, const grid* grid, goto cleanup; } + /* Turbulence transport: advance k-eps/SA with the updated velocity, + * then apply turbulence BCs (including wall functions). nu_t is + * frozen across RK stages and updated once per full step. */ + status = turbulence_step_explicit_with_workspace( + field, grid, params, dt, iter * dt, turb_ws, + turb_on ? TURB_WORKSPACE_SIZE(total) : 0); + if (status == CFD_SUCCESS) { + status = turbulence_apply_bcs(field, grid, params); + } + if (status != CFD_SUCCESS) { + goto cleanup; + } + /* NaN / Inf check */ for (size_t n = 0; n < total; n++) { if (!isfinite(field->u[n]) || !isfinite(field->v[n]) || @@ -253,7 +275,7 @@ cfd_status_t rk4_impl(flow_field* field, const grid* grid, cfd_free(k3_u); cfd_free(k3_v); cfd_free(k3_w); cfd_free(k3_p); cfd_free(k4_u); cfd_free(k4_v); cfd_free(k4_w); cfd_free(k4_p); cfd_free(u0); cfd_free(v0); cfd_free(w0); cfd_free(p0); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return status; } diff --git a/lib/src/solvers/navier_stokes/momentum_rhs/ns_momentum_rhs_avx2.h b/lib/src/solvers/navier_stokes/momentum_rhs/ns_momentum_rhs_avx2.h index 2c573613..c8490dab 100644 --- a/lib/src/solvers/navier_stokes/momentum_rhs/ns_momentum_rhs_avx2.h +++ b/lib/src/solvers/navier_stokes/momentum_rhs/ns_momentum_rhs_avx2.h @@ -52,6 +52,7 @@ static void ns_rhs_point( const double* u, const double* v, const double* w, const double* p, const double* rho, const double* T, + const double* nu_t, double* rhs_u, double* rhs_v, double* rhs_w, double* rhs_p, const grid* g, const ns_solver_params_t* params, size_t idx, size_t il, size_t ir, size_t jd, size_t ju, @@ -97,6 +98,8 @@ static void ns_rhs_point( double nu = params->mu / fmax(rho[idx], 1e-10); nu = fmin(nu, 1.0); + const int turb_on_pt = (params->turb_model != TURB_MODEL_NONE) && (nu_t != NULL); + du_dx = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, du_dx)); du_dy = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, du_dy)); du_dz = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, du_dz)); @@ -129,19 +132,58 @@ static void ns_rhs_point( energy_compute_buoyancy(T[idx], params, &source_u, &source_v, &source_w); } + /* Viscous terms: laminar constant-nu Laplacian, or conservative + * face-averaged effective viscosity div((nu + nu_t) grad u) when + * a turbulence model is active. The z-term keeps the laminar nu + * (turbulence is 2D-only; it vanishes when nz == 1). */ + double visc_u, visc_v, visc_w; + if (!turb_on_pt) { + visc_u = nu * (d2u_dx2 + d2u_dy2 + d2u_dz2); + visc_v = nu * (d2v_dx2 + d2v_dy2 + d2v_dz2); + visc_w = nu * (d2w_dx2 + d2w_dy2 + d2w_dz2); + } else { + /* Face-averaged nu_eff = nu + nu_t (nu_t already bounded by the + * realizability clamp; the flux clamp below caps it) */ + double nu_xp = nu + 0.5 * (nu_t[idx] + nu_t[ir]); + double nu_xm = nu + 0.5 * (nu_t[idx] + nu_t[il]); + double nu_yp = nu + 0.5 * (nu_t[idx] + nu_t[ju]); + double nu_ym = nu + 0.5 * (nu_t[idx] + nu_t[jd]); + double inv_dxi2 = 1.0 / dx2; + double inv_dyj2 = 1.0 / dy2; + + double t_u_x = (nu_xp * (u[ir] - u[idx]) - nu_xm * (u[idx] - u[il])) * inv_dxi2; + double t_u_y = (nu_yp * (u[ju] - u[idx]) - nu_ym * (u[idx] - u[jd])) * inv_dyj2; + double t_v_x = (nu_xp * (v[ir] - v[idx]) - nu_xm * (v[idx] - v[il])) * inv_dxi2; + double t_v_y = (nu_yp * (v[ju] - v[idx]) - nu_ym * (v[idx] - v[jd])) * inv_dyj2; + double t_w_x = (nu_xp * (w[ir] - w[idx]) - nu_xm * (w[idx] - w[il])) * inv_dxi2; + double t_w_y = (nu_yp * (w[ju] - w[idx]) - nu_ym * (w[idx] - w[jd])) * inv_dyj2; + + /* Same safety clamp as the laminar second derivatives */ + t_u_x = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_u_x)); + t_u_y = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_u_y)); + t_v_x = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_v_x)); + t_v_y = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_v_y)); + t_w_x = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_w_x)); + t_w_y = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_w_y)); + + visc_u = t_u_x + t_u_y + nu * d2u_dz2; + visc_v = t_v_x + t_v_y + nu * d2v_dz2; + visc_w = t_w_x + t_w_y + nu * d2w_dz2; + } + rhs_u[idx] = -(u[idx] * du_dx) - (v[idx] * du_dy) - (w[idx] * du_dz) - dp_dx / rho[idx] - + (nu * (d2u_dx2 + d2u_dy2 + d2u_dz2)) + + visc_u + source_u; rhs_v[idx] = -(u[idx] * dv_dx) - (v[idx] * dv_dy) - (w[idx] * dv_dz) - dp_dy / rho[idx] - + (nu * (d2v_dx2 + d2v_dy2 + d2v_dz2)) + + visc_v + source_v; rhs_w[idx] = -(u[idx] * dw_dx) - (v[idx] * dw_dy) - (w[idx] * dw_dz) - dp_dz / rho[idx] - + (nu * (d2w_dx2 + d2w_dy2 + d2w_dz2)) + + visc_w + source_w; double divergence = du_dx + dv_dy + dw_dz; @@ -168,10 +210,12 @@ static inline __m256d avx2_clamp(__m256d x, __m256d lo, __m256d hi) { static void compute_rhs_row( const double* u, const double* v, const double* w, const double* p, const double* rho, const double* T, + const double* nu_t, double* rhs_u, double* rhs_v, double* rhs_w, double* rhs_p, const RHS_CTX_T* ctx, const grid* g, const ns_solver_params_t* params, size_t j, size_t k, int iter, double dt) { + const int turb_on = (params->turb_model != TURB_MODEL_NONE) && (nu_t != NULL); size_t nx = ctx->nx; size_t ny = ctx->ny; size_t nz = ctx->nz; @@ -204,11 +248,30 @@ static void compute_rhs_row( size_t ir = idx + 1; size_t kd = kd_off + j * nx + si; size_t ku = ku_off + j * nx + si; - ns_rhs_point(u, v, w, p, rho, T, rhs_u, rhs_v, rhs_w, rhs_p, g, params, + ns_rhs_point(u, v, w, p, rho, T, nu_t, rhs_u, rhs_v, rhs_w, rhs_p, g, params, idx, il, ir, jd_row + si, ju_row + si, kd, ku, inv_2dz, inv_dz2, si, j, k, nz, iter, dt); } + /* --- AVX2: i = 2 while i+3 <= nx-3 (interior, no wrapping needed) --- + * When turbulence is active, route all interior points through the scalar + * per-point helper to guarantee identical numerics with the scalar/OMP + * backends (cross-backend Linf agreement < 1e-12). */ + if (turb_on) { + for (ptrdiff_t i = 2; i <= (ptrdiff_t)(nx - 2); i++) { + size_t si = (size_t)i; + size_t idx = j_off + si; + size_t il = (si == 1) ? j_off + (nx - 2) : idx - 1; + size_t ir = (si == nx - 2) ? j_off + 1 : idx + 1; + size_t kd = kd_off + j * nx + si; + size_t ku = ku_off + j * nx + si; + ns_rhs_point(u, v, w, p, rho, T, nu_t, rhs_u, rhs_v, rhs_w, rhs_p, g, params, + idx, il, ir, jd_row + si, ju_row + si, kd, ku, + inv_2dz, inv_dz2, si, j, k, nz, iter, dt); + } + return; /* scalar tail already handled above */ + } + /* --- AVX2: i = 2 while i+3 <= nx-3 (interior, no wrapping needed) --- */ { __m256d dy_inv_v = _mm256_set1_pd(ctx->dy_inv[j]); @@ -459,7 +522,7 @@ static void compute_rhs_row( size_t ir = (si == nx - 2) ? j_off + 1 : idx + 1; /* periodic at last point */ size_t kd = kd_off + j * nx + si; size_t ku = ku_off + j * nx + si; - ns_rhs_point(u, v, w, p, rho, T, rhs_u, rhs_v, rhs_w, rhs_p, g, params, + ns_rhs_point(u, v, w, p, rho, T, nu_t, rhs_u, rhs_v, rhs_w, rhs_p, g, params, idx, il, ir, jd_row + si, ju_row + si, kd, ku, inv_2dz, inv_dz2, si, j, k, nz, iter, dt); } @@ -473,6 +536,7 @@ static void compute_rhs_row( static void compute_rhs_avx2( const double* u, const double* v, const double* w, const double* p, const double* rho, const double* T, + const double* nu_t, double* rhs_u, double* rhs_v, double* rhs_w, double* rhs_p, const RHS_CTX_T* ctx, const grid* g, const ns_solver_params_t* params, int iter, double dt) @@ -484,7 +548,7 @@ static void compute_rhs_avx2( #pragma omp parallel for schedule(static) #endif for (j = 1; j < ny_int - 1; j++) { - compute_rhs_row(u, v, w, p, rho, T, rhs_u, rhs_v, rhs_w, rhs_p, + compute_rhs_row(u, v, w, p, rho, T, nu_t, rhs_u, rhs_v, rhs_w, rhs_p, ctx, g, params, (size_t)j, k, iter, dt); } } diff --git a/lib/src/solvers/navier_stokes/momentum_rhs/ns_momentum_rhs_omp.h b/lib/src/solvers/navier_stokes/momentum_rhs/ns_momentum_rhs_omp.h index bc88b10f..09fd787d 100644 --- a/lib/src/solvers/navier_stokes/momentum_rhs/ns_momentum_rhs_omp.h +++ b/lib/src/solvers/navier_stokes/momentum_rhs/ns_momentum_rhs_omp.h @@ -41,6 +41,7 @@ */ static void compute_rhs_omp(const double* u, const double* v, const double* w, const double* p, const double* rho, const double* T, + const double* nu_t, double* rhs_u, double* rhs_v, double* rhs_w, double* rhs_p, const grid* grid, const ns_solver_params_t* params, @@ -48,6 +49,8 @@ static void compute_rhs_omp(const double* u, const double* v, const double* w, size_t stride_z, size_t k_start, size_t k_end, double inv_2dz, double inv_dz2, int iter, double dt) { + /* nu_t is frozen across RK stages (updated once per full step) */ + const int turb_on = (params->turb_model != TURB_MODEL_NONE) && (nu_t != NULL); ptrdiff_t ny_int = (ptrdiff_t)ny; ptrdiff_t nx_int = (ptrdiff_t)nx; @@ -160,22 +163,61 @@ static void compute_rhs_omp(const double* u, const double* v, const double* w, &source_u, &source_v, &source_w); } + /* Viscous terms: laminar constant-nu Laplacian, or conservative + * face-averaged effective viscosity div((nu + nu_t) grad u) when + * a turbulence model is active. The z-term keeps the laminar nu + * (turbulence is 2D-only; it vanishes when nz == 1). */ + double visc_u, visc_v, visc_w; + if (!turb_on) { + visc_u = nu * (d2u_dx2 + d2u_dy2 + d2u_dz2); + visc_v = nu * (d2v_dx2 + d2v_dy2 + d2v_dz2); + visc_w = nu * (d2w_dx2 + d2w_dy2 + d2w_dz2); + } else { + /* Face-averaged nu_eff = nu + nu_t (nu_t already bounded by + * the realizability clamp; the flux clamp below caps it) */ + double nu_xp = nu + 0.5 * (nu_t[idx] + nu_t[ir]); + double nu_xm = nu + 0.5 * (nu_t[idx] + nu_t[il]); + double nu_yp = nu + 0.5 * (nu_t[idx] + nu_t[ju]); + double nu_ym = nu + 0.5 * (nu_t[idx] + nu_t[jd]); + double inv_dxi2 = 1.0 / (grid->dx[i] * grid->dx[i]); + double inv_dyj2 = 1.0 / (grid->dy[j] * grid->dy[j]); + + double t_u_x = (nu_xp * (u[ir] - u[idx]) - nu_xm * (u[idx] - u[il])) * inv_dxi2; + double t_u_y = (nu_yp * (u[ju] - u[idx]) - nu_ym * (u[idx] - u[jd])) * inv_dyj2; + double t_v_x = (nu_xp * (v[ir] - v[idx]) - nu_xm * (v[idx] - v[il])) * inv_dxi2; + double t_v_y = (nu_yp * (v[ju] - v[idx]) - nu_ym * (v[idx] - v[jd])) * inv_dyj2; + double t_w_x = (nu_xp * (w[ir] - w[idx]) - nu_xm * (w[idx] - w[il])) * inv_dxi2; + double t_w_y = (nu_yp * (w[ju] - w[idx]) - nu_ym * (w[idx] - w[jd])) * inv_dyj2; + + /* Same safety clamp as the laminar second derivatives */ + t_u_x = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_u_x)); + t_u_y = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_u_y)); + t_v_x = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_v_x)); + t_v_y = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_v_y)); + t_w_x = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_w_x)); + t_w_y = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_w_y)); + + visc_u = t_u_x + t_u_y + nu * d2u_dz2; + visc_v = t_v_x + t_v_y + nu * d2v_dz2; + visc_w = t_w_x + t_w_y + nu * d2w_dz2; + } + /* RHS for u-momentum */ rhs_u[idx] = -u[idx] * du_dx - v[idx] * du_dy - w[idx] * du_dz - dp_dx / rho[idx] - + nu * (d2u_dx2 + d2u_dy2 + d2u_dz2) + + visc_u + source_u; /* RHS for v-momentum */ rhs_v[idx] = -u[idx] * dv_dx - v[idx] * dv_dy - w[idx] * dv_dz - dp_dy / rho[idx] - + nu * (d2v_dx2 + d2v_dy2 + d2v_dz2) + + visc_v + source_v; /* RHS for w-momentum */ rhs_w[idx] = -u[idx] * dw_dx - v[idx] * dw_dy - w[idx] * dw_dz - dp_dz / rho[idx] - + nu * (d2w_dx2 + d2w_dy2 + d2w_dz2) + + visc_w + source_w; /* Simplified pressure RHS (divergence-based) */ diff --git a/lib/src/solvers/navier_stokes/momentum_rhs/ns_momentum_rhs_scalar.h b/lib/src/solvers/navier_stokes/momentum_rhs/ns_momentum_rhs_scalar.h index 96a66a01..f4671f39 100644 --- a/lib/src/solvers/navier_stokes/momentum_rhs/ns_momentum_rhs_scalar.h +++ b/lib/src/solvers/navier_stokes/momentum_rhs/ns_momentum_rhs_scalar.h @@ -48,12 +48,16 @@ */ static void compute_rhs(const double* u, const double* v, const double* w, const double* p, const double* rho, const double* T, + const double* nu_t, double* rhs_u, double* rhs_v, double* rhs_w, double* rhs_p, const grid* grid, const ns_solver_params_t* params, size_t nx, size_t ny, size_t nz, size_t stride_z, size_t k_start, size_t k_end, double inv_2dz, double inv_dz2, int iter, double dt) { + /* nu_t is frozen across RK stages (updated once per full step) */ + const int turb_on = (params->turb_model != TURB_MODEL_NONE) && (nu_t != NULL); + for (size_t k = k_start; k < k_end; k++) { for (size_t j = 1; j < ny - 1; j++) { for (size_t i = 1; i < nx - 1; i++) { @@ -161,22 +165,61 @@ static void compute_rhs(const double* u, const double* v, const double* w, &source_u, &source_v, &source_w); } + /* Viscous terms: laminar constant-nu Laplacian, or conservative + * face-averaged effective viscosity div((nu + nu_t) grad u) when + * a turbulence model is active. The z-term keeps the laminar nu + * (turbulence is 2D-only; it vanishes when nz == 1). */ + double visc_u, visc_v, visc_w; + if (!turb_on) { + visc_u = nu * (d2u_dx2 + d2u_dy2 + d2u_dz2); + visc_v = nu * (d2v_dx2 + d2v_dy2 + d2v_dz2); + visc_w = nu * (d2w_dx2 + d2w_dy2 + d2w_dz2); + } else { + /* Face-averaged nu_eff = nu + nu_t (nu_t already bounded by + * the realizability clamp; the flux clamp below caps it) */ + double nu_xp = nu + 0.5 * (nu_t[idx] + nu_t[ir]); + double nu_xm = nu + 0.5 * (nu_t[idx] + nu_t[il]); + double nu_yp = nu + 0.5 * (nu_t[idx] + nu_t[ju]); + double nu_ym = nu + 0.5 * (nu_t[idx] + nu_t[jd]); + double inv_dxi2 = 1.0 / (grid->dx[i] * grid->dx[i]); + double inv_dyj2 = 1.0 / (grid->dy[j] * grid->dy[j]); + + double t_u_x = (nu_xp * (u[ir] - u[idx]) - nu_xm * (u[idx] - u[il])) * inv_dxi2; + double t_u_y = (nu_yp * (u[ju] - u[idx]) - nu_ym * (u[idx] - u[jd])) * inv_dyj2; + double t_v_x = (nu_xp * (v[ir] - v[idx]) - nu_xm * (v[idx] - v[il])) * inv_dxi2; + double t_v_y = (nu_yp * (v[ju] - v[idx]) - nu_ym * (v[idx] - v[jd])) * inv_dyj2; + double t_w_x = (nu_xp * (w[ir] - w[idx]) - nu_xm * (w[idx] - w[il])) * inv_dxi2; + double t_w_y = (nu_yp * (w[ju] - w[idx]) - nu_ym * (w[idx] - w[jd])) * inv_dyj2; + + /* Same safety clamp as the laminar second derivatives */ + t_u_x = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_u_x)); + t_u_y = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_u_y)); + t_v_x = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_v_x)); + t_v_y = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_v_y)); + t_w_x = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_w_x)); + t_w_y = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, t_w_y)); + + visc_u = t_u_x + t_u_y + nu * d2u_dz2; + visc_v = t_v_x + t_v_y + nu * d2v_dz2; + visc_w = t_w_x + t_w_y + nu * d2w_dz2; + } + /* RHS for u-momentum */ rhs_u[idx] = -u[idx] * du_dx - v[idx] * du_dy - w[idx] * du_dz - dp_dx / rho[idx] - + nu * (d2u_dx2 + d2u_dy2 + d2u_dz2) + + visc_u + source_u; /* RHS for v-momentum */ rhs_v[idx] = -u[idx] * dv_dx - v[idx] * dv_dy - w[idx] * dv_dz - dp_dy / rho[idx] - + nu * (d2v_dx2 + d2v_dy2 + d2v_dz2) + + visc_v + source_v; /* RHS for w-momentum */ rhs_w[idx] = -u[idx] * dw_dx - v[idx] * dw_dy - w[idx] * dw_dz - dp_dz / rho[idx] - + nu * (d2w_dx2 + d2w_dy2 + d2w_dz2) + + visc_w + source_w; /* Simplified pressure RHS (divergence-based) */ diff --git a/lib/src/solvers/navier_stokes/omp/solver_explicit_euler_omp.c b/lib/src/solvers/navier_stokes/omp/solver_explicit_euler_omp.c index 6f3ddd39..4b2a8fa3 100644 --- a/lib/src/solvers/navier_stokes/omp/solver_explicit_euler_omp.c +++ b/lib/src/solvers/navier_stokes/omp/solver_explicit_euler_omp.c @@ -3,9 +3,11 @@ #include "cfd/core/indexing.h" #include "cfd/core/memory.h" -#include "cfd/solvers/navier_stokes_solver.h" #include "cfd/solvers/energy_solver.h" +#include "cfd/solvers/navier_stokes_solver.h" +#include "cfd/solvers/turbulence_solver.h" #include "../../energy/energy_solver_internal.h" +#include "../../turbulence/turbulence_solver_internal.h" #include "../boundary_copy_utils.h" #include #include @@ -62,14 +64,18 @@ cfd_status_t explicit_euler_omp_impl(flow_field* field, const grid* grid, int needs_T_ws = (params->alpha > 0.0 || params->beta != 0.0); double* T_energy_ws = needs_T_ws ? (double*)cfd_calloc(total, sizeof(double)) : NULL; + const int turb_on = (params->turb_model != TURB_MODEL_NONE); + double* turb_ws = turb_on + ? (double*)cfd_calloc(TURB_WORKSPACE_SIZE(total), sizeof(double)) : NULL; if (!u_new || !v_new || !w_new || !p_new || - (needs_T_ws && !T_energy_ws)) { + (needs_T_ws && !T_energy_ws) || (turb_on && !turb_ws)) { cfd_free(u_new); cfd_free(v_new); cfd_free(w_new); cfd_free(p_new); cfd_free(T_energy_ws); + cfd_free(turb_ws); return CFD_ERROR_NOMEM; } @@ -153,23 +159,59 @@ cfd_status_t explicit_euler_omp_impl(flow_field* field, const grid* grid, energy_compute_buoyancy(field->T[idx], params, &source_u, &source_v, &source_w); + // Viscous terms: laminar or turbulent (face-averaged nu_eff) + double visc_u, visc_v, visc_w; + if (!turb_on) { + visc_u = nu * (d2u_dx2 + d2u_dy2 + d2u_dz2); + visc_v = nu * (d2v_dx2 + d2v_dy2 + d2v_dz2); + visc_w = nu * (d2w_dx2 + d2w_dy2 + d2w_dz2); + } else { + const double* nu_t = field->nu_t; + double dx2 = grid->dx[i] * grid->dx[i]; + double dy2 = grid->dy[j] * grid->dy[j]; + /* Face-averaged nu_eff = nu + nu_t (nu_t is bounded by the + * realizability clamp) */ + double nu_xp = nu + 0.5 * (nu_t[idx] + nu_t[idx + 1]); + double nu_xm = nu + 0.5 * (nu_t[idx] + nu_t[idx - 1]); + double nu_yp = nu + 0.5 * (nu_t[idx] + nu_t[idx + nx]); + double nu_ym = nu + 0.5 * (nu_t[idx] + nu_t[idx - nx]); + visc_u = (nu_xp * (field->u[idx + 1] - field->u[idx]) - + nu_xm * (field->u[idx] - field->u[idx - 1])) / dx2 + + (nu_yp * (field->u[idx + nx] - field->u[idx]) - + nu_ym * (field->u[idx] - field->u[idx - nx])) / dy2 + + nu * d2u_dz2; + visc_v = (nu_xp * (field->v[idx + 1] - field->v[idx]) - + nu_xm * (field->v[idx] - field->v[idx - 1])) / dx2 + + (nu_yp * (field->v[idx + nx] - field->v[idx]) - + nu_ym * (field->v[idx] - field->v[idx - nx])) / dy2 + + nu * d2v_dz2; + visc_w = (nu_xp * (field->w[idx + 1] - field->w[idx]) - + nu_xm * (field->w[idx] - field->w[idx - 1])) / dx2 + + (nu_yp * (field->w[idx + nx] - field->w[idx]) - + nu_ym * (field->w[idx] - field->w[idx - nx])) / dy2 + + nu * d2w_dz2; + visc_u = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, visc_u)); + visc_v = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, visc_v)); + visc_w = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, visc_w)); + } + // Update u double du = conservative_dt * (-field->u[idx] * du_dx - field->v[idx] * du_dy - field->w[idx] * du_dz - dp_dx / fmax(field->rho[idx], 1e-10) - + nu * (d2u_dx2 + d2u_dy2 + d2u_dz2) + source_u); + + visc_u + source_u); // Update v double dv = conservative_dt * (-field->u[idx] * dv_dx - field->v[idx] * dv_dy - field->w[idx] * dv_dz - dp_dy / fmax(field->rho[idx], 1e-10) - + nu * (d2v_dx2 + d2v_dy2 + d2v_dz2) + source_v); + + visc_v + source_v); // Update w double dw = conservative_dt * (-field->u[idx] * dw_dx - field->v[idx] * dw_dy - field->w[idx] * dw_dz - dp_dz / fmax(field->rho[idx], 1e-10) - + nu * (d2w_dx2 + d2w_dy2 + d2w_dz2) + source_w); + + visc_w + source_w); du = fmax(-UPDATE_LIMIT, fmin(UPDATE_LIMIT, du)); dv = fmax(-UPDATE_LIMIT, fmin(UPDATE_LIMIT, dv)); @@ -223,10 +265,27 @@ cfd_status_t explicit_euler_omp_impl(flow_field* field, const grid* grid, cfd_status_t bc_status = energy_apply_thermal_bcs(field, params); if (bc_status != CFD_SUCCESS) { cfd_free(u_new); cfd_free(v_new); cfd_free(w_new); - cfd_free(p_new); cfd_free(T_energy_ws); + cfd_free(p_new); cfd_free(T_energy_ws); cfd_free(turb_ws); return bc_status; } + /* Turbulence transport: advance k-eps/SA with the updated velocity, + * then apply turbulence BCs (including wall functions). nu_t is + * frozen across the explicit Euler step and updated once per step. */ + { + cfd_status_t turb_status = turbulence_step_explicit_omp_with_workspace( + field, grid, params, conservative_dt, iter * conservative_dt, + turb_ws, turb_on ? TURB_WORKSPACE_SIZE(total) : 0); + if (turb_status == CFD_SUCCESS) { + turb_status = turbulence_apply_bcs(field, grid, params); + } + if (turb_status != CFD_SUCCESS) { + cfd_free(u_new); cfd_free(v_new); cfd_free(w_new); + cfd_free(p_new); cfd_free(T_energy_ws); cfd_free(turb_ws); + return turb_status; + } + } + // Check for NaN/Inf values int has_nan = 0; ptrdiff_t total_int = (ptrdiff_t)total; @@ -245,6 +304,7 @@ cfd_status_t explicit_euler_omp_impl(flow_field* field, const grid* grid, cfd_free(w_new); cfd_free(p_new); cfd_free(T_energy_ws); + cfd_free(turb_ws); cfd_set_error(CFD_ERROR_DIVERGED, "NaN/Inf detected in explicit_euler_omp step"); return CFD_ERROR_DIVERGED; @@ -256,6 +316,7 @@ cfd_status_t explicit_euler_omp_impl(flow_field* field, const grid* grid, cfd_free(w_new); cfd_free(p_new); cfd_free(T_energy_ws); + cfd_free(turb_ws); return CFD_SUCCESS; } diff --git a/lib/src/solvers/navier_stokes/omp/solver_projection_omp.c b/lib/src/solvers/navier_stokes/omp/solver_projection_omp.c index b345e85e..401202cb 100644 --- a/lib/src/solvers/navier_stokes/omp/solver_projection_omp.c +++ b/lib/src/solvers/navier_stokes/omp/solver_projection_omp.c @@ -3,10 +3,12 @@ #include "cfd/core/grid.h" #include "cfd/core/indexing.h" #include "cfd/core/memory.h" +#include "cfd/solvers/energy_solver.h" #include "cfd/solvers/navier_stokes_solver.h" #include "cfd/solvers/poisson_solver.h" -#include "cfd/solvers/energy_solver.h" +#include "cfd/solvers/turbulence_solver.h" #include "../../energy/energy_solver_internal.h" +#include "../../turbulence/turbulence_solver_internal.h" #include "../boundary_copy_utils.h" @@ -70,9 +72,12 @@ cfd_status_t solve_projection_method_omp(flow_field* field, const grid* grid, int needs_T_ws = (params->alpha > 0.0 || params->beta != 0.0); double* T_energy_ws = needs_T_ws ? (double*)cfd_calloc(total, sizeof(double)) : NULL; + const int turb_on = (params->turb_model != TURB_MODEL_NONE); + double* turb_ws = turb_on + ? (double*)cfd_calloc(TURB_WORKSPACE_SIZE(total), sizeof(double)) : NULL; if (!u_star || !v_star || !w_star || !p_new || !p_temp || !rhs || - (needs_T_ws && !T_energy_ws)) { + (needs_T_ws && !T_energy_ws) || (turb_on && !turb_ws)) { cfd_free(u_star); cfd_free(v_star); cfd_free(w_star); @@ -80,6 +85,7 @@ cfd_status_t solve_projection_method_omp(flow_field* field, const grid* grid, cfd_free(p_temp); cfd_free(rhs); cfd_free(T_energy_ws); + cfd_free(turb_ws); return CFD_ERROR_NOMEM; } @@ -125,9 +131,37 @@ cfd_status_t solve_projection_method_omp(flow_field* field, const grid* grid, double d2w_dy2 = (field->w[idx + nx] - 2.0 * w + field->w[idx - nx]) / (dy * dy); double d2w_dz2 = (field->w[idx + stride_z] - 2.0 * w + field->w[idx - stride_z]) * inv_dz2; - double visc_u = nu * (d2u_dx2 + d2u_dy2 + d2u_dz2); - double visc_v = nu * (d2v_dx2 + d2v_dy2 + d2v_dz2); - double visc_w = nu * (d2w_dx2 + d2w_dy2 + d2w_dz2); + double visc_u, visc_v, visc_w; + if (!turb_on) { + visc_u = nu * (d2u_dx2 + d2u_dy2 + d2u_dz2); + visc_v = nu * (d2v_dx2 + d2v_dy2 + d2v_dz2); + visc_w = nu * (d2w_dx2 + d2w_dy2 + d2w_dz2); + } else { + /* Turbulent: conservative face-averaged effective viscosity + * div((nu + nu_t) grad u). The z-term keeps the laminar nu + * (turbulence is 2D-only; it vanishes when nz == 1). */ + const double* nu_t = field->nu_t; + double nu_xp = nu + 0.5 * (nu_t[idx] + nu_t[idx + 1]); + double nu_xm = nu + 0.5 * (nu_t[idx] + nu_t[idx - 1]); + double nu_yp = nu + 0.5 * (nu_t[idx] + nu_t[idx + nx]); + double nu_ym = nu + 0.5 * (nu_t[idx] + nu_t[idx - nx]); + + visc_u = (nu_xp * (field->u[idx + 1] - u) - + nu_xm * (u - field->u[idx - 1])) / (dx * dx) + + (nu_yp * (field->u[idx + nx] - u) - + nu_ym * (u - field->u[idx - nx])) / (dy * dy) + + nu * d2u_dz2; + visc_v = (nu_xp * (field->v[idx + 1] - v) - + nu_xm * (v - field->v[idx - 1])) / (dx * dx) + + (nu_yp * (field->v[idx + nx] - v) - + nu_ym * (v - field->v[idx - nx])) / (dy * dy) + + nu * d2v_dz2; + visc_w = (nu_xp * (field->w[idx + 1] - w) - + nu_xm * (w - field->w[idx - 1])) / (dx * dx) + + (nu_yp * (field->w[idx + nx] - w) - + nu_ym * (w - field->w[idx - nx])) / (dy * dy) + + nu * d2w_dz2; + } double source_u = 0.0; double source_v = 0.0; @@ -192,6 +226,7 @@ cfd_status_t solve_projection_method_omp(flow_field* field, const grid* grid, cfd_free(p_temp); cfd_free(rhs); cfd_free(T_energy_ws); + cfd_free(turb_ws); return CFD_ERROR_MAX_ITER; } @@ -227,7 +262,7 @@ cfd_status_t solve_projection_method_omp(flow_field* field, const grid* grid, if (energy_status != CFD_SUCCESS) { cfd_free(u_star); cfd_free(v_star); cfd_free(w_star); cfd_free(p_new); cfd_free(p_temp); cfd_free(rhs); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return energy_status; } } @@ -237,7 +272,7 @@ cfd_status_t solve_projection_method_omp(flow_field* field, const grid* grid, if (bc_status != CFD_SUCCESS) { cfd_free(u_star); cfd_free(v_star); cfd_free(w_star); cfd_free(p_new); cfd_free(p_temp); cfd_free(rhs); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return bc_status; } @@ -245,6 +280,23 @@ cfd_status_t solve_projection_method_omp(flow_field* field, const grid* grid, copy_boundary_velocities_3d(field->u, field->v, field->w, u_star, v_star, w_star, nx, ny, nz); + /* Turbulence transport: advance k-eps/SA with the corrected velocity, + * then apply turbulence BCs (including wall functions) */ + { + cfd_status_t turb_status = turbulence_step_explicit_omp_with_workspace( + field, grid, params, dt, iter * dt, turb_ws, + turb_on ? TURB_WORKSPACE_SIZE(total) : 0); + if (turb_status == CFD_SUCCESS) { + turb_status = turbulence_apply_bcs(field, grid, params); + } + if (turb_status != CFD_SUCCESS) { + cfd_free(u_star); cfd_free(v_star); cfd_free(w_star); + cfd_free(p_new); cfd_free(p_temp); cfd_free(rhs); + cfd_free(T_energy_ws); cfd_free(turb_ws); + return turb_status; + } + } + /* Check for NaN/Inf values (parallelized) */ int has_nan = 0; ptrdiff_t total_int = (ptrdiff_t)total; @@ -264,6 +316,7 @@ cfd_status_t solve_projection_method_omp(flow_field* field, const grid* grid, cfd_free(p_temp); cfd_free(rhs); cfd_free(T_energy_ws); + cfd_free(turb_ws); return CFD_ERROR_DIVERGED; } } @@ -275,5 +328,6 @@ cfd_status_t solve_projection_method_omp(flow_field* field, const grid* grid, cfd_free(p_temp); cfd_free(rhs); cfd_free(T_energy_ws); + cfd_free(turb_ws); return CFD_SUCCESS; } diff --git a/lib/src/solvers/navier_stokes/omp/solver_rk2_omp.c b/lib/src/solvers/navier_stokes/omp/solver_rk2_omp.c index 4871e8d9..2286a674 100644 --- a/lib/src/solvers/navier_stokes/omp/solver_rk2_omp.c +++ b/lib/src/solvers/navier_stokes/omp/solver_rk2_omp.c @@ -13,9 +13,11 @@ #include "cfd/core/grid.h" #include "cfd/core/indexing.h" #include "cfd/core/memory.h" -#include "cfd/solvers/navier_stokes_solver.h" #include "cfd/solvers/energy_solver.h" +#include "cfd/solvers/navier_stokes_solver.h" +#include "cfd/solvers/turbulence_solver.h" #include "../../energy/energy_solver_internal.h" +#include "../../turbulence/turbulence_solver_internal.h" #include #include @@ -100,15 +102,18 @@ cfd_status_t rk2_omp_impl(flow_field* field, const grid* grid, int needs_T_ws = (params->alpha > 0.0 || params->beta != 0.0); double* T_energy_ws = needs_T_ws ? (double*)cfd_calloc(total, sizeof(double)) : NULL; + const int turb_on = (params->turb_model != TURB_MODEL_NONE); + double* turb_ws = turb_on + ? (double*)cfd_calloc(TURB_WORKSPACE_SIZE(total), sizeof(double)) : NULL; if (!k1_u || !k1_v || !k1_w || !k1_p || !k2_u || !k2_v || !k2_w || !k2_p || !u0 || !v0 || !w0 || !p0 || - (needs_T_ws && !T_energy_ws)) { + (needs_T_ws && !T_energy_ws) || (turb_on && !turb_ws)) { cfd_free(k1_u); cfd_free(k1_v); cfd_free(k1_w); cfd_free(k1_p); cfd_free(k2_u); cfd_free(k2_v); cfd_free(k2_w); cfd_free(k2_p); cfd_free(u0); cfd_free(v0); cfd_free(w0); cfd_free(p0); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return CFD_ERROR_NOMEM; } @@ -130,6 +135,7 @@ cfd_status_t rk2_omp_impl(flow_field* field, const grid* grid, memset(k1_p, 0, bytes); compute_rhs_omp(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, k1_u, k1_v, k1_w, k1_p, grid, params, nx, ny, nz, stride_z, k_start, k_end, inv_2dz, inv_dz2, @@ -163,6 +169,7 @@ cfd_status_t rk2_omp_impl(flow_field* field, const grid* grid, memset(k2_p, 0, bytes); compute_rhs_omp(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, k2_u, k2_v, k2_w, k2_p, grid, params, nx, ny, nz, stride_z, k_start, k_end, inv_2dz, inv_dz2, @@ -203,6 +210,19 @@ cfd_status_t rk2_omp_impl(flow_field* field, const grid* grid, goto cleanup; } + /* Turbulence transport: advance k-eps/SA with the updated velocity, + * then apply turbulence BCs (including wall functions). nu_t is + * frozen across RK stages and updated once per full step. */ + status = turbulence_step_explicit_omp_with_workspace( + field, grid, params, dt, iter * dt, turb_ws, + turb_on ? TURB_WORKSPACE_SIZE(total) : 0); + if (status == CFD_SUCCESS) { + status = turbulence_apply_bcs(field, grid, params); + } + if (status != CFD_SUCCESS) { + goto cleanup; + } + /* NaN / Inf check (parallelized) */ { int has_nan = 0; @@ -225,7 +245,7 @@ cfd_status_t rk2_omp_impl(flow_field* field, const grid* grid, cfd_free(k1_u); cfd_free(k1_v); cfd_free(k1_w); cfd_free(k1_p); cfd_free(k2_u); cfd_free(k2_v); cfd_free(k2_w); cfd_free(k2_p); cfd_free(u0); cfd_free(v0); cfd_free(w0); cfd_free(p0); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return status; } diff --git a/lib/src/solvers/navier_stokes/omp/solver_rk4_omp.c b/lib/src/solvers/navier_stokes/omp/solver_rk4_omp.c index c1cd0fab..904e6dd3 100644 --- a/lib/src/solvers/navier_stokes/omp/solver_rk4_omp.c +++ b/lib/src/solvers/navier_stokes/omp/solver_rk4_omp.c @@ -19,9 +19,11 @@ #include "cfd/core/grid.h" #include "cfd/core/indexing.h" #include "cfd/core/memory.h" -#include "cfd/solvers/navier_stokes_solver.h" #include "cfd/solvers/energy_solver.h" +#include "cfd/solvers/navier_stokes_solver.h" +#include "cfd/solvers/turbulence_solver.h" #include "../../energy/energy_solver_internal.h" +#include "../../turbulence/turbulence_solver_internal.h" #include #include @@ -132,19 +134,22 @@ cfd_status_t rk4_omp_impl(flow_field* field, const grid* grid, int needs_T_ws = (params->alpha > 0.0 || params->beta != 0.0); double* T_energy_ws = needs_T_ws ? (double*)cfd_calloc(total, sizeof(double)) : NULL; + const int turb_on = (params->turb_model != TURB_MODEL_NONE); + double* turb_ws = turb_on + ? (double*)cfd_calloc(TURB_WORKSPACE_SIZE(total), sizeof(double)) : NULL; if (!k1_u || !k1_v || !k1_w || !k1_p || !k2_u || !k2_v || !k2_w || !k2_p || !k3_u || !k3_v || !k3_w || !k3_p || !k4_u || !k4_v || !k4_w || !k4_p || !u0 || !v0 || !w0 || !p0 || - (needs_T_ws && !T_energy_ws)) { + (needs_T_ws && !T_energy_ws) || (turb_on && !turb_ws)) { cfd_free(k1_u); cfd_free(k1_v); cfd_free(k1_w); cfd_free(k1_p); cfd_free(k2_u); cfd_free(k2_v); cfd_free(k2_w); cfd_free(k2_p); cfd_free(k3_u); cfd_free(k3_v); cfd_free(k3_w); cfd_free(k3_p); cfd_free(k4_u); cfd_free(k4_v); cfd_free(k4_w); cfd_free(k4_p); cfd_free(u0); cfd_free(v0); cfd_free(w0); cfd_free(p0); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return CFD_ERROR_NOMEM; } @@ -165,6 +170,7 @@ cfd_status_t rk4_omp_impl(flow_field* field, const grid* grid, memset(k1_w, 0, bytes); memset(k1_p, 0, bytes); compute_rhs_omp(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, k1_u, k1_v, k1_w, k1_p, grid, params, nx, ny, nz, stride_z, k_start, k_end, inv_2dz, inv_dz2, @@ -178,6 +184,7 @@ cfd_status_t rk4_omp_impl(flow_field* field, const grid* grid, memset(k2_w, 0, bytes); memset(k2_p, 0, bytes); compute_rhs_omp(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, k2_u, k2_v, k2_w, k2_p, grid, params, nx, ny, nz, stride_z, k_start, k_end, inv_2dz, inv_dz2, @@ -191,6 +198,7 @@ cfd_status_t rk4_omp_impl(flow_field* field, const grid* grid, memset(k3_w, 0, bytes); memset(k3_p, 0, bytes); compute_rhs_omp(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, k3_u, k3_v, k3_w, k3_p, grid, params, nx, ny, nz, stride_z, k_start, k_end, inv_2dz, inv_dz2, @@ -204,6 +212,7 @@ cfd_status_t rk4_omp_impl(flow_field* field, const grid* grid, memset(k4_w, 0, bytes); memset(k4_p, 0, bytes); compute_rhs_omp(field->u, field->v, field->w, field->p, field->rho, field->T, + field->nu_t, k4_u, k4_v, k4_w, k4_p, grid, params, nx, ny, nz, stride_z, k_start, k_end, inv_2dz, inv_dz2, @@ -249,6 +258,19 @@ cfd_status_t rk4_omp_impl(flow_field* field, const grid* grid, goto cleanup; } + /* Turbulence transport: advance k-eps/SA with the updated velocity, + * then apply turbulence BCs (including wall functions). nu_t is + * frozen across RK stages and updated once per full step. */ + status = turbulence_step_explicit_omp_with_workspace( + field, grid, params, dt, iter * dt, turb_ws, + turb_on ? TURB_WORKSPACE_SIZE(total) : 0); + if (status == CFD_SUCCESS) { + status = turbulence_apply_bcs(field, grid, params); + } + if (status != CFD_SUCCESS) { + goto cleanup; + } + /* NaN / Inf check (parallelized) */ { int has_nan = 0; @@ -273,7 +295,7 @@ cfd_status_t rk4_omp_impl(flow_field* field, const grid* grid, cfd_free(k3_u); cfd_free(k3_v); cfd_free(k3_w); cfd_free(k3_p); cfd_free(k4_u); cfd_free(k4_v); cfd_free(k4_w); cfd_free(k4_p); cfd_free(u0); cfd_free(v0); cfd_free(w0); cfd_free(p0); - cfd_free(T_energy_ws); + cfd_free(T_energy_ws); cfd_free(turb_ws); return status; } diff --git a/lib/src/solvers/turbulence/avx2/turbulence_solver_avx2.c b/lib/src/solvers/turbulence/avx2/turbulence_solver_avx2.c new file mode 100644 index 00000000..b9e4dc58 --- /dev/null +++ b/lib/src/solvers/turbulence/avx2/turbulence_solver_avx2.c @@ -0,0 +1,449 @@ +/** + * @file turbulence_solver_avx2.c + * @brief AVX2-vectorized RANS turbulence transport step + * + * Same numerics as the scalar reference (turbulence/cpu/). The k-epsilon + * interior stencil is vectorized along the unit-stride i direction (4 doubles + * per AVX2 register, upwind selection via blendv) with a scalar remainder + * tail; the j loop is parallelized with OpenMP. The Spalart-Allmaras kernel is + * dominated by pow()-heavy closure functions (fw, fv1) that do not vectorize + * profitably without a vector libm, so it is processed with OMP-parallel + * scalar rows — numerically identical to the scalar backend by construction. + * When AVX2 is unavailable at compile time the whole interior is processed + * with the scalar path, so this file always provides a correct implementation. + */ + +#include "../turbulence_solver_internal.h" + +#include "cfd/core/indexing.h" +#include "cfd/core/memory.h" + +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +#if defined(CFD_HAS_AVX2) +#include +#define USE_AVX 1 +#else +#define USE_AVX 0 +#endif + +/* Scalar k-epsilon update for one point (vector-loop tail and non-AVX builds). + * Must remain numerically identical to turb_kepsilon_step_scalar. */ +static inline void kepsilon_point(const flow_field* field, + const ns_solver_params_t* params, + size_t idx, size_t nx, double dt, + double inv_dx, double inv_dy, + double inv_2dx, double inv_2dy, + double inv_dx2, double inv_dy2, + double* k_new, double* eps_new) { + const double* u = field->u; + const double* v = field->v; + const double* k = field->turb_k; + const double* eps = field->turb_eps; + const double* nu_t = field->nu_t; + + double nu = params->mu / fmax(field->rho[idx], 1e-10); + double u_c = u[idx]; + double v_c = v[idx]; + double k_c = fmax(k[idx], TURB_K_MIN); + double eps_c = fmax(eps[idx], TURB_EPS_MIN); + + double adv_k = (u_c > 0.0 ? u_c * (k[idx] - k[idx - 1]) * inv_dx + : u_c * (k[idx + 1] - k[idx]) * inv_dx) + + (v_c > 0.0 ? v_c * (k[idx] - k[idx - nx]) * inv_dy + : v_c * (k[idx + nx] - k[idx]) * inv_dy); + double adv_e = (u_c > 0.0 ? u_c * (eps[idx] - eps[idx - 1]) * inv_dx + : u_c * (eps[idx + 1] - eps[idx]) * inv_dx) + + (v_c > 0.0 ? v_c * (eps[idx] - eps[idx - nx]) * inv_dy + : v_c * (eps[idx + nx] - eps[idx]) * inv_dy); + + double gam_e_k = nu + 0.5 * (nu_t[idx] + nu_t[idx + 1]) / TURB_SIGMA_K; + double gam_w_k = nu + 0.5 * (nu_t[idx] + nu_t[idx - 1]) / TURB_SIGMA_K; + double gam_n_k = nu + 0.5 * (nu_t[idx] + nu_t[idx + nx]) / TURB_SIGMA_K; + double gam_s_k = nu + 0.5 * (nu_t[idx] + nu_t[idx - nx]) / TURB_SIGMA_K; + double diff_k = (gam_e_k * (k[idx + 1] - k[idx]) - + gam_w_k * (k[idx] - k[idx - 1])) * inv_dx2 + + (gam_n_k * (k[idx + nx] - k[idx]) - + gam_s_k * (k[idx] - k[idx - nx])) * inv_dy2; + + double gam_e_e = nu + 0.5 * (nu_t[idx] + nu_t[idx + 1]) / TURB_SIGMA_EPS; + double gam_w_e = nu + 0.5 * (nu_t[idx] + nu_t[idx - 1]) / TURB_SIGMA_EPS; + double gam_n_e = nu + 0.5 * (nu_t[idx] + nu_t[idx + nx]) / TURB_SIGMA_EPS; + double gam_s_e = nu + 0.5 * (nu_t[idx] + nu_t[idx - nx]) / TURB_SIGMA_EPS; + double diff_e = (gam_e_e * (eps[idx + 1] - eps[idx]) - + gam_w_e * (eps[idx] - eps[idx - 1])) * inv_dx2 + + (gam_n_e * (eps[idx + nx] - eps[idx]) - + gam_s_e * (eps[idx] - eps[idx - nx])) * inv_dy2; + + double du_dx = (u[idx + 1] - u[idx - 1]) * inv_2dx; + double du_dy = (u[idx + nx] - u[idx - nx]) * inv_2dy; + double dv_dx = (v[idx + 1] - v[idx - 1]) * inv_2dx; + double dv_dy = (v[idx + nx] - v[idx - nx]) * inv_2dy; + double S2 = 2.0 * du_dx * du_dx + 2.0 * dv_dy * dv_dy + + (du_dy + dv_dx) * (du_dy + dv_dx); + double P_k = fmin(nu_t[idx] * S2, TURB_PROD_LIMIT_FACTOR * eps_c); + + double eps_over_k = eps_c / k_c; + double k_upd = (k[idx] + dt * (-adv_k + diff_k + P_k)) / + (1.0 + dt * eps_over_k); + double e_upd = (eps[idx] + dt * (-adv_e + diff_e + + TURB_C1_EPS * eps_over_k * P_k)) / + (1.0 + dt * TURB_C2_EPS * eps_over_k); + + k_new[idx] = fmax(k_upd, TURB_K_MIN); + eps_new[idx] = fmax(e_upd, TURB_EPS_MIN); +} + +#if USE_AVX +/* Vector helpers: upwind advection term c * d(phi) using blendv selection */ +static inline __m256d avx2_upwind(__m256d c, __m256d phi_m, __m256d phi_c, + __m256d phi_p, __m256d inv_d) { + __m256d zero = _mm256_setzero_pd(); + __m256d pos = _mm256_mul_pd(_mm256_mul_pd(c, _mm256_sub_pd(phi_c, phi_m)), inv_d); + __m256d neg = _mm256_mul_pd(_mm256_mul_pd(c, _mm256_sub_pd(phi_p, phi_c)), inv_d); + __m256d mask = _mm256_cmp_pd(c, zero, _CMP_GT_OQ); + return _mm256_blendv_pd(neg, pos, mask); +} + +/* Face-averaged diffusion coefficient nu + 0.5*(a+b)/sigma */ +static inline __m256d avx2_gamma(__m256d nu, __m256d a, __m256d b, __m256d sigma) { + __m256d half = _mm256_set1_pd(0.5); + return _mm256_add_pd(nu, _mm256_div_pd( + _mm256_mul_pd(half, _mm256_add_pd(a, b)), sigma)); +} +#endif + +/* AVX2 k-epsilon transport kernel: i-vectorized with scalar tail, OMP over j */ +static void turb_kepsilon_step_avx2(const flow_field* field, const grid* grid, + const ns_solver_params_t* params, double dt, + double* k_new, double* eps_new) { + size_t nx = field->nx; + size_t ny = field->ny; + + const double dx = grid->dx[0]; + const double dy = grid->dy[0]; + const double inv_dx = 1.0 / dx; + const double inv_dy = 1.0 / dy; + const double inv_2dx = 0.5 * inv_dx; + const double inv_2dy = 0.5 * inv_dy; + const double inv_dx2 = inv_dx * inv_dx; + const double inv_dy2 = inv_dy * inv_dy; + +#if USE_AVX + const double* u = field->u; + const double* v = field->v; + const double* k = field->turb_k; + const double* eps = field->turb_eps; + const double* nu_t = field->nu_t; + const double* rho = field->rho; + + const __m256d v_mu = _mm256_set1_pd(params->mu); + const __m256d v_rho_min = _mm256_set1_pd(1e-10); + const __m256d v_k_min = _mm256_set1_pd(TURB_K_MIN); + const __m256d v_eps_min = _mm256_set1_pd(TURB_EPS_MIN); + const __m256d v_prod_lim = _mm256_set1_pd(TURB_PROD_LIMIT_FACTOR); + const __m256d v_sigma_k = _mm256_set1_pd(TURB_SIGMA_K); + const __m256d v_sigma_e = _mm256_set1_pd(TURB_SIGMA_EPS); + const __m256d v_c1 = _mm256_set1_pd(TURB_C1_EPS); + const __m256d v_c2 = _mm256_set1_pd(TURB_C2_EPS); + const __m256d v_dt = _mm256_set1_pd(dt); + const __m256d v_one = _mm256_set1_pd(1.0); + const __m256d v_two = _mm256_set1_pd(2.0); + const __m256d v_inv_dx = _mm256_set1_pd(inv_dx); + const __m256d v_inv_dy = _mm256_set1_pd(inv_dy); + const __m256d v_inv_2dx = _mm256_set1_pd(inv_2dx); + const __m256d v_inv_2dy = _mm256_set1_pd(inv_2dy); + const __m256d v_inv_dx2 = _mm256_set1_pd(inv_dx2); + const __m256d v_inv_dy2 = _mm256_set1_pd(inv_dy2); + + int j; +#ifdef _OPENMP +#pragma omp parallel for schedule(static) +#endif + for (j = 1; j < (int)ny - 1; j++) { + size_t row = (size_t)j * nx; + size_t i = 1; + for (; i + 4 <= nx - 1; i += 4) { + size_t idx = row + i; + + __m256d v_u = _mm256_loadu_pd(&u[idx]); + __m256d v_v = _mm256_loadu_pd(&v[idx]); + __m256d k_c0 = _mm256_loadu_pd(&k[idx]); + __m256d k_w = _mm256_loadu_pd(&k[idx - 1]); + __m256d k_e = _mm256_loadu_pd(&k[idx + 1]); + __m256d k_s = _mm256_loadu_pd(&k[idx - nx]); + __m256d k_n = _mm256_loadu_pd(&k[idx + nx]); + __m256d e_c0 = _mm256_loadu_pd(&eps[idx]); + __m256d e_w = _mm256_loadu_pd(&eps[idx - 1]); + __m256d e_e = _mm256_loadu_pd(&eps[idx + 1]); + __m256d e_s = _mm256_loadu_pd(&eps[idx - nx]); + __m256d e_n = _mm256_loadu_pd(&eps[idx + nx]); + __m256d nt_c = _mm256_loadu_pd(&nu_t[idx]); + __m256d nt_w = _mm256_loadu_pd(&nu_t[idx - 1]); + __m256d nt_e = _mm256_loadu_pd(&nu_t[idx + 1]); + __m256d nt_s = _mm256_loadu_pd(&nu_t[idx - nx]); + __m256d nt_n = _mm256_loadu_pd(&nu_t[idx + nx]); + + __m256d v_nu = _mm256_div_pd(v_mu, + _mm256_max_pd(_mm256_loadu_pd(&rho[idx]), v_rho_min)); + __m256d k_fl = _mm256_max_pd(k_c0, v_k_min); + __m256d e_fl = _mm256_max_pd(e_c0, v_eps_min); + + /* Upwind advection */ + __m256d adv_k = _mm256_add_pd( + avx2_upwind(v_u, k_w, k_c0, k_e, v_inv_dx), + avx2_upwind(v_v, k_s, k_c0, k_n, v_inv_dy)); + __m256d adv_e = _mm256_add_pd( + avx2_upwind(v_u, e_w, e_c0, e_e, v_inv_dx), + avx2_upwind(v_v, e_s, e_c0, e_n, v_inv_dy)); + + /* Conservative face-averaged diffusion */ + __m256d diff_k = _mm256_add_pd( + _mm256_mul_pd(_mm256_sub_pd( + _mm256_mul_pd(avx2_gamma(v_nu, nt_c, nt_e, v_sigma_k), + _mm256_sub_pd(k_e, k_c0)), + _mm256_mul_pd(avx2_gamma(v_nu, nt_c, nt_w, v_sigma_k), + _mm256_sub_pd(k_c0, k_w))), v_inv_dx2), + _mm256_mul_pd(_mm256_sub_pd( + _mm256_mul_pd(avx2_gamma(v_nu, nt_c, nt_n, v_sigma_k), + _mm256_sub_pd(k_n, k_c0)), + _mm256_mul_pd(avx2_gamma(v_nu, nt_c, nt_s, v_sigma_k), + _mm256_sub_pd(k_c0, k_s))), v_inv_dy2)); + __m256d diff_e = _mm256_add_pd( + _mm256_mul_pd(_mm256_sub_pd( + _mm256_mul_pd(avx2_gamma(v_nu, nt_c, nt_e, v_sigma_e), + _mm256_sub_pd(e_e, e_c0)), + _mm256_mul_pd(avx2_gamma(v_nu, nt_c, nt_w, v_sigma_e), + _mm256_sub_pd(e_c0, e_w))), v_inv_dx2), + _mm256_mul_pd(_mm256_sub_pd( + _mm256_mul_pd(avx2_gamma(v_nu, nt_c, nt_n, v_sigma_e), + _mm256_sub_pd(e_n, e_c0)), + _mm256_mul_pd(avx2_gamma(v_nu, nt_c, nt_s, v_sigma_e), + _mm256_sub_pd(e_c0, e_s))), v_inv_dy2)); + + /* Production from central velocity gradients, limited */ + __m256d du_dx = _mm256_mul_pd(_mm256_sub_pd( + _mm256_loadu_pd(&u[idx + 1]), _mm256_loadu_pd(&u[idx - 1])), v_inv_2dx); + __m256d du_dy = _mm256_mul_pd(_mm256_sub_pd( + _mm256_loadu_pd(&u[idx + nx]), _mm256_loadu_pd(&u[idx - nx])), v_inv_2dy); + __m256d dv_dx = _mm256_mul_pd(_mm256_sub_pd( + _mm256_loadu_pd(&v[idx + 1]), _mm256_loadu_pd(&v[idx - 1])), v_inv_2dx); + __m256d dv_dy = _mm256_mul_pd(_mm256_sub_pd( + _mm256_loadu_pd(&v[idx + nx]), _mm256_loadu_pd(&v[idx - nx])), v_inv_2dy); + __m256d shear = _mm256_add_pd(du_dy, dv_dx); + __m256d S2 = _mm256_add_pd( + _mm256_add_pd( + _mm256_mul_pd(v_two, _mm256_mul_pd(du_dx, du_dx)), + _mm256_mul_pd(v_two, _mm256_mul_pd(dv_dy, dv_dy))), + _mm256_mul_pd(shear, shear)); + __m256d P_k = _mm256_min_pd(_mm256_mul_pd(nt_c, S2), + _mm256_mul_pd(v_prod_lim, e_fl)); + + /* Patankar semi-implicit updates */ + __m256d eok = _mm256_div_pd(e_fl, k_fl); + __m256d k_rhs = _mm256_add_pd(k_c0, _mm256_mul_pd(v_dt, + _mm256_add_pd(_mm256_sub_pd(diff_k, adv_k), P_k))); + __m256d k_upd = _mm256_div_pd(k_rhs, + _mm256_add_pd(v_one, _mm256_mul_pd(v_dt, eok))); + __m256d e_src = _mm256_mul_pd(v_c1, _mm256_mul_pd(eok, P_k)); + __m256d e_rhs = _mm256_add_pd(e_c0, _mm256_mul_pd(v_dt, + _mm256_add_pd(_mm256_sub_pd(diff_e, adv_e), e_src))); + __m256d e_upd = _mm256_div_pd(e_rhs, + _mm256_add_pd(v_one, _mm256_mul_pd(v_dt, _mm256_mul_pd(v_c2, eok)))); + + _mm256_storeu_pd(&k_new[idx], _mm256_max_pd(k_upd, v_k_min)); + _mm256_storeu_pd(&eps_new[idx], _mm256_max_pd(e_upd, v_eps_min)); + } + /* Scalar remainder tail */ + for (; i < nx - 1; i++) { + kepsilon_point(field, params, row + i, nx, dt, inv_dx, inv_dy, + inv_2dx, inv_2dy, inv_dx2, inv_dy2, k_new, eps_new); + } + } +#else + int j; +#ifdef _OPENMP +#pragma omp parallel for schedule(static) +#endif + for (j = 1; j < (int)ny - 1; j++) { + for (size_t i = 1; i < nx - 1; i++) { + kepsilon_point(field, params, (size_t)j * nx + i, nx, dt, inv_dx, inv_dy, + inv_2dx, inv_2dy, inv_dx2, inv_dy2, k_new, eps_new); + } + } +#endif +} + +/* SA transport: OMP-parallel scalar rows (see file header for rationale). + * Numerics identical to turb_sa_step_scalar. */ +static void turb_sa_step_avx2(const flow_field* field, const grid* grid, + const ns_solver_params_t* params, double dt, + double* nt_new) { + size_t nx = field->nx; + size_t ny = field->ny; + + const double dx = grid->dx[0]; + const double dy = grid->dy[0]; + const double inv_dx = 1.0 / dx; + const double inv_dy = 1.0 / dy; + const double inv_2dx = 0.5 * inv_dx; + const double inv_2dy = 0.5 * inv_dy; + const double inv_dx2 = inv_dx * inv_dx; + const double inv_dy2 = inv_dy * inv_dy; + + const double* u = field->u; + const double* v = field->v; + const double* nt = field->turb_nu_tilde; + const double cv1_3 = SA_CV1 * SA_CV1 * SA_CV1; + + int j; +#ifdef _OPENMP +#pragma omp parallel for schedule(static) +#endif + for (j = 1; j < (int)ny - 1; j++) { + for (int i = 1; i < (int)nx - 1; i++) { + size_t idx = IDX_2D((size_t)i, (size_t)j, nx); + + double nu = params->mu / fmax(field->rho[idx], 1e-10); + double u_c = u[idx]; + double v_c = v[idx]; + double nt_c = fmax(nt[idx], 0.0); + + double chi = nt_c / nu; + double chi3 = chi * chi * chi; + double fv1 = chi3 / (chi3 + cv1_3); + double fv2 = 1.0 - chi / (1.0 + chi * fv1); + + double du_dy = (u[idx + nx] - u[idx - nx]) * inv_2dy; + double dv_dx = (v[idx + 1] - v[idx - 1]) * inv_2dx; + double omega = fabs(dv_dx - du_dy); + + int has_wall = 0; + double d = turb_wall_distance(grid, ¶ms->turb_bc, (size_t)i, (size_t)j, + &has_wall); + double s_tilde = omega; + double destr_rate = 0.0; + if (has_wall && d > 1e-12) { + double inv_kd2 = 1.0 / (SA_KAPPA * SA_KAPPA * d * d); + s_tilde = fmax(omega + nt_c * fv2 * inv_kd2, 0.3 * omega); + + double r = fmin(nt_c / (s_tilde * SA_KAPPA * SA_KAPPA * d * d + 1e-16), + 10.0); + double r6 = pow(r, 6.0); + double g = r + SA_CW2 * (r6 - r); + double g6 = pow(g, 6.0); + double cw3_6 = pow(SA_CW3, 6.0); + double fw = g * pow((1.0 + cw3_6) / (g6 + cw3_6), 1.0 / 6.0); + + destr_rate = SA_CW1 * fw * nt_c / (d * d); + } + + double adv = (u_c > 0.0 ? u_c * (nt[idx] - nt[idx - 1]) * inv_dx + : u_c * (nt[idx + 1] - nt[idx]) * inv_dx) + + (v_c > 0.0 ? v_c * (nt[idx] - nt[idx - nx]) * inv_dy + : v_c * (nt[idx + nx] - nt[idx]) * inv_dy); + + double gam_e = (nu + 0.5 * (fmax(nt[idx], 0.0) + fmax(nt[idx + 1], 0.0))) / SA_SIGMA; + double gam_w = (nu + 0.5 * (fmax(nt[idx], 0.0) + fmax(nt[idx - 1], 0.0))) / SA_SIGMA; + double gam_n = (nu + 0.5 * (fmax(nt[idx], 0.0) + fmax(nt[idx + nx], 0.0))) / SA_SIGMA; + double gam_s = (nu + 0.5 * (fmax(nt[idx], 0.0) + fmax(nt[idx - nx], 0.0))) / SA_SIGMA; + double diff = (gam_e * (nt[idx + 1] - nt[idx]) - + gam_w * (nt[idx] - nt[idx - 1])) * inv_dx2 + + (gam_n * (nt[idx + nx] - nt[idx]) - + gam_s * (nt[idx] - nt[idx - nx])) * inv_dy2; + + double dnt_dx = (nt[idx + 1] - nt[idx - 1]) * inv_2dx; + double dnt_dy = (nt[idx + nx] - nt[idx - nx]) * inv_2dy; + double grad2 = (SA_CB2 / SA_SIGMA) * (dnt_dx * dnt_dx + dnt_dy * dnt_dy); + + double prod = SA_CB1 * s_tilde * nt_c; + + double nt_upd = (nt[idx] + dt * (-adv + diff + grad2 + prod)) / + (1.0 + dt * destr_rate); + + nt_new[idx] = fmax(nt_upd, 0.0); + } + } +} + +cfd_status_t turbulence_step_explicit_avx2_with_workspace( + flow_field* field, const grid* grid, + const ns_solver_params_t* params, + double dt, double time, + double* workspace, size_t workspace_size) { + (void)time; + + if (!params) { + cfd_set_error(CFD_ERROR_INVALID, + "turbulence_solver_avx2: params must be non-NULL"); + return CFD_ERROR_INVALID; + } + if (params->turb_model == TURB_MODEL_NONE) { + return CFD_SUCCESS; + } + + cfd_status_t status = turb_validate_step_args(field, grid, params); + if (status != CFD_SUCCESS) { + return status; + } + + size_t total = field->nx * field->ny * field->nz; + + int owns_buffer = 0; + double* buf; + if (workspace && workspace_size >= TURB_WORKSPACE_SIZE(total)) { + buf = workspace; + } else { + buf = (double*)cfd_calloc(TURB_WORKSPACE_SIZE(total), sizeof(double)); + if (!buf) { + return CFD_ERROR_NOMEM; + } + owns_buffer = 1; + } + + if (params->turb_model == TURB_MODEL_K_EPSILON) { + double* k_new = buf; + double* eps_new = buf + total; + memcpy(k_new, field->turb_k, total * sizeof(double)); + memcpy(eps_new, field->turb_eps, total * sizeof(double)); + + turb_kepsilon_step_avx2(field, grid, params, dt, k_new, eps_new); + + for (size_t n = 0; n < total; n++) { + if (!isfinite(k_new[n]) || !isfinite(eps_new[n])) { + cfd_set_error(CFD_ERROR_DIVERGED, + "NaN/Inf detected in turbulence_step_explicit_avx2 (k-epsilon)"); + if (owns_buffer) cfd_free(buf); + return CFD_ERROR_DIVERGED; + } + } + memcpy(field->turb_k, k_new, total * sizeof(double)); + memcpy(field->turb_eps, eps_new, total * sizeof(double)); + } else { + double* nt_new = buf; + memcpy(nt_new, field->turb_nu_tilde, total * sizeof(double)); + + turb_sa_step_avx2(field, grid, params, dt, nt_new); + + for (size_t n = 0; n < total; n++) { + if (!isfinite(nt_new[n])) { + cfd_set_error(CFD_ERROR_DIVERGED, + "NaN/Inf detected in turbulence_step_explicit_avx2 (SA)"); + if (owns_buffer) cfd_free(buf); + return CFD_ERROR_DIVERGED; + } + } + memcpy(field->turb_nu_tilde, nt_new, total * sizeof(double)); + } + + turb_update_nu_t(field, params); + + if (owns_buffer) cfd_free(buf); + return CFD_SUCCESS; +} diff --git a/lib/src/solvers/turbulence/cpu/turbulence_kepsilon.c b/lib/src/solvers/turbulence/cpu/turbulence_kepsilon.c new file mode 100644 index 00000000..111c1b84 --- /dev/null +++ b/lib/src/solvers/turbulence/cpu/turbulence_kepsilon.c @@ -0,0 +1,102 @@ +/** + * @file turbulence_kepsilon.c + * @brief Scalar CPU kernel for the standard k-epsilon transport equations + * + * dk/dt + u*grad(k) = div((nu + nu_t/sigma_k) grad k) + P_k - eps + * deps/dt + u*grad(eps) = div((nu + nu_t/sigma_e) grad eps) + (eps/k)(C1*P_k - C2*eps) + * + * First-order upwind advection (positivity), conservative face-averaged + * diffusion, production from central velocity gradients with the limiter + * P_k <= 10*eps, and semi-implicit (Patankar) sink treatment: destruction is + * written as (eps/k)*k_new resp. C2*(eps/k)*eps_new so the update divides by + * (1 + dt*sink_rate) and can never cross zero. Floors keep k and eps positive. + */ + +#include "../turbulence_solver_internal.h" + +#include "cfd/core/indexing.h" + +#include + +void turb_kepsilon_step_scalar(const flow_field* field, const grid* grid, + const ns_solver_params_t* params, double dt, + double* k_new, double* eps_new) { + size_t nx = field->nx; + size_t ny = field->ny; + + const double dx = grid->dx[0]; + const double dy = grid->dy[0]; + const double inv_dx = 1.0 / dx; + const double inv_dy = 1.0 / dy; + const double inv_2dx = 0.5 * inv_dx; + const double inv_2dy = 0.5 * inv_dy; + const double inv_dx2 = inv_dx * inv_dx; + const double inv_dy2 = inv_dy * inv_dy; + + const double* u = field->u; + const double* v = field->v; + const double* k = field->turb_k; + const double* eps = field->turb_eps; + const double* nu_t = field->nu_t; + + for (size_t j = 1; j < ny - 1; j++) { + for (size_t i = 1; i < nx - 1; i++) { + size_t idx = IDX_2D(i, j, nx); + + double nu = params->mu / fmax(field->rho[idx], 1e-10); + double u_c = u[idx]; + double v_c = v[idx]; + double k_c = fmax(k[idx], TURB_K_MIN); + double eps_c = fmax(eps[idx], TURB_EPS_MIN); + + /* First-order upwind advection */ + double adv_k = (u_c > 0.0 ? u_c * (k[idx] - k[idx - 1]) * inv_dx + : u_c * (k[idx + 1] - k[idx]) * inv_dx) + + (v_c > 0.0 ? v_c * (k[idx] - k[idx - nx]) * inv_dy + : v_c * (k[idx + nx] - k[idx]) * inv_dy); + double adv_e = (u_c > 0.0 ? u_c * (eps[idx] - eps[idx - 1]) * inv_dx + : u_c * (eps[idx + 1] - eps[idx]) * inv_dx) + + (v_c > 0.0 ? v_c * (eps[idx] - eps[idx - nx]) * inv_dy + : v_c * (eps[idx + nx] - eps[idx]) * inv_dy); + + /* Conservative face-averaged diffusion */ + double gam_e_k = nu + 0.5 * (nu_t[idx] + nu_t[idx + 1]) / TURB_SIGMA_K; + double gam_w_k = nu + 0.5 * (nu_t[idx] + nu_t[idx - 1]) / TURB_SIGMA_K; + double gam_n_k = nu + 0.5 * (nu_t[idx] + nu_t[idx + nx]) / TURB_SIGMA_K; + double gam_s_k = nu + 0.5 * (nu_t[idx] + nu_t[idx - nx]) / TURB_SIGMA_K; + double diff_k = (gam_e_k * (k[idx + 1] - k[idx]) - + gam_w_k * (k[idx] - k[idx - 1])) * inv_dx2 + + (gam_n_k * (k[idx + nx] - k[idx]) - + gam_s_k * (k[idx] - k[idx - nx])) * inv_dy2; + + double gam_e_e = nu + 0.5 * (nu_t[idx] + nu_t[idx + 1]) / TURB_SIGMA_EPS; + double gam_w_e = nu + 0.5 * (nu_t[idx] + nu_t[idx - 1]) / TURB_SIGMA_EPS; + double gam_n_e = nu + 0.5 * (nu_t[idx] + nu_t[idx + nx]) / TURB_SIGMA_EPS; + double gam_s_e = nu + 0.5 * (nu_t[idx] + nu_t[idx - nx]) / TURB_SIGMA_EPS; + double diff_e = (gam_e_e * (eps[idx + 1] - eps[idx]) - + gam_w_e * (eps[idx] - eps[idx - 1])) * inv_dx2 + + (gam_n_e * (eps[idx + nx] - eps[idx]) - + gam_s_e * (eps[idx] - eps[idx - nx])) * inv_dy2; + + /* Production from central velocity gradients, limited */ + double du_dx = (u[idx + 1] - u[idx - 1]) * inv_2dx; + double du_dy = (u[idx + nx] - u[idx - nx]) * inv_2dy; + double dv_dx = (v[idx + 1] - v[idx - 1]) * inv_2dx; + double dv_dy = (v[idx + nx] - v[idx - nx]) * inv_2dy; + double S2 = 2.0 * du_dx * du_dx + 2.0 * dv_dy * dv_dy + + (du_dy + dv_dx) * (du_dy + dv_dx); + double P_k = fmin(nu_t[idx] * S2, TURB_PROD_LIMIT_FACTOR * eps_c); + + /* Patankar semi-implicit updates: sink terms cannot cross zero */ + double eps_over_k = eps_c / k_c; + double k_upd = (k[idx] + dt * (-adv_k + diff_k + P_k)) / + (1.0 + dt * eps_over_k); + double e_upd = (eps[idx] + dt * (-adv_e + diff_e + + TURB_C1_EPS * eps_over_k * P_k)) / + (1.0 + dt * TURB_C2_EPS * eps_over_k); + + k_new[idx] = fmax(k_upd, TURB_K_MIN); + eps_new[idx] = fmax(e_upd, TURB_EPS_MIN); + } + } +} diff --git a/lib/src/solvers/turbulence/cpu/turbulence_sa.c b/lib/src/solvers/turbulence/cpu/turbulence_sa.c new file mode 100644 index 00000000..dc70d5e8 --- /dev/null +++ b/lib/src/solvers/turbulence/cpu/turbulence_sa.c @@ -0,0 +1,114 @@ +/** + * @file turbulence_sa.c + * @brief Scalar CPU kernel for the Spalart-Allmaras transport equation + * + * No-ft2 (fully turbulent) variant — the trip terms only matter for laminar- + * turbulent transition prediction and are omitted by virtually all RANS + * solvers by default: + * + * dnt/dt + u*grad(nt) = (1/sigma)[div((nu+nt) grad nt) + cb2*|grad nt|^2] + * + cb1*S_tilde*nt - cw1*fw*(nt/d)^2 + * + * First-order upwind advection, conservative face-averaged diffusion, and + * semi-implicit (Patankar) destruction so nt can never cross zero. Cells with + * no wall-function wall configured (has_wall == 0) drop the wall-destruction + * and fv2 near-wall terms entirely (S_tilde = Omega). + */ + +#include "../turbulence_solver_internal.h" + +#include "cfd/core/indexing.h" + +#include + +void turb_sa_step_scalar(const flow_field* field, const grid* grid, + const ns_solver_params_t* params, double dt, + double* nt_new) { + size_t nx = field->nx; + size_t ny = field->ny; + + const double dx = grid->dx[0]; + const double dy = grid->dy[0]; + const double inv_dx = 1.0 / dx; + const double inv_dy = 1.0 / dy; + const double inv_2dx = 0.5 * inv_dx; + const double inv_2dy = 0.5 * inv_dy; + const double inv_dx2 = inv_dx * inv_dx; + const double inv_dy2 = inv_dy * inv_dy; + + const double* u = field->u; + const double* v = field->v; + const double* nt = field->turb_nu_tilde; + const double cv1_3 = SA_CV1 * SA_CV1 * SA_CV1; + + for (size_t j = 1; j < ny - 1; j++) { + for (size_t i = 1; i < nx - 1; i++) { + size_t idx = IDX_2D(i, j, nx); + + double nu = params->mu / fmax(field->rho[idx], 1e-10); + double u_c = u[idx]; + double v_c = v[idx]; + double nt_c = fmax(nt[idx], 0.0); + + /* Closure functions */ + double chi = nt_c / nu; + double chi3 = chi * chi * chi; + double fv1 = chi3 / (chi3 + cv1_3); + double fv2 = 1.0 - chi / (1.0 + chi * fv1); + + /* Vorticity magnitude (2D) */ + double du_dy = (u[idx + nx] - u[idx - nx]) * inv_2dy; + double dv_dx = (v[idx + 1] - v[idx - 1]) * inv_2dx; + double omega = fabs(dv_dx - du_dy); + + /* Wall distance and modified vorticity S_tilde */ + int has_wall = 0; + double d = turb_wall_distance(grid, ¶ms->turb_bc, i, j, &has_wall); + double s_tilde = omega; + double destr_rate = 0.0; /* cw1*fw*nt/d^2, the Patankar sink rate */ + if (has_wall && d > 1e-12) { + double inv_kd2 = 1.0 / (SA_KAPPA * SA_KAPPA * d * d); + s_tilde = fmax(omega + nt_c * fv2 * inv_kd2, 0.3 * omega); + + double r = fmin(nt_c / (s_tilde * SA_KAPPA * SA_KAPPA * d * d + 1e-16), + 10.0); + double r6 = pow(r, 6.0); + double g = r + SA_CW2 * (r6 - r); + double g6 = pow(g, 6.0); + double cw3_6 = pow(SA_CW3, 6.0); + double fw = g * pow((1.0 + cw3_6) / (g6 + cw3_6), 1.0 / 6.0); + + destr_rate = SA_CW1 * fw * nt_c / (d * d); + } + + /* First-order upwind advection */ + double adv = (u_c > 0.0 ? u_c * (nt[idx] - nt[idx - 1]) * inv_dx + : u_c * (nt[idx + 1] - nt[idx]) * inv_dx) + + (v_c > 0.0 ? v_c * (nt[idx] - nt[idx - nx]) * inv_dy + : v_c * (nt[idx + nx] - nt[idx]) * inv_dy); + + /* Conservative face-averaged diffusion + cb2 gradient-squared term */ + double gam_e = (nu + 0.5 * (fmax(nt[idx], 0.0) + fmax(nt[idx + 1], 0.0))) / SA_SIGMA; + double gam_w = (nu + 0.5 * (fmax(nt[idx], 0.0) + fmax(nt[idx - 1], 0.0))) / SA_SIGMA; + double gam_n = (nu + 0.5 * (fmax(nt[idx], 0.0) + fmax(nt[idx + nx], 0.0))) / SA_SIGMA; + double gam_s = (nu + 0.5 * (fmax(nt[idx], 0.0) + fmax(nt[idx - nx], 0.0))) / SA_SIGMA; + double diff = (gam_e * (nt[idx + 1] - nt[idx]) - + gam_w * (nt[idx] - nt[idx - 1])) * inv_dx2 + + (gam_n * (nt[idx + nx] - nt[idx]) - + gam_s * (nt[idx] - nt[idx - nx])) * inv_dy2; + + double dnt_dx = (nt[idx + 1] - nt[idx - 1]) * inv_2dx; + double dnt_dy = (nt[idx + nx] - nt[idx - nx]) * inv_2dy; + double grad2 = (SA_CB2 / SA_SIGMA) * (dnt_dx * dnt_dx + dnt_dy * dnt_dy); + + /* Production */ + double prod = SA_CB1 * s_tilde * nt_c; + + /* Patankar semi-implicit update: destruction cannot cross zero */ + double nt_upd = (nt[idx] + dt * (-adv + diff + grad2 + prod)) / + (1.0 + dt * destr_rate); + + nt_new[idx] = fmax(nt_upd, 0.0); + } + } +} diff --git a/lib/src/solvers/turbulence/cpu/turbulence_solver.c b/lib/src/solvers/turbulence/cpu/turbulence_solver.c new file mode 100644 index 00000000..ce1cb08f --- /dev/null +++ b/lib/src/solvers/turbulence/cpu/turbulence_solver.c @@ -0,0 +1,550 @@ +/** + * @file turbulence_solver.c + * @brief Scalar CPU implementation of the RANS turbulence solver + * + * Public entry points, model dispatch, boundary conditions, and the standard + * log-law wall functions. The per-model transport kernels live in + * turbulence_kepsilon.c and turbulence_sa.c. + */ + +#include "cfd/solvers/turbulence_solver.h" +#include "../turbulence_solver_internal.h" + +#include "cfd/core/indexing.h" +#include "cfd/core/memory.h" + +#include +#include + +/* Kinematic viscosity at a grid point (density-floored, like the RHS kernel). */ +static double local_nu(const ns_solver_params_t* params, const flow_field* field, + size_t idx) { + return params->mu / fmax(field->rho[idx], 1e-10); +} + +/* SA viscous damping function fv1(chi). */ +static double sa_fv1(double chi) { + double chi3 = chi * chi * chi; + return chi3 / (chi3 + SA_CV1 * SA_CV1 * SA_CV1); +} + +void turb_update_nu_t(flow_field* field, const ns_solver_params_t* params) { + size_t total = field->nx * field->ny * field->nz; + + if (params->turb_model == TURB_MODEL_K_EPSILON) { + for (size_t n = 0; n < total; n++) { + double nu = local_nu(params, field, n); + double k_c = fmax(field->turb_k[n], 0.0); + double eps_c = fmax(field->turb_eps[n], TURB_EPS_MIN); + double nu_t = TURB_C_MU * k_c * k_c / eps_c; + field->nu_t[n] = fmin(nu_t, TURB_NU_T_MAX_FACTOR * nu); + } + } else if (params->turb_model == TURB_MODEL_SPALART_ALLMARAS) { + for (size_t n = 0; n < total; n++) { + double nu = local_nu(params, field, n); + double nt = fmax(field->turb_nu_tilde[n], 0.0); + double nu_t = nt * sa_fv1(nt / nu); + field->nu_t[n] = fmin(nu_t, TURB_NU_T_MAX_FACTOR * nu); + } + } +} + +double turb_wall_distance(const grid* grid, const ns_turbulence_bc_config_t* tbc, + size_t i, size_t j, int* has_wall) { + double d = 0.0; + int found = 0; + + if (tbc->left == BC_TYPE_NOSLIP) { + double c = grid->x[i] - grid->x[0]; + d = found ? fmin(d, c) : c; + found = 1; + } + if (tbc->right == BC_TYPE_NOSLIP) { + double c = grid->x[grid->nx - 1] - grid->x[i]; + d = found ? fmin(d, c) : c; + found = 1; + } + if (tbc->bottom == BC_TYPE_NOSLIP) { + double c = grid->y[j] - grid->y[0]; + d = found ? fmin(d, c) : c; + found = 1; + } + if (tbc->top == BC_TYPE_NOSLIP) { + double c = grid->y[grid->ny - 1] - grid->y[j]; + d = found ? fmin(d, c) : c; + found = 1; + } + + *has_wall = found; + return d; +} + +double turbulence_wall_u_tau(double u_p, double y_p, double nu) { + if (u_p <= 0.0 || y_p <= 0.0 || nu <= 0.0) { + return 0.0; + } + + /* Viscous sublayer (linear law u+ = y+): u_tau = sqrt(nu*u_p/y_p) */ + double ut = sqrt(nu * u_p / y_p); + if (ut * y_p / nu < WALL_YPLUS_LAMINAR) { + return ut; + } + + /* Log law: solve ut*(ln(ut*y_p/nu)/kappa + B) = u_p by Newton iteration */ + for (int it = 0; it < 20; it++) { + double yplus = ut * y_p / nu; + double f = ut * (log(yplus) / WALL_KAPPA + WALL_B) - u_p; + double fp = (log(yplus) + 1.0) / WALL_KAPPA + WALL_B; + double ut_new = ut - f / fp; + if (ut_new < 1e-12) { + ut_new = 1e-12; + } + double converged = fabs(ut_new - ut) < 1e-12 * fmax(ut, 1.0); + ut = ut_new; + if (converged) { + break; + } + } + return ut; +} + +/* Validate uniform spacing (the transport stencils assume it, like energy). */ +static cfd_status_t validate_uniform_spacing(const grid* grid, size_t nx, size_t ny) { + const double dx0 = grid->dx[0]; + const double tol_x = 1e-12 * fmax(1.0, fabs(dx0)); + for (size_t i = 1; i < nx - 1; i++) { + if (fabs(grid->dx[i] - dx0) > tol_x) { + cfd_set_error(CFD_ERROR_UNSUPPORTED, + "turbulence_solver: non-uniform dx not supported"); + return CFD_ERROR_UNSUPPORTED; + } + } + const double dy0 = grid->dy[0]; + const double tol_y = 1e-12 * fmax(1.0, fabs(dy0)); + for (size_t j = 1; j < ny - 1; j++) { + if (fabs(grid->dy[j] - dy0) > tol_y) { + cfd_set_error(CFD_ERROR_UNSUPPORTED, + "turbulence_solver: non-uniform dy not supported"); + return CFD_ERROR_UNSUPPORTED; + } + } + return CFD_SUCCESS; +} + +/* Shared validation for step and BC application. */ +static cfd_status_t validate_turbulence_args(const flow_field* field, const grid* grid, + const ns_solver_params_t* params) { + if (!field || !grid || !params) { + cfd_set_error(CFD_ERROR_INVALID, + "turbulence_solver: field, grid, and params must be non-NULL"); + return CFD_ERROR_INVALID; + } + if (!field->turb_k || !field->turb_eps || !field->turb_nu_tilde || !field->nu_t) { + cfd_set_error(CFD_ERROR_INVALID, + "turbulence_solver: missing turbulence fields"); + return CFD_ERROR_INVALID; + } + if (params->turb_model != TURB_MODEL_K_EPSILON && + params->turb_model != TURB_MODEL_SPALART_ALLMARAS) { + cfd_set_error(CFD_ERROR_INVALID, + "turbulence_solver: unknown turbulence model"); + return CFD_ERROR_INVALID; + } + /* nu = mu/rho appears in denominators (SA chi, wall functions); mu <= 0 + * would produce Inf/NaN rather than a diagnosable error. */ + if (params->mu <= 0.0) { + cfd_set_error(CFD_ERROR_INVALID, + "turbulence_solver: mu must be positive when a " + "turbulence model is active"); + return CFD_ERROR_INVALID; + } + if (field->nz > 1) { + cfd_set_error(CFD_ERROR_UNSUPPORTED, + "turbulence_solver: 3D turbulence not supported"); + return CFD_ERROR_UNSUPPORTED; + } + if (!grid->dx || !grid->dy || field->nx < 3 || field->ny < 3) { + cfd_set_error(CFD_ERROR_INVALID, + "turbulence_solver: grid too small or missing dx/dy"); + return CFD_ERROR_INVALID; + } + return CFD_SUCCESS; +} + +cfd_status_t turb_validate_step_args(const flow_field* field, const grid* grid, + const ns_solver_params_t* params) { + cfd_status_t status = validate_turbulence_args(field, grid, params); + if (status != CFD_SUCCESS) { + return status; + } + return validate_uniform_spacing(grid, field->nx, field->ny); +} + +cfd_status_t turbulence_step_explicit_with_workspace( + flow_field* field, const grid* grid, + const ns_solver_params_t* params, + double dt, double time, + double* workspace, size_t workspace_size) { + (void)time; + + if (!params) { + cfd_set_error(CFD_ERROR_INVALID, "turbulence_solver: params must be non-NULL"); + return CFD_ERROR_INVALID; + } + /* Turbulence disabled: a legitimate no-op. */ + if (params->turb_model == TURB_MODEL_NONE) { + return CFD_SUCCESS; + } + + cfd_status_t status = turb_validate_step_args(field, grid, params); + if (status != CFD_SUCCESS) { + return status; + } + + size_t total = field->nx * field->ny * field->nz; + + /* Use caller's workspace or allocate internally */ + int owns_buffer = 0; + double* buf; + if (workspace && workspace_size >= TURB_WORKSPACE_SIZE(total)) { + buf = workspace; + } else { + buf = (double*)cfd_calloc(TURB_WORKSPACE_SIZE(total), sizeof(double)); + if (!buf) { + return CFD_ERROR_NOMEM; + } + owns_buffer = 1; + } + + if (params->turb_model == TURB_MODEL_K_EPSILON) { + double* k_new = buf; + double* eps_new = buf + total; + memcpy(k_new, field->turb_k, total * sizeof(double)); + memcpy(eps_new, field->turb_eps, total * sizeof(double)); + + turb_kepsilon_step_scalar(field, grid, params, dt, k_new, eps_new); + + for (size_t n = 0; n < total; n++) { + if (!isfinite(k_new[n]) || !isfinite(eps_new[n])) { + cfd_set_error(CFD_ERROR_DIVERGED, + "NaN/Inf detected in turbulence_step_explicit (k-epsilon)"); + if (owns_buffer) cfd_free(buf); + return CFD_ERROR_DIVERGED; + } + } + memcpy(field->turb_k, k_new, total * sizeof(double)); + memcpy(field->turb_eps, eps_new, total * sizeof(double)); + } else { + double* nt_new = buf; + memcpy(nt_new, field->turb_nu_tilde, total * sizeof(double)); + + turb_sa_step_scalar(field, grid, params, dt, nt_new); + + for (size_t n = 0; n < total; n++) { + if (!isfinite(nt_new[n])) { + cfd_set_error(CFD_ERROR_DIVERGED, + "NaN/Inf detected in turbulence_step_explicit (SA)"); + if (owns_buffer) cfd_free(buf); + return CFD_ERROR_DIVERGED; + } + } + memcpy(field->turb_nu_tilde, nt_new, total * sizeof(double)); + } + + turb_update_nu_t(field, params); + + if (owns_buffer) cfd_free(buf); + return CFD_SUCCESS; +} + +cfd_status_t turbulence_step_explicit(flow_field* field, const grid* grid, + const ns_solver_params_t* params, + double dt, double time) { + return turbulence_step_explicit_with_workspace(field, grid, params, + dt, time, NULL, 0); +} + +/* A face may only request a turbulence BC type the solver implements. */ +static int is_supported_turb_bc(bc_type_t type) { + return type == BC_TYPE_PERIODIC || type == BC_TYPE_NEUMANN || + type == BC_TYPE_DIRICHLET || type == BC_TYPE_NOSLIP; +} + +/* + * Standard log-law wall function at one wall node. + * + * idx_w: wall node, idx_p: first interior node at distance y_p, u_p: wall- + * parallel speed at idx_p. Sets equilibrium turbulence values at the first + * interior node (fixed-value wall-function variant): + * k_p = u_tau^2 / sqrt(C_mu) eps_p = u_tau^3 / (kappa*y_p) + * nt_p = kappa*u_tau*y_p + * and imposes the log-law momentum sink through the wall-face viscosity: the + * discrete wall shear is (nu + 0.5*(nu_t_w + nu_t_p)) * u_p/y_p, so storing + * nu_t_w = 0, nu_t_p = max(2*(u_tau^2*y_p/u_p - nu), 0) + * makes it exactly u_tau^2. The equilibrium eddy viscosity kappa*u_tau*y_p is + * deliberately NOT stored at idx_p: the coarse one-sided gradient u_p/y_p + * cannot represent the log profile's curvature, and pairing it with the + * physical nu_t would overpredict the wall shear several-fold. In the viscous + * sublayer u_tau^2 = nu*u_p/y_p, so nu_t_p reduces to 0 (pure laminar shear). + */ +static void apply_wall_function_node(flow_field* field, + const ns_solver_params_t* params, + size_t idx_w, size_t idx_p, + double y_p, double u_p) { + double nu = local_nu(params, field, idx_p); + double ut = turbulence_wall_u_tau(u_p, y_p, nu); + + if (params->turb_model == TURB_MODEL_K_EPSILON) { + field->turb_k[idx_p] = fmax(ut * ut / sqrt(TURB_C_MU), TURB_K_MIN); + field->turb_eps[idx_p] = fmax(ut * ut * ut / (WALL_KAPPA * y_p), TURB_EPS_MIN); + field->turb_k[idx_w] = 0.0; + field->turb_eps[idx_w] = field->turb_eps[idx_p]; + } else { + field->turb_nu_tilde[idx_p] = WALL_KAPPA * ut * y_p; + field->turb_nu_tilde[idx_w] = 0.0; + } + + /* Wall-shear-matching viscosity (see function comment), subject to the + * same realizability bound as turb_update_nu_t: on very coarse grids or + * near-zero u_p the matching formula can blow up and destabilize the + * momentum step / time-step estimation. */ + if (u_p > 1e-12) { + double nu_wall_eff = ut * ut * y_p / u_p; + field->nu_t[idx_p] = fmin(fmax(2.0 * (nu_wall_eff - nu), 0.0), + TURB_NU_T_MAX_FACTOR * nu); + } else { + field->nu_t[idx_p] = 0.0; + } + field->nu_t[idx_w] = 0.0; +} + +/* Apply one BC type to one scalar array cell (periodic/neumann/dirichlet). */ +static void apply_scalar_face_bc(double* arr, bc_type_t type, size_t idx, + size_t idx_interior, size_t idx_periodic, + double dirichlet_value) { + if (type == BC_TYPE_DIRICHLET) { + arr[idx] = dirichlet_value; + } else if (type == BC_TYPE_NEUMANN) { + arr[idx] = arr[idx_interior]; + } else if (type == BC_TYPE_PERIODIC) { + arr[idx] = arr[idx_periodic]; + } + /* BC_TYPE_NOSLIP handled by the wall-function path, not here. */ +} + +/* Set nu_t at a Dirichlet boundary node consistently with the fixed values. */ +static void set_dirichlet_nu_t(flow_field* field, const ns_solver_params_t* params, + size_t idx) { + double nu = local_nu(params, field, idx); + double nu_t; + if (params->turb_model == TURB_MODEL_K_EPSILON) { + double k_c = fmax(field->turb_k[idx], 0.0); + double eps_c = fmax(field->turb_eps[idx], TURB_EPS_MIN); + nu_t = TURB_C_MU * k_c * k_c / eps_c; + } else { + double nt = fmax(field->turb_nu_tilde[idx], 0.0); + nu_t = nt * sa_fv1(nt / nu); + } + field->nu_t[idx] = fmin(nu_t, TURB_NU_T_MAX_FACTOR * nu); +} + +cfd_status_t turbulence_apply_bcs(flow_field* field, const grid* grid, + const ns_solver_params_t* params) { + if (!params) { + cfd_set_error(CFD_ERROR_INVALID, + "turbulence_apply_bcs: params must be non-NULL"); + return CFD_ERROR_INVALID; + } + if (params->turb_model == TURB_MODEL_NONE) { + return CFD_SUCCESS; + } + + cfd_status_t status = validate_turbulence_args(field, grid, params); + if (status != CFD_SUCCESS) { + return status; + } + + const ns_turbulence_bc_config_t* tbc = ¶ms->turb_bc; + size_t nx = field->nx; + size_t ny = field->ny; + + if (!is_supported_turb_bc(tbc->left) || !is_supported_turb_bc(tbc->right) || + !is_supported_turb_bc(tbc->bottom) || !is_supported_turb_bc(tbc->top)) { + cfd_set_error(CFD_ERROR_INVALID, + "turbulence_apply_bcs: unsupported turbulence BC type on a face " + "(only PERIODIC, NEUMANN, DIRICHLET, NOSLIP are valid)"); + return CFD_ERROR_INVALID; + } + + const int is_ke = (params->turb_model == TURB_MODEL_K_EPSILON); + + /* Left face (i=0) */ + if (tbc->left == BC_TYPE_NOSLIP) { + double y_p = grid->x[1] - grid->x[0]; + for (size_t j = 0; j < ny; j++) { + size_t idx_w = j * nx; + size_t idx_p = idx_w + 1; + apply_wall_function_node(field, params, idx_w, idx_p, y_p, + fabs(field->v[idx_p])); + } + } else { + for (size_t j = 0; j < ny; j++) { + size_t idx = j * nx; + size_t idx_int = idx + 1; + size_t idx_per = j * nx + (nx - 2); + if (is_ke) { + apply_scalar_face_bc(field->turb_k, tbc->left, idx, idx_int, idx_per, + tbc->k_values.left); + apply_scalar_face_bc(field->turb_eps, tbc->left, idx, idx_int, idx_per, + tbc->eps_values.left); + } else { + apply_scalar_face_bc(field->turb_nu_tilde, tbc->left, idx, idx_int, + idx_per, tbc->nu_tilde_values.left); + } + if (tbc->left == BC_TYPE_DIRICHLET) { + set_dirichlet_nu_t(field, params, idx); + } else { + apply_scalar_face_bc(field->nu_t, tbc->left, idx, idx_int, idx_per, 0.0); + } + } + } + + /* Right face (i=nx-1) */ + if (tbc->right == BC_TYPE_NOSLIP) { + double y_p = grid->x[nx - 1] - grid->x[nx - 2]; + for (size_t j = 0; j < ny; j++) { + size_t idx_w = j * nx + (nx - 1); + size_t idx_p = idx_w - 1; + apply_wall_function_node(field, params, idx_w, idx_p, y_p, + fabs(field->v[idx_p])); + } + } else { + for (size_t j = 0; j < ny; j++) { + size_t idx = j * nx + (nx - 1); + size_t idx_int = idx - 1; + size_t idx_per = j * nx + 1; + if (is_ke) { + apply_scalar_face_bc(field->turb_k, tbc->right, idx, idx_int, idx_per, + tbc->k_values.right); + apply_scalar_face_bc(field->turb_eps, tbc->right, idx, idx_int, idx_per, + tbc->eps_values.right); + } else { + apply_scalar_face_bc(field->turb_nu_tilde, tbc->right, idx, idx_int, + idx_per, tbc->nu_tilde_values.right); + } + if (tbc->right == BC_TYPE_DIRICHLET) { + set_dirichlet_nu_t(field, params, idx); + } else { + apply_scalar_face_bc(field->nu_t, tbc->right, idx, idx_int, idx_per, 0.0); + } + } + } + + /* Bottom face (j=0) — runs after left/right, overwrites shared corners */ + if (tbc->bottom == BC_TYPE_NOSLIP) { + double y_p = grid->y[1] - grid->y[0]; + for (size_t i = 0; i < nx; i++) { + size_t idx_w = i; + size_t idx_p = idx_w + nx; + apply_wall_function_node(field, params, idx_w, idx_p, y_p, + fabs(field->u[idx_p])); + } + } else { + for (size_t i = 0; i < nx; i++) { + size_t idx = i; + size_t idx_int = idx + nx; + size_t idx_per = (ny - 2) * nx + i; + if (is_ke) { + apply_scalar_face_bc(field->turb_k, tbc->bottom, idx, idx_int, idx_per, + tbc->k_values.bottom); + apply_scalar_face_bc(field->turb_eps, tbc->bottom, idx, idx_int, idx_per, + tbc->eps_values.bottom); + } else { + apply_scalar_face_bc(field->turb_nu_tilde, tbc->bottom, idx, idx_int, + idx_per, tbc->nu_tilde_values.bottom); + } + if (tbc->bottom == BC_TYPE_DIRICHLET) { + set_dirichlet_nu_t(field, params, idx); + } else { + apply_scalar_face_bc(field->nu_t, tbc->bottom, idx, idx_int, idx_per, 0.0); + } + } + } + + /* Top face (j=ny-1) */ + if (tbc->top == BC_TYPE_NOSLIP) { + double y_p = grid->y[ny - 1] - grid->y[ny - 2]; + for (size_t i = 0; i < nx; i++) { + size_t idx_w = (ny - 1) * nx + i; + size_t idx_p = idx_w - nx; + apply_wall_function_node(field, params, idx_w, idx_p, y_p, + fabs(field->u[idx_p])); + } + } else { + for (size_t i = 0; i < nx; i++) { + size_t idx = (ny - 1) * nx + i; + size_t idx_int = idx - nx; + size_t idx_per = nx + i; + if (is_ke) { + apply_scalar_face_bc(field->turb_k, tbc->top, idx, idx_int, idx_per, + tbc->k_values.top); + apply_scalar_face_bc(field->turb_eps, tbc->top, idx, idx_int, idx_per, + tbc->eps_values.top); + } else { + apply_scalar_face_bc(field->turb_nu_tilde, tbc->top, idx, idx_int, + idx_per, tbc->nu_tilde_values.top); + } + if (tbc->top == BC_TYPE_DIRICHLET) { + set_dirichlet_nu_t(field, params, idx); + } else { + apply_scalar_face_bc(field->nu_t, tbc->top, idx, idx_int, idx_per, 0.0); + } + } + } + + return CFD_SUCCESS; +} + +cfd_status_t turbulence_init_uniform(flow_field* field, + const ns_solver_params_t* params, + double k0, double eps0, double nu_tilde0) { + if (!field || !params) { + cfd_set_error(CFD_ERROR_INVALID, + "turbulence_init_uniform: field and params must be non-NULL"); + return CFD_ERROR_INVALID; + } + if (params->turb_model == TURB_MODEL_NONE) { + return CFD_SUCCESS; + } + if (!field->turb_k || !field->turb_eps || !field->turb_nu_tilde || !field->nu_t) { + cfd_set_error(CFD_ERROR_INVALID, + "turbulence_init_uniform: missing turbulence fields"); + return CFD_ERROR_INVALID; + } + + size_t total = field->nx * field->ny * field->nz; + + if (params->turb_model == TURB_MODEL_K_EPSILON) { + if (k0 < 0.0 || eps0 <= 0.0) { + cfd_set_error(CFD_ERROR_INVALID, + "turbulence_init_uniform: k0 must be >= 0 and eps0 > 0"); + return CFD_ERROR_INVALID; + } + for (size_t n = 0; n < total; n++) { + field->turb_k[n] = k0; + field->turb_eps[n] = eps0; + } + } else { + if (nu_tilde0 < 0.0) { + cfd_set_error(CFD_ERROR_INVALID, + "turbulence_init_uniform: nu_tilde0 must be >= 0"); + return CFD_ERROR_INVALID; + } + for (size_t n = 0; n < total; n++) { + field->turb_nu_tilde[n] = nu_tilde0; + } + } + + turb_update_nu_t(field, params); + return CFD_SUCCESS; +} diff --git a/lib/src/solvers/turbulence/omp/turbulence_solver_omp.c b/lib/src/solvers/turbulence/omp/turbulence_solver_omp.c new file mode 100644 index 00000000..dbb61def --- /dev/null +++ b/lib/src/solvers/turbulence/omp/turbulence_solver_omp.c @@ -0,0 +1,301 @@ +/** + * @file turbulence_solver_omp.c + * @brief OpenMP-parallelized RANS turbulence transport step + * + * Same numerics as the scalar reference (turbulence/cpu/turbulence_kepsilon.c + * and turbulence_sa.c); the interior stencil loops are parallelized over j. + * Read/write separation (reads field arrays, writes the workspace buffers) + * makes the loops race-free. Validation, nu_t update, boundary conditions, + * and wall functions are shared with the scalar backend (boundary-only work). + * + * This file is always compiled (like the AVX2 backend). It uses only #pragma + * omp directives — no OpenMP runtime calls — so when OpenMP is unavailable the + * pragmas are ignored and the interior loops run serially. The entry point + * therefore always provides a correct implementation, and the OMP-named symbol + * is present regardless of whether the toolchain supports OpenMP. + */ + +#include "../turbulence_solver_internal.h" + +#include "cfd/core/indexing.h" +#include "cfd/core/memory.h" + +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +/* OMP k-epsilon transport kernel — numerics identical to + * turb_kepsilon_step_scalar, j-loop parallelized. */ +static void turb_kepsilon_step_omp(const flow_field* field, const grid* grid, + const ns_solver_params_t* params, double dt, + double* k_new, double* eps_new) { + size_t nx = field->nx; + size_t ny = field->ny; + + const double dx = grid->dx[0]; + const double dy = grid->dy[0]; + const double inv_dx = 1.0 / dx; + const double inv_dy = 1.0 / dy; + const double inv_2dx = 0.5 * inv_dx; + const double inv_2dy = 0.5 * inv_dy; + const double inv_dx2 = inv_dx * inv_dx; + const double inv_dy2 = inv_dy * inv_dy; + + const double* u = field->u; + const double* v = field->v; + const double* k = field->turb_k; + const double* eps = field->turb_eps; + const double* nu_t = field->nu_t; + + int j; +#pragma omp parallel for schedule(static) + for (j = 1; j < (int)ny - 1; j++) { + for (int i = 1; i < (int)nx - 1; i++) { + size_t idx = IDX_2D((size_t)i, (size_t)j, nx); + + double nu = params->mu / fmax(field->rho[idx], 1e-10); + double u_c = u[idx]; + double v_c = v[idx]; + double k_c = fmax(k[idx], TURB_K_MIN); + double eps_c = fmax(eps[idx], TURB_EPS_MIN); + + /* First-order upwind advection */ + double adv_k = (u_c > 0.0 ? u_c * (k[idx] - k[idx - 1]) * inv_dx + : u_c * (k[idx + 1] - k[idx]) * inv_dx) + + (v_c > 0.0 ? v_c * (k[idx] - k[idx - nx]) * inv_dy + : v_c * (k[idx + nx] - k[idx]) * inv_dy); + double adv_e = (u_c > 0.0 ? u_c * (eps[idx] - eps[idx - 1]) * inv_dx + : u_c * (eps[idx + 1] - eps[idx]) * inv_dx) + + (v_c > 0.0 ? v_c * (eps[idx] - eps[idx - nx]) * inv_dy + : v_c * (eps[idx + nx] - eps[idx]) * inv_dy); + + /* Conservative face-averaged diffusion */ + double gam_e_k = nu + 0.5 * (nu_t[idx] + nu_t[idx + 1]) / TURB_SIGMA_K; + double gam_w_k = nu + 0.5 * (nu_t[idx] + nu_t[idx - 1]) / TURB_SIGMA_K; + double gam_n_k = nu + 0.5 * (nu_t[idx] + nu_t[idx + nx]) / TURB_SIGMA_K; + double gam_s_k = nu + 0.5 * (nu_t[idx] + nu_t[idx - nx]) / TURB_SIGMA_K; + double diff_k = (gam_e_k * (k[idx + 1] - k[idx]) - + gam_w_k * (k[idx] - k[idx - 1])) * inv_dx2 + + (gam_n_k * (k[idx + nx] - k[idx]) - + gam_s_k * (k[idx] - k[idx - nx])) * inv_dy2; + + double gam_e_e = nu + 0.5 * (nu_t[idx] + nu_t[idx + 1]) / TURB_SIGMA_EPS; + double gam_w_e = nu + 0.5 * (nu_t[idx] + nu_t[idx - 1]) / TURB_SIGMA_EPS; + double gam_n_e = nu + 0.5 * (nu_t[idx] + nu_t[idx + nx]) / TURB_SIGMA_EPS; + double gam_s_e = nu + 0.5 * (nu_t[idx] + nu_t[idx - nx]) / TURB_SIGMA_EPS; + double diff_e = (gam_e_e * (eps[idx + 1] - eps[idx]) - + gam_w_e * (eps[idx] - eps[idx - 1])) * inv_dx2 + + (gam_n_e * (eps[idx + nx] - eps[idx]) - + gam_s_e * (eps[idx] - eps[idx - nx])) * inv_dy2; + + /* Production from central velocity gradients, limited */ + double du_dx = (u[idx + 1] - u[idx - 1]) * inv_2dx; + double du_dy = (u[idx + nx] - u[idx - nx]) * inv_2dy; + double dv_dx = (v[idx + 1] - v[idx - 1]) * inv_2dx; + double dv_dy = (v[idx + nx] - v[idx - nx]) * inv_2dy; + double S2 = 2.0 * du_dx * du_dx + 2.0 * dv_dy * dv_dy + + (du_dy + dv_dx) * (du_dy + dv_dx); + double P_k = fmin(nu_t[idx] * S2, TURB_PROD_LIMIT_FACTOR * eps_c); + + /* Patankar semi-implicit updates: sink terms cannot cross zero */ + double eps_over_k = eps_c / k_c; + double k_upd = (k[idx] + dt * (-adv_k + diff_k + P_k)) / + (1.0 + dt * eps_over_k); + double e_upd = (eps[idx] + dt * (-adv_e + diff_e + + TURB_C1_EPS * eps_over_k * P_k)) / + (1.0 + dt * TURB_C2_EPS * eps_over_k); + + k_new[idx] = fmax(k_upd, TURB_K_MIN); + eps_new[idx] = fmax(e_upd, TURB_EPS_MIN); + } + } +} + +/* OMP Spalart-Allmaras transport kernel — numerics identical to + * turb_sa_step_scalar, j-loop parallelized. */ +static void turb_sa_step_omp(const flow_field* field, const grid* grid, + const ns_solver_params_t* params, double dt, + double* nt_new) { + size_t nx = field->nx; + size_t ny = field->ny; + + const double dx = grid->dx[0]; + const double dy = grid->dy[0]; + const double inv_dx = 1.0 / dx; + const double inv_dy = 1.0 / dy; + const double inv_2dx = 0.5 * inv_dx; + const double inv_2dy = 0.5 * inv_dy; + const double inv_dx2 = inv_dx * inv_dx; + const double inv_dy2 = inv_dy * inv_dy; + + const double* u = field->u; + const double* v = field->v; + const double* nt = field->turb_nu_tilde; + const double cv1_3 = SA_CV1 * SA_CV1 * SA_CV1; + + int j; +#pragma omp parallel for schedule(static) + for (j = 1; j < (int)ny - 1; j++) { + for (int i = 1; i < (int)nx - 1; i++) { + size_t idx = IDX_2D((size_t)i, (size_t)j, nx); + + double nu = params->mu / fmax(field->rho[idx], 1e-10); + double u_c = u[idx]; + double v_c = v[idx]; + double nt_c = fmax(nt[idx], 0.0); + + /* Closure functions */ + double chi = nt_c / nu; + double chi3 = chi * chi * chi; + double fv1 = chi3 / (chi3 + cv1_3); + double fv2 = 1.0 - chi / (1.0 + chi * fv1); + + /* Vorticity magnitude (2D) */ + double du_dy = (u[idx + nx] - u[idx - nx]) * inv_2dy; + double dv_dx = (v[idx + 1] - v[idx - 1]) * inv_2dx; + double omega = fabs(dv_dx - du_dy); + + /* Wall distance and modified vorticity S_tilde */ + int has_wall = 0; + double d = turb_wall_distance(grid, ¶ms->turb_bc, (size_t)i, (size_t)j, + &has_wall); + double s_tilde = omega; + double destr_rate = 0.0; /* cw1*fw*nt/d^2, the Patankar sink rate */ + if (has_wall && d > 1e-12) { + double inv_kd2 = 1.0 / (SA_KAPPA * SA_KAPPA * d * d); + s_tilde = fmax(omega + nt_c * fv2 * inv_kd2, 0.3 * omega); + + double r = fmin(nt_c / (s_tilde * SA_KAPPA * SA_KAPPA * d * d + 1e-16), + 10.0); + double r6 = pow(r, 6.0); + double g = r + SA_CW2 * (r6 - r); + double g6 = pow(g, 6.0); + double cw3_6 = pow(SA_CW3, 6.0); + double fw = g * pow((1.0 + cw3_6) / (g6 + cw3_6), 1.0 / 6.0); + + destr_rate = SA_CW1 * fw * nt_c / (d * d); + } + + /* First-order upwind advection */ + double adv = (u_c > 0.0 ? u_c * (nt[idx] - nt[idx - 1]) * inv_dx + : u_c * (nt[idx + 1] - nt[idx]) * inv_dx) + + (v_c > 0.0 ? v_c * (nt[idx] - nt[idx - nx]) * inv_dy + : v_c * (nt[idx + nx] - nt[idx]) * inv_dy); + + /* Conservative face-averaged diffusion + cb2 gradient-squared term */ + double gam_e = (nu + 0.5 * (fmax(nt[idx], 0.0) + fmax(nt[idx + 1], 0.0))) / SA_SIGMA; + double gam_w = (nu + 0.5 * (fmax(nt[idx], 0.0) + fmax(nt[idx - 1], 0.0))) / SA_SIGMA; + double gam_n = (nu + 0.5 * (fmax(nt[idx], 0.0) + fmax(nt[idx + nx], 0.0))) / SA_SIGMA; + double gam_s = (nu + 0.5 * (fmax(nt[idx], 0.0) + fmax(nt[idx - nx], 0.0))) / SA_SIGMA; + double diff = (gam_e * (nt[idx + 1] - nt[idx]) - + gam_w * (nt[idx] - nt[idx - 1])) * inv_dx2 + + (gam_n * (nt[idx + nx] - nt[idx]) - + gam_s * (nt[idx] - nt[idx - nx])) * inv_dy2; + + double dnt_dx = (nt[idx + 1] - nt[idx - 1]) * inv_2dx; + double dnt_dy = (nt[idx + nx] - nt[idx - nx]) * inv_2dy; + double grad2 = (SA_CB2 / SA_SIGMA) * (dnt_dx * dnt_dx + dnt_dy * dnt_dy); + + /* Production */ + double prod = SA_CB1 * s_tilde * nt_c; + + /* Patankar semi-implicit update: destruction cannot cross zero */ + double nt_upd = (nt[idx] + dt * (-adv + diff + grad2 + prod)) / + (1.0 + dt * destr_rate); + + nt_new[idx] = fmax(nt_upd, 0.0); + } + } +} + +cfd_status_t turbulence_step_explicit_omp_with_workspace( + flow_field* field, const grid* grid, + const ns_solver_params_t* params, + double dt, double time, + double* workspace, size_t workspace_size) { + (void)time; + + if (!params) { + cfd_set_error(CFD_ERROR_INVALID, + "turbulence_solver_omp: params must be non-NULL"); + return CFD_ERROR_INVALID; + } + if (params->turb_model == TURB_MODEL_NONE) { + return CFD_SUCCESS; + } + + cfd_status_t status = turb_validate_step_args(field, grid, params); + if (status != CFD_SUCCESS) { + return status; + } + + size_t total = field->nx * field->ny * field->nz; + + int owns_buffer = 0; + double* buf; + if (workspace && workspace_size >= TURB_WORKSPACE_SIZE(total)) { + buf = workspace; + } else { + buf = (double*)cfd_calloc(TURB_WORKSPACE_SIZE(total), sizeof(double)); + if (!buf) { + return CFD_ERROR_NOMEM; + } + owns_buffer = 1; + } + + int has_nan = 0; + ptrdiff_t total_int = (ptrdiff_t)total; + ptrdiff_t n; + + if (params->turb_model == TURB_MODEL_K_EPSILON) { + double* k_new = buf; + double* eps_new = buf + total; + memcpy(k_new, field->turb_k, total * sizeof(double)); + memcpy(eps_new, field->turb_eps, total * sizeof(double)); + + turb_kepsilon_step_omp(field, grid, params, dt, k_new, eps_new); + +#pragma omp parallel for reduction(| : has_nan) schedule(static) + for (n = 0; n < total_int; n++) { + if (!isfinite(k_new[n]) || !isfinite(eps_new[n])) { + has_nan = 1; + } + } + if (has_nan) { + cfd_set_error(CFD_ERROR_DIVERGED, + "NaN/Inf detected in turbulence_step_explicit_omp (k-epsilon)"); + if (owns_buffer) cfd_free(buf); + return CFD_ERROR_DIVERGED; + } + memcpy(field->turb_k, k_new, total * sizeof(double)); + memcpy(field->turb_eps, eps_new, total * sizeof(double)); + } else { + double* nt_new = buf; + memcpy(nt_new, field->turb_nu_tilde, total * sizeof(double)); + + turb_sa_step_omp(field, grid, params, dt, nt_new); + +#pragma omp parallel for reduction(| : has_nan) schedule(static) + for (n = 0; n < total_int; n++) { + if (!isfinite(nt_new[n])) { + has_nan = 1; + } + } + if (has_nan) { + cfd_set_error(CFD_ERROR_DIVERGED, + "NaN/Inf detected in turbulence_step_explicit_omp (SA)"); + if (owns_buffer) cfd_free(buf); + return CFD_ERROR_DIVERGED; + } + memcpy(field->turb_nu_tilde, nt_new, total * sizeof(double)); + } + + turb_update_nu_t(field, params); + + if (owns_buffer) cfd_free(buf); + return CFD_SUCCESS; +} diff --git a/lib/src/solvers/turbulence/turbulence_solver_internal.h b/lib/src/solvers/turbulence/turbulence_solver_internal.h new file mode 100644 index 00000000..b65cef9d --- /dev/null +++ b/lib/src/solvers/turbulence/turbulence_solver_internal.h @@ -0,0 +1,112 @@ +/** + * @file turbulence_solver_internal.h + * @brief Internal workspace-aware turbulence solver interface and model constants + * + * Not part of the public API. Used by the NS solvers to avoid per-step + * allocation of scratch buffers. Each backend (scalar, OMP, AVX2) provides a + * workspace-aware turbulence step with identical numerics; the matching NS + * backend calls its own variant so optimized solvers never fall back to the + * scalar turbulence step. + */ + +#ifndef CFD_TURBULENCE_SOLVER_INTERNAL_H +#define CFD_TURBULENCE_SOLVER_INTERNAL_H + +#include "cfd/core/cfd_status.h" +#include "cfd/core/grid.h" +#include "cfd/solvers/navier_stokes_solver.h" + +#include + +/* --- Standard k-epsilon (Launder-Spalding) constants --- */ +#define TURB_C_MU 0.09 +#define TURB_C1_EPS 1.44 +#define TURB_C2_EPS 1.92 +#define TURB_SIGMA_K 1.0 +#define TURB_SIGMA_EPS 1.3 + +/* --- Spalart-Allmaras (no-ft2 variant) constants --- */ +#define SA_CB1 0.1355 +#define SA_CB2 0.622 +#define SA_SIGMA (2.0 / 3.0) +#define SA_KAPPA 0.41 +#define SA_CW1 (SA_CB1 / (SA_KAPPA * SA_KAPPA) + (1.0 + SA_CB2) / SA_SIGMA) +#define SA_CW2 0.3 +#define SA_CW3 2.0 +#define SA_CV1 7.1 + +/* --- Log-law wall function constants --- */ +#define WALL_KAPPA 0.41 +#define WALL_B 5.2 +#define WALL_YPLUS_LAMINAR 11.63 /* linear/log law crossover y+ */ + +/* --- Positivity floors and realizability limits --- */ +#define TURB_K_MIN 1e-10 /* floor for k */ +#define TURB_EPS_MIN 1e-10 /* floor for epsilon */ +#define TURB_PROD_LIMIT_FACTOR 10.0 /* P_k <= factor * eps */ +#define TURB_NU_T_MAX_FACTOR 1e5 /* nu_t <= factor * nu */ + +/* Workspace requirement: 2*nx*ny*nz doubles (k-epsilon uses both halves, + * Spalart-Allmaras uses the first half only). */ +#define TURB_WORKSPACE_SIZE(total) (2 * (total)) + +/** + * Workspace-aware turbulence step (scalar CPU). + * + * When workspace is non-NULL and workspace_size >= 2*nx*ny*nz, uses it as + * scratch instead of allocating. Caller owns the buffer lifetime. + * When workspace is NULL, allocates internally (same as public API). + */ +cfd_status_t turbulence_step_explicit_with_workspace( + flow_field* field, const grid* grid, + const ns_solver_params_t* params, + double dt, double time, + double* workspace, size_t workspace_size); + +/** OpenMP-parallelized turbulence step (cfd_omp). Same numerics as the scalar + * variant; the interior stencil loop is parallelized over j. */ +cfd_status_t turbulence_step_explicit_omp_with_workspace( + flow_field* field, const grid* grid, + const ns_solver_params_t* params, + double dt, double time, + double* workspace, size_t workspace_size); + +/** AVX2-vectorized turbulence step (cfd_simd). Same numerics as the scalar + * variant; the interior stencil is vectorized along i with a scalar tail. */ +cfd_status_t turbulence_step_explicit_avx2_with_workspace( + flow_field* field, const grid* grid, + const ns_solver_params_t* params, + double dt, double time, + double* workspace, size_t workspace_size); + +/** Scalar k-epsilon transport kernel: writes updated k/eps into k_new/eps_new + * (interior points only; boundary values are carried over by the caller). */ +void turb_kepsilon_step_scalar(const flow_field* field, const grid* grid, + const ns_solver_params_t* params, double dt, + double* k_new, double* eps_new); + +/** Scalar Spalart-Allmaras transport kernel: writes updated nu_tilde into + * nt_new (interior points only). */ +void turb_sa_step_scalar(const flow_field* field, const grid* grid, + const ns_solver_params_t* params, double dt, + double* nt_new); + +/** + * Minimum wall distance for point (i,j): distance to the nearest face marked + * BC_TYPE_NOSLIP (wall-function wall) in tbc. Sets *has_wall to 0 and returns + * 0.0 when no face is a wall (SA destruction term must then be disabled). + */ +double turb_wall_distance(const grid* grid, const ns_turbulence_bc_config_t* tbc, + size_t i, size_t j, int* has_wall); + +/** Recompute nu_t from the active model fields with realizability clipping + * (nu_t <= TURB_NU_T_MAX_FACTOR * nu). Used after transport and by BCs. */ +void turb_update_nu_t(flow_field* field, const ns_solver_params_t* params); + +/** Shared argument/grid validation for the turbulence step (all backends): + * non-NULL args and fields, known model, 2D only, nx/ny >= 3, uniform + * spacing. Assumes turb_model != TURB_MODEL_NONE was already checked. */ +cfd_status_t turb_validate_step_args(const flow_field* field, const grid* grid, + const ns_solver_params_t* params); + +#endif /* CFD_TURBULENCE_SOLVER_INTERNAL_H */ diff --git a/tests/solvers/turbulence/test_turbulence_cross_backend.c b/tests/solvers/turbulence/test_turbulence_cross_backend.c new file mode 100644 index 00000000..4d9bcf80 --- /dev/null +++ b/tests/solvers/turbulence/test_turbulence_cross_backend.c @@ -0,0 +1,305 @@ +/** + * @file test_turbulence_cross_backend.c + * @brief Cross-backend consistency for the turbulence transport kernels + * + * Part 1 — kernel-level: advances the k-epsilon and Spalart-Allmaras + * transport equations with the scalar, OMP, and AVX2 backend entry points on + * identical field states (frozen shear velocity) and requires Linf agreement + * within 1e-10 (the OMP kernel is numerically identical to scalar; the AVX2 + * kernel mirrors the scalar operation order). + * + * Part 2 — solver-level: runs the full RK2 solver family (scalar vs OMP vs + * AVX2) with each turbulence model on a wall-bounded channel-like setup and + * requires 1% relative agreement on u (the tolerance existing cross-arch + * solver tests use), plus finite positive turbulence fields. + * + * Unavailable backends (CFD_ERROR_UNSUPPORTED / missing solver) are skipped + * without failing. + */ + +#include "cfd/core/cfd_init.h" +#include "cfd/core/cfd_status.h" +#include "cfd/core/grid.h" +#include "cfd/boundary/boundary_conditions.h" +#include "cfd/solvers/navier_stokes_solver.h" +#include "cfd/solvers/turbulence_solver.h" +#include "unity.h" + +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +/* Internal backend entry points (exported from the static library; declared + * in turbulence_solver_internal.h which is not on the test include path). */ +extern cfd_status_t turbulence_step_explicit_with_workspace( + flow_field* field, const grid* grid, const ns_solver_params_t* params, + double dt, double time, double* workspace, size_t workspace_size); +extern cfd_status_t turbulence_step_explicit_omp_with_workspace( + flow_field* field, const grid* grid, const ns_solver_params_t* params, + double dt, double time, double* workspace, size_t workspace_size); +extern cfd_status_t turbulence_step_explicit_avx2_with_workspace( + flow_field* field, const grid* grid, const ns_solver_params_t* params, + double dt, double time, double* workspace, size_t workspace_size); + +typedef cfd_status_t (*turb_step_fn)(flow_field*, const grid*, + const ns_solver_params_t*, + double, double, double*, size_t); + +void setUp(void) { cfd_init(); } +void tearDown(void) { cfd_finalize(); } + +#define XB_NX 34 +#define XB_NY 18 +#define XB_STEPS 20 +#define XB_DT 5e-4 +#define XB_KERNEL_TOL 1e-10 + +/* ============================================================================ + * Part 1: kernel-level comparison + * ============================================================================ */ + +static void init_kernel_case(flow_field* field, const grid* g, + const ns_solver_params_t* params) { + for (size_t j = 0; j < XB_NY; j++) { + for (size_t i = 0; i < XB_NX; i++) { + size_t idx = j * XB_NX + i; + double x = g->x[i], y = g->y[j]; + field->rho[idx] = 1.0; + field->u[idx] = 4.0 * y * (2.0 - y) + 0.2 * sin(2.0 * M_PI * x / 4.0); + field->v[idx] = 0.1 * sin(2.0 * M_PI * x / 4.0) * cos(M_PI * y); + } + } + TEST_ASSERT_EQUAL(CFD_SUCCESS, + turbulence_init_uniform(field, params, 0.05, 0.1, 3e-3)); +} + +static double linf_diff(const double* a, const double* b, size_t n) { + double m = 0.0; + for (size_t i = 0; i < n; i++) { + double d = fabs(a[i] - b[i]); + if (d > m) m = d; + } + return m; +} + +static void run_kernel_backend(turb_step_fn step, flow_field* field, const grid* g, + const ns_solver_params_t* params, int* unavailable) { + for (int s = 0; s < XB_STEPS; s++) { + cfd_status_t status = step(field, g, params, XB_DT, s * XB_DT, NULL, 0); + if (status == CFD_ERROR_UNSUPPORTED) { + *unavailable = 1; + return; + } + TEST_ASSERT_EQUAL(CFD_SUCCESS, status); + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_apply_bcs(field, g, params)); + } +} + +static void compare_kernel_backends(turbulence_model_t model, const char* label) { + grid* g = grid_create(XB_NX, XB_NY, 1, 0.0, 4.0, 0.0, 2.0, 0.0, 0.0); + TEST_ASSERT_NOT_NULL(g); + grid_initialize_uniform(g); + + ns_solver_params_t params = ns_solver_params_default(); + params.mu = 1e-3; + params.turb_model = model; + params.turb_bc.bottom = BC_TYPE_NOSLIP; + params.turb_bc.top = BC_TYPE_NOSLIP; + + turb_step_fn backends[3] = { + turbulence_step_explicit_with_workspace, + turbulence_step_explicit_omp_with_workspace, + turbulence_step_explicit_avx2_with_workspace, + }; + const char* names[3] = {"scalar", "omp", "avx2"}; + flow_field* fields[3] = {NULL, NULL, NULL}; + int available[3] = {1, 1, 1}; + + for (int b = 0; b < 3; b++) { + fields[b] = flow_field_create(XB_NX, XB_NY, 1); + TEST_ASSERT_NOT_NULL(fields[b]); + init_kernel_case(fields[b], g, ¶ms); + int unavailable = 0; + run_kernel_backend(backends[b], fields[b], g, ¶ms, &unavailable); + if (unavailable) { + available[b] = 0; + printf("[%s] %s backend unavailable, skipping\n", label, names[b]); + } + } + + size_t total = XB_NX * XB_NY; + for (int b = 1; b < 3; b++) { + if (!available[b]) continue; + double d_k = linf_diff(fields[0]->turb_k, fields[b]->turb_k, total); + double d_e = linf_diff(fields[0]->turb_eps, fields[b]->turb_eps, total); + double d_nt = linf_diff(fields[0]->turb_nu_tilde, fields[b]->turb_nu_tilde, total); + double d_nut = linf_diff(fields[0]->nu_t, fields[b]->nu_t, total); + printf("[%s] scalar vs %s: Linf k=%.2e eps=%.2e nt=%.2e nu_t=%.2e\n", + label, names[b], d_k, d_e, d_nt, d_nut); + TEST_ASSERT_TRUE_MESSAGE(d_k < XB_KERNEL_TOL, "k differs across backends"); + TEST_ASSERT_TRUE_MESSAGE(d_e < XB_KERNEL_TOL, "eps differs across backends"); + TEST_ASSERT_TRUE_MESSAGE(d_nt < XB_KERNEL_TOL, "nu_tilde differs across backends"); + TEST_ASSERT_TRUE_MESSAGE(d_nut < XB_KERNEL_TOL, "nu_t differs across backends"); + } + + for (int b = 0; b < 3; b++) { + flow_field_destroy(fields[b]); + } + grid_destroy(g); +} + +static void test_kernel_consistency_kepsilon(void) { + compare_kernel_backends(TURB_MODEL_K_EPSILON, "k-epsilon kernels"); +} + +static void test_kernel_consistency_sa(void) { + compare_kernel_backends(TURB_MODEL_SPALART_ALLMARAS, "SA kernels"); +} + +/* ============================================================================ + * Part 2: full-solver comparison (RK2 family) + * ============================================================================ */ + +static void channel_force(double x, double y, double z, double t, void* ctx, + double* su, double* sv, double* sw) { + (void)x; (void)y; (void)z; (void)t; (void)ctx; + *su = 1.0; + *sv = 0.0; + *sw = 0.0; +} + +static void init_solver_case(flow_field* field, const grid* g, + const ns_solver_params_t* params) { + for (size_t j = 0; j < XB_NY; j++) { + for (size_t i = 0; i < XB_NX; i++) { + size_t idx = j * XB_NX + i; + double y = g->y[j]; + int at_wall = (j == 0 || j == XB_NY - 1); + field->u[idx] = at_wall ? 0.0 : 10.0 * y * (2.0 - y) / 2.0; + field->v[idx] = 0.0; + field->p[idx] = 1.0; + field->rho[idx] = 1.0; + field->T[idx] = 300.0; + } + } + TEST_ASSERT_EQUAL(CFD_SUCCESS, + turbulence_init_uniform(field, params, 0.05, 0.1, 3e-3)); +} + +/* Run one solver for XB_STEPS; returns 0 if solver/backend unavailable. */ +static int run_solver_case(const char* solver_type, turbulence_model_t model, + flow_field* field, const grid* g) { + ns_solver_params_t params = ns_solver_params_default(); + params.dt = XB_DT; + params.max_iter = 1; + params.mu = 1e-3; + params.source_func = channel_force; + params.turb_model = model; + params.turb_bc.bottom = BC_TYPE_NOSLIP; + params.turb_bc.top = BC_TYPE_NOSLIP; + + init_solver_case(field, g, ¶ms); + + ns_solver_registry_t* registry = cfd_registry_create(); + TEST_ASSERT_NOT_NULL(registry); + cfd_registry_register_defaults(registry); + ns_solver_t* slv = cfd_solver_create(registry, solver_type); + if (!slv) { + cfd_registry_destroy(registry); + return 0; + } + cfd_status_t status = solver_init(slv, g, ¶ms); + if (status == CFD_ERROR_UNSUPPORTED) { + solver_destroy(slv); + cfd_registry_destroy(registry); + return 0; + } + TEST_ASSERT_EQUAL(CFD_SUCCESS, status); + + for (int s = 0; s < XB_STEPS; s++) { + ns_solver_stats_t stats; + status = solver_step(slv, field, g, ¶ms, &stats); + if (status == CFD_ERROR_UNSUPPORTED) { + solver_destroy(slv); + cfd_registry_destroy(registry); + return 0; + } + TEST_ASSERT_EQUAL_MESSAGE(CFD_SUCCESS, status, solver_type); + } + + solver_destroy(slv); + cfd_registry_destroy(registry); + return 1; +} + +static void compare_solver_backends(turbulence_model_t model, const char* label) { + grid* g = grid_create(XB_NX, XB_NY, 1, 0.0, 4.0, 0.0, 2.0, 0.0, 0.0); + TEST_ASSERT_NOT_NULL(g); + grid_initialize_uniform(g); + + const char* solvers[3] = {NS_SOLVER_TYPE_RK2, NS_SOLVER_TYPE_RK2_OMP, + NS_SOLVER_TYPE_RK2_OPTIMIZED}; + const char* names[3] = {"scalar", "omp", "avx2"}; + flow_field* fields[3] = {NULL, NULL, NULL}; + int available[3] = {0, 0, 0}; + + for (int b = 0; b < 3; b++) { + fields[b] = flow_field_create(XB_NX, XB_NY, 1); + TEST_ASSERT_NOT_NULL(fields[b]); + available[b] = run_solver_case(solvers[b], model, fields[b], g); + if (!available[b]) { + printf("[%s] %s solver unavailable, skipping\n", label, names[b]); + } + } + TEST_ASSERT_TRUE_MESSAGE(available[0], "scalar reference solver unavailable"); + + size_t total = XB_NX * XB_NY; + double u_max = 0.0; + for (size_t n = 0; n < total; n++) { + double a = fabs(fields[0]->u[n]); + if (a > u_max) u_max = a; + } + TEST_ASSERT_TRUE(u_max > 0.0); + + for (int b = 1; b < 3; b++) { + if (!available[b]) continue; + double d_u = linf_diff(fields[0]->u, fields[b]->u, total) / u_max; + double d_nut = linf_diff(fields[0]->nu_t, fields[b]->nu_t, total); + printf("[%s] scalar vs %s: rel Linf u=%.2e, Linf nu_t=%.2e\n", + label, names[b], d_u, d_nut); + /* 1% relative tolerance, matching existing cross-arch solver tests */ + TEST_ASSERT_TRUE_MESSAGE(d_u < 0.01, "u differs >1% across backends"); + for (size_t n = 0; n < total; n++) { + TEST_ASSERT_TRUE(isfinite(fields[b]->nu_t[n])); + TEST_ASSERT_TRUE(fields[b]->nu_t[n] >= 0.0); + } + } + + for (int b = 0; b < 3; b++) { + flow_field_destroy(fields[b]); + } + grid_destroy(g); +} + +static void test_solver_consistency_kepsilon(void) { + compare_solver_backends(TURB_MODEL_K_EPSILON, "k-epsilon rk2"); +} + +static void test_solver_consistency_sa(void) { + compare_solver_backends(TURB_MODEL_SPALART_ALLMARAS, "SA rk2"); +} + +/* ============================================================================ */ + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_kernel_consistency_kepsilon); + RUN_TEST(test_kernel_consistency_sa); + RUN_TEST(test_solver_consistency_kepsilon); + RUN_TEST(test_solver_consistency_sa); + return UNITY_END(); +} diff --git a/tests/solvers/turbulence/test_turbulence_disabled_regression.c b/tests/solvers/turbulence/test_turbulence_disabled_regression.c new file mode 100644 index 00000000..20ebe2d1 --- /dev/null +++ b/tests/solvers/turbulence/test_turbulence_disabled_regression.c @@ -0,0 +1,124 @@ +/** + * @file test_turbulence_disabled_regression.c + * @brief Laminar solves must be bitwise unaffected by the turbulence fields + * + * With params.turb_model == TURB_MODEL_NONE (the default), the momentum + * kernels must take the original laminar code path and never read the + * turbulence arrays. Verified by poisoning the turbulence arrays with garbage + * in one field, running identical laminar solves on both, and comparing + * u/v/p bitwise. + */ + +#include "cfd/core/cfd_init.h" +#include "cfd/core/cfd_status.h" +#include "cfd/core/grid.h" +#include "cfd/solvers/navier_stokes_solver.h" +#include "unity.h" + +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +void setUp(void) { cfd_init(); } +void tearDown(void) { cfd_finalize(); } + +#define REG_NX 17 +#define REG_NY 17 +#define REG_STEPS 20 + +static void init_taylor_green(flow_field* field, const grid* g) { + for (size_t j = 0; j < field->ny; j++) { + for (size_t i = 0; i < field->nx; i++) { + size_t idx = j * field->nx + i; + double x = g->x[i], y = g->y[j]; + field->u[idx] = cos(2.0 * M_PI * x) * sin(2.0 * M_PI * y); + field->v[idx] = -sin(2.0 * M_PI * x) * cos(2.0 * M_PI * y); + field->p[idx] = 1.0; + field->rho[idx] = 1.0; + field->T[idx] = 300.0; + } + } +} + +static void run_laminar_regression(const char* solver_type) { + grid* g = grid_create(REG_NX, REG_NY, 1, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0); + TEST_ASSERT_NOT_NULL(g); + grid_initialize_uniform(g); + + flow_field* field_a = flow_field_create(REG_NX, REG_NY, 1); + flow_field* field_b = flow_field_create(REG_NX, REG_NY, 1); + TEST_ASSERT_NOT_NULL(field_a); + TEST_ASSERT_NOT_NULL(field_b); + init_taylor_green(field_a, g); + init_taylor_green(field_b, g); + + /* Poison field_b's turbulence arrays: a laminar solve must never read them */ + size_t total = REG_NX * REG_NY; + for (size_t n = 0; n < total; n++) { + field_b->turb_k[n] = 1e30; + field_b->turb_eps[n] = -1e30; + field_b->turb_nu_tilde[n] = 1e30; + field_b->nu_t[n] = 1e30; + } + + ns_solver_registry_t* registry = cfd_registry_create(); + TEST_ASSERT_NOT_NULL(registry); + cfd_registry_register_defaults(registry); + + ns_solver_t* slv_a = cfd_solver_create(registry, solver_type); + ns_solver_t* slv_b = cfd_solver_create(registry, solver_type); + TEST_ASSERT_NOT_NULL_MESSAGE(slv_a, "solver not available"); + TEST_ASSERT_NOT_NULL_MESSAGE(slv_b, "solver not available"); + + ns_solver_params_t params = ns_solver_params_default(); + params.dt = 1e-4; + params.max_iter = 1; + TEST_ASSERT_EQUAL(TURB_MODEL_NONE, params.turb_model); + + TEST_ASSERT_EQUAL(CFD_SUCCESS, solver_init(slv_a, g, ¶ms)); + TEST_ASSERT_EQUAL(CFD_SUCCESS, solver_init(slv_b, g, ¶ms)); + + for (int s = 0; s < REG_STEPS; s++) { + ns_solver_stats_t stats_a, stats_b; + TEST_ASSERT_EQUAL(CFD_SUCCESS, solver_step(slv_a, field_a, g, ¶ms, &stats_a)); + TEST_ASSERT_EQUAL(CFD_SUCCESS, solver_step(slv_b, field_b, g, ¶ms, &stats_b)); + } + + /* Bitwise identical velocity and pressure */ + TEST_ASSERT_EQUAL_MESSAGE(0, memcmp(field_a->u, field_b->u, total * sizeof(double)), + "u differs: laminar path read turbulence arrays"); + TEST_ASSERT_EQUAL_MESSAGE(0, memcmp(field_a->v, field_b->v, total * sizeof(double)), + "v differs: laminar path read turbulence arrays"); + TEST_ASSERT_EQUAL_MESSAGE(0, memcmp(field_a->p, field_b->p, total * sizeof(double)), + "p differs: laminar path read turbulence arrays"); + + solver_destroy(slv_a); + solver_destroy(slv_b); + cfd_registry_destroy(registry); + flow_field_destroy(field_a); + flow_field_destroy(field_b); + grid_destroy(g); +} + +static void test_projection_laminar_unaffected(void) { + run_laminar_regression(NS_SOLVER_TYPE_PROJECTION); +} + +static void test_rk2_laminar_unaffected(void) { + run_laminar_regression(NS_SOLVER_TYPE_RK2); +} + +static void test_explicit_euler_laminar_unaffected(void) { + run_laminar_regression(NS_SOLVER_TYPE_EXPLICIT_EULER); +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_projection_laminar_unaffected); + RUN_TEST(test_rk2_laminar_unaffected); + RUN_TEST(test_explicit_euler_laminar_unaffected); + return UNITY_END(); +} diff --git a/tests/solvers/turbulence/test_turbulence_kepsilon.c b/tests/solvers/turbulence/test_turbulence_kepsilon.c new file mode 100644 index 00000000..3e80aa80 --- /dev/null +++ b/tests/solvers/turbulence/test_turbulence_kepsilon.c @@ -0,0 +1,237 @@ +/** + * @file test_turbulence_kepsilon.c + * @brief Unit tests for the standard k-epsilon turbulence model + * + * Tests: + * 1. Decaying homogeneous turbulence: with zero velocity and uniform k/eps, + * the model reduces to dk/dt = -eps, deps/dt = -C2*eps^2/k with the + * analytical solution k(t) = k0 * (1 + (C2-1)*eps0*t/k0)^(-1/(C2-1)). + * 2. First-order temporal convergence of the decay solution. + * 3. Positivity under an abusive time step (Patankar sink treatment). + * 4. TURB_MODEL_NONE is a strict no-op. + * 5. Production term under prescribed linear shear matches P_k = nu_t*S^2. + */ + +#include "cfd/core/cfd_init.h" +#include "cfd/core/cfd_status.h" +#include "cfd/core/grid.h" +#include "cfd/solvers/navier_stokes_solver.h" +#include "cfd/solvers/turbulence_solver.h" +#include "unity.h" + +#include +#include + +/* Model constants (must match turbulence_solver_internal.h) */ +#define KE_C_MU 0.09 +#define KE_C1 1.44 +#define KE_C2 1.92 + +void setUp(void) { cfd_init(); } +void tearDown(void) { cfd_finalize(); } + +/* ============================================================================ + * Helpers + * ============================================================================ */ + +static grid* make_grid(size_t nx, size_t ny) { + grid* g = grid_create(nx, ny, 1, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0); + TEST_ASSERT_NOT_NULL(g); + grid_initialize_uniform(g); + return g; +} + +static flow_field* make_field(size_t nx, size_t ny) { + flow_field* field = flow_field_create(nx, ny, 1); + TEST_ASSERT_NOT_NULL(field); + for (size_t n = 0; n < nx * ny; n++) { + field->rho[n] = 1.0; + } + return field; +} + +/* Run the homogeneous decay case and return k at the domain center. */ +static double run_decay(double dt, int steps) { + const size_t nx = 9, ny = 9; + grid* g = make_grid(nx, ny); + flow_field* field = make_field(nx, ny); + + ns_solver_params_t params = ns_solver_params_default(); + params.mu = 1e-5; + params.turb_model = TURB_MODEL_K_EPSILON; + /* turb_bc zero-init = all PERIODIC */ + + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_init_uniform(field, ¶ms, 1.0, 1.0, 0.0)); + + for (int s = 0; s < steps; s++) { + TEST_ASSERT_EQUAL(CFD_SUCCESS, + turbulence_step_explicit(field, g, ¶ms, dt, s * dt)); + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_apply_bcs(field, g, ¶ms)); + } + + double k_center = field->turb_k[(ny / 2) * nx + nx / 2]; + + flow_field_destroy(field); + grid_destroy(g); + return k_center; +} + +static double decay_exact(double k0, double eps0, double t) { + return k0 * pow(1.0 + (KE_C2 - 1.0) * eps0 * t / k0, -1.0 / (KE_C2 - 1.0)); +} + +/* ============================================================================ + * TEST 1: analytic decay of homogeneous turbulence + * ============================================================================ */ + +static void test_homogeneous_decay(void) { + const double dt = 1e-3; + const int steps = 500; + double k_num = run_decay(dt, steps); + double k_ref = decay_exact(1.0, 1.0, steps * dt); + + TEST_ASSERT_TRUE(k_num > 0.0); + double rel_err = fabs(k_num - k_ref) / k_ref; + TEST_ASSERT_TRUE_MESSAGE(rel_err < 0.02, "decay error exceeds 2%"); +} + +/* ============================================================================ + * TEST 2: first-order temporal convergence + * ============================================================================ */ + +static void test_decay_convergence(void) { + const double t_final = 0.4; + double e_coarse = fabs(run_decay(2e-3, 200) - decay_exact(1.0, 1.0, t_final)); + double e_fine = fabs(run_decay(1e-3, 400) - decay_exact(1.0, 1.0, t_final)); + + TEST_ASSERT_TRUE(e_fine > 0.0); + double ratio = e_coarse / e_fine; + /* First-order scheme: halving dt should roughly halve the error */ + TEST_ASSERT_TRUE_MESSAGE(ratio > 1.5 && ratio < 3.0, + "decay error does not converge at first order"); +} + +/* ============================================================================ + * TEST 3: positivity under an abusive time step + * ============================================================================ */ + +static void test_positivity_abusive_dt(void) { + const size_t nx = 9, ny = 9; + grid* g = make_grid(nx, ny); + flow_field* field = make_field(nx, ny); + + ns_solver_params_t params = ns_solver_params_default(); + params.mu = 1e-5; + params.turb_model = TURB_MODEL_K_EPSILON; + + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_init_uniform(field, ¶ms, 1.0, 1.0, 0.0)); + + /* dt orders of magnitude above the source-term stability estimate */ + const double dt = 10.0; + for (int s = 0; s < 100; s++) { + TEST_ASSERT_EQUAL(CFD_SUCCESS, + turbulence_step_explicit(field, g, ¶ms, dt, s * dt)); + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_apply_bcs(field, g, ¶ms)); + } + + for (size_t n = 0; n < nx * ny; n++) { + TEST_ASSERT_TRUE(isfinite(field->turb_k[n])); + TEST_ASSERT_TRUE(isfinite(field->turb_eps[n])); + TEST_ASSERT_TRUE(field->turb_k[n] > 0.0); + TEST_ASSERT_TRUE(field->turb_eps[n] > 0.0); + } + + flow_field_destroy(field); + grid_destroy(g); +} + +/* ============================================================================ + * TEST 4: TURB_MODEL_NONE is a strict no-op + * ============================================================================ */ + +static void test_disabled_noop(void) { + const size_t nx = 9, ny = 9; + grid* g = make_grid(nx, ny); + flow_field* field = make_field(nx, ny); + + ns_solver_params_t params = ns_solver_params_default(); + TEST_ASSERT_EQUAL(TURB_MODEL_NONE, params.turb_model); + + for (size_t n = 0; n < nx * ny; n++) { + field->turb_k[n] = 5.0; + field->turb_eps[n] = 7.0; + field->nu_t[n] = 3.0; + } + + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_step_explicit(field, g, ¶ms, 0.1, 0.0)); + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_apply_bcs(field, g, ¶ms)); + + for (size_t n = 0; n < nx * ny; n++) { + TEST_ASSERT_EQUAL_DOUBLE(5.0, field->turb_k[n]); + TEST_ASSERT_EQUAL_DOUBLE(7.0, field->turb_eps[n]); + TEST_ASSERT_EQUAL_DOUBLE(3.0, field->nu_t[n]); + } + + flow_field_destroy(field); + grid_destroy(g); +} + +/* ============================================================================ + * TEST 5: production under prescribed linear shear u = S*y + * + * With uniform k/eps (no advection or diffusion of k) and central velocity + * gradients, S^2 = (du/dy)^2 and one Patankar step gives exactly + * k_new = (k + dt*P_k) / (1 + dt*eps/k), P_k = min(nu_t*S^2, 10*eps) + * ============================================================================ */ + +static void test_production_linear_shear(void) { + const size_t nx = 17, ny = 17; + const double S = 2.0; + const double dt = 1e-3; + grid* g = make_grid(nx, ny); + flow_field* field = make_field(nx, ny); + + for (size_t j = 0; j < ny; j++) { + for (size_t i = 0; i < nx; i++) { + size_t idx = j * nx + i; + field->u[idx] = S * g->y[j]; + field->v[idx] = 0.0; + } + } + + ns_solver_params_t params = ns_solver_params_default(); + params.mu = 1e-5; + params.turb_model = TURB_MODEL_K_EPSILON; + + const double k0 = 1.0, eps0 = 1.0; + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_init_uniform(field, ¶ms, k0, eps0, 0.0)); + double nu_t0 = field->nu_t[0]; + TEST_ASSERT_DOUBLE_WITHIN(1e-12, KE_C_MU * k0 * k0 / eps0, nu_t0); + + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_step_explicit(field, g, ¶ms, dt, 0.0)); + + double P_k = nu_t0 * S * S; + if (P_k > 10.0 * eps0) P_k = 10.0 * eps0; + double k_expected = (k0 + dt * P_k) / (1.0 + dt * eps0 / k0); + double e_expected = (eps0 + dt * KE_C1 * (eps0 / k0) * P_k) / + (1.0 + dt * KE_C2 * eps0 / k0); + + size_t center = (ny / 2) * nx + nx / 2; + TEST_ASSERT_DOUBLE_WITHIN(1e-12, k_expected, field->turb_k[center]); + TEST_ASSERT_DOUBLE_WITHIN(1e-12, e_expected, field->turb_eps[center]); + + flow_field_destroy(field); + grid_destroy(g); +} + +/* ============================================================================ */ + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_homogeneous_decay); + RUN_TEST(test_decay_convergence); + RUN_TEST(test_positivity_abusive_dt); + RUN_TEST(test_disabled_noop); + RUN_TEST(test_production_linear_shear); + return UNITY_END(); +} diff --git a/tests/solvers/turbulence/test_turbulence_sa.c b/tests/solvers/turbulence/test_turbulence_sa.c new file mode 100644 index 00000000..a7401961 --- /dev/null +++ b/tests/solvers/turbulence/test_turbulence_sa.c @@ -0,0 +1,277 @@ +/** + * @file test_turbulence_sa.c + * @brief Unit tests for the Spalart-Allmaras turbulence model (no-ft2 variant) + * + * Tests: + * 1. turbulence_init_uniform sets nu_t = nu_tilde * fv1(chi) exactly. + * 2. No-wall invariance: with zero velocity, uniform nu_tilde, and no + * wall-function faces, nu_tilde is constant to machine precision. + * 3. Production under prescribed linear shear (no wall): one step gives + * exactly nt_new = nt * (1 + dt*cb1*Omega). + * 4. Positivity under an abusive time step with wall destruction active. + * 5. Wall-distance helper: nearest NOSLIP face distance, has_wall flag. + */ + +#include "cfd/core/cfd_init.h" +#include "cfd/core/cfd_status.h" +#include "cfd/core/grid.h" +#include "cfd/boundary/boundary_conditions.h" +#include "cfd/solvers/navier_stokes_solver.h" +#include "cfd/solvers/turbulence_solver.h" +#include "unity.h" + +#include + +/* SA constants (must match turbulence_solver_internal.h) */ +#define SA_T_CB1 0.1355 +#define SA_T_CV1 7.1 + +/* Internal helper under test: exported from the static library but not part + * of the public API (declared in turbulence_solver_internal.h). */ +extern double turb_wall_distance(const grid* grid, const ns_turbulence_bc_config_t* tbc, + size_t i, size_t j, int* has_wall); + +void setUp(void) { cfd_init(); } +void tearDown(void) { cfd_finalize(); } + +static grid* make_grid(size_t nx, size_t ny, double lx, double ly) { + grid* g = grid_create(nx, ny, 1, 0.0, lx, 0.0, ly, 0.0, 0.0); + TEST_ASSERT_NOT_NULL(g); + grid_initialize_uniform(g); + return g; +} + +static flow_field* make_field(size_t nx, size_t ny) { + flow_field* field = flow_field_create(nx, ny, 1); + TEST_ASSERT_NOT_NULL(field); + for (size_t n = 0; n < nx * ny; n++) { + field->rho[n] = 1.0; + } + return field; +} + +/* ============================================================================ + * TEST 1: nu_t = nu_tilde * fv1(chi) + * ============================================================================ */ + +static void test_init_uniform_fv1(void) { + const size_t nx = 9, ny = 9; + grid* g = make_grid(nx, ny, 1.0, 1.0); + flow_field* field = make_field(nx, ny); + + ns_solver_params_t params = ns_solver_params_default(); + params.mu = 1e-3; + params.turb_model = TURB_MODEL_SPALART_ALLMARAS; + + const double nt0 = 3e-3; /* chi = 3 */ + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_init_uniform(field, ¶ms, 0.0, 0.0, nt0)); + + double chi = nt0 / params.mu; /* rho = 1 */ + double chi3 = chi * chi * chi; + double fv1 = chi3 / (chi3 + SA_T_CV1 * SA_T_CV1 * SA_T_CV1); + double nu_t_expected = nt0 * fv1; + + TEST_ASSERT_DOUBLE_WITHIN(1e-15, nu_t_expected, field->nu_t[0]); + TEST_ASSERT_DOUBLE_WITHIN(1e-15, nu_t_expected, field->nu_t[nx * ny - 1]); + + flow_field_destroy(field); + grid_destroy(g); +} + +/* ============================================================================ + * TEST 2: no-wall invariance + * ============================================================================ */ + +static void test_no_wall_invariance(void) { + const size_t nx = 9, ny = 9; + grid* g = make_grid(nx, ny, 1.0, 1.0); + flow_field* field = make_field(nx, ny); + + ns_solver_params_t params = ns_solver_params_default(); + params.mu = 1e-3; + params.turb_model = TURB_MODEL_SPALART_ALLMARAS; + /* all faces PERIODIC (zero-init): no wall anywhere */ + + const double nt0 = 5e-3; + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_init_uniform(field, ¶ms, 0.0, 0.0, nt0)); + + for (int s = 0; s < 100; s++) { + TEST_ASSERT_EQUAL(CFD_SUCCESS, + turbulence_step_explicit(field, g, ¶ms, 1e-2, s * 1e-2)); + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_apply_bcs(field, g, ¶ms)); + } + + for (size_t n = 0; n < nx * ny; n++) { + TEST_ASSERT_DOUBLE_WITHIN(1e-15, nt0, field->turb_nu_tilde[n]); + } + + flow_field_destroy(field); + grid_destroy(g); +} + +/* ============================================================================ + * TEST 3: production under linear shear u = S*y (no wall) + * + * Omega = |dv/dx - du/dy| = S, uniform nu_tilde: advection, diffusion, and + * the cb2 gradient term vanish; no wall means no destruction and S_tilde = + * Omega. One step is exactly nt_new = nt + dt*cb1*S*nt. + * ============================================================================ */ + +static void test_production_linear_shear(void) { + const size_t nx = 17, ny = 17; + const double S = 2.0; + const double dt = 1e-3; + grid* g = make_grid(nx, ny, 1.0, 1.0); + flow_field* field = make_field(nx, ny); + + for (size_t j = 0; j < ny; j++) { + for (size_t i = 0; i < nx; i++) { + size_t idx = j * nx + i; + field->u[idx] = S * g->y[j]; + field->v[idx] = 0.0; + } + } + + ns_solver_params_t params = ns_solver_params_default(); + params.mu = 1e-3; + params.turb_model = TURB_MODEL_SPALART_ALLMARAS; + + const double nt0 = 3e-3; + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_init_uniform(field, ¶ms, 0.0, 0.0, nt0)); + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_step_explicit(field, g, ¶ms, dt, 0.0)); + + double nt_expected = nt0 * (1.0 + dt * SA_T_CB1 * S); + size_t center = (ny / 2) * nx + nx / 2; + TEST_ASSERT_DOUBLE_WITHIN(1e-15, nt_expected, field->turb_nu_tilde[center]); + + flow_field_destroy(field); + grid_destroy(g); +} + +/* ============================================================================ + * TEST 4a: Patankar destruction cannot cross zero, even with a huge dt + * + * Zero velocity, uniform nu_tilde, wall faces configured (destruction active + * via turb_bc without applying BCs, so no gradients ever form and only the + * semi-implicit sink acts). A single step with dt = 1000 must leave nu_tilde + * positive and finite everywhere. + * ============================================================================ */ + +static void test_positivity_huge_dt_sink(void) { + const size_t nx = 9, ny = 9; + grid* g = make_grid(nx, ny, 1.0, 1.0); + flow_field* field = make_field(nx, ny); + + ns_solver_params_t params = ns_solver_params_default(); + params.mu = 1e-4; + params.turb_model = TURB_MODEL_SPALART_ALLMARAS; + params.turb_bc.bottom = BC_TYPE_NOSLIP; + params.turb_bc.top = BC_TYPE_NOSLIP; + + const double nt0 = 1e-2; + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_init_uniform(field, ¶ms, 0.0, 0.0, nt0)); + + TEST_ASSERT_EQUAL(CFD_SUCCESS, + turbulence_step_explicit(field, g, ¶ms, 1000.0, 0.0)); + + for (size_t n = 0; n < nx * ny; n++) { + TEST_ASSERT_TRUE(isfinite(field->turb_nu_tilde[n])); + TEST_ASSERT_TRUE(field->turb_nu_tilde[n] >= 0.0); + TEST_ASSERT_TRUE(field->turb_nu_tilde[n] <= nt0); + } + + flow_field_destroy(field); + grid_destroy(g); +} + +/* ============================================================================ + * TEST 4b: multi-step robustness with shear, wall functions, and a time step + * at the explicit diffusion stability margin + * ============================================================================ */ + +static void test_positivity_robustness(void) { + const size_t nx = 9, ny = 9; + grid* g = make_grid(nx, ny, 1.0, 1.0); + flow_field* field = make_field(nx, ny); + + /* Shear flow to activate production alongside wall destruction */ + for (size_t j = 0; j < ny; j++) { + for (size_t i = 0; i < nx; i++) { + field->u[j * nx + i] = 5.0 * g->y[j]; + } + } + + ns_solver_params_t params = ns_solver_params_default(); + params.mu = 1e-4; + params.turb_model = TURB_MODEL_SPALART_ALLMARAS; + params.turb_bc.bottom = BC_TYPE_NOSLIP; + params.turb_bc.top = BC_TYPE_NOSLIP; + + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_init_uniform(field, ¶ms, 0.0, 0.0, 1e-2)); + + const double dt = 0.04; + for (int s = 0; s < 500; s++) { + TEST_ASSERT_EQUAL(CFD_SUCCESS, + turbulence_step_explicit(field, g, ¶ms, dt, s * dt)); + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_apply_bcs(field, g, ¶ms)); + } + + for (size_t n = 0; n < nx * ny; n++) { + TEST_ASSERT_TRUE(isfinite(field->turb_nu_tilde[n])); + TEST_ASSERT_TRUE(field->turb_nu_tilde[n] >= 0.0); + TEST_ASSERT_TRUE(isfinite(field->nu_t[n])); + TEST_ASSERT_TRUE(field->nu_t[n] >= 0.0); + } + + flow_field_destroy(field); + grid_destroy(g); +} + +/* ============================================================================ + * TEST 5: wall-distance helper + * ============================================================================ */ + +static void test_wall_distance(void) { + const size_t nx = 11, ny = 11; + grid* g = make_grid(nx, ny, 4.0, 2.0); + + ns_turbulence_bc_config_t tbc = {0}; + int has_wall = 0; + + /* No wall anywhere */ + double d = turb_wall_distance(g, &tbc, 5, 5, &has_wall); + TEST_ASSERT_EQUAL(0, has_wall); + TEST_ASSERT_EQUAL_DOUBLE(0.0, d); + + /* Channel: bottom and top walls */ + tbc.bottom = BC_TYPE_NOSLIP; + tbc.top = BC_TYPE_NOSLIP; + d = turb_wall_distance(g, &tbc, 5, 2, &has_wall); + TEST_ASSERT_EQUAL(1, has_wall); + TEST_ASSERT_DOUBLE_WITHIN(1e-14, g->y[2] - g->y[0], d); + + d = turb_wall_distance(g, &tbc, 5, 8, &has_wall); + TEST_ASSERT_EQUAL(1, has_wall); + TEST_ASSERT_DOUBLE_WITHIN(1e-14, g->y[ny - 1] - g->y[8], d); + + /* Add a left wall closer than the y-walls */ + tbc.left = BC_TYPE_NOSLIP; + d = turb_wall_distance(g, &tbc, 1, 5, &has_wall); + TEST_ASSERT_EQUAL(1, has_wall); + TEST_ASSERT_DOUBLE_WITHIN(1e-14, g->x[1] - g->x[0], d); + + grid_destroy(g); +} + +/* ============================================================================ */ + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_init_uniform_fv1); + RUN_TEST(test_no_wall_invariance); + RUN_TEST(test_production_linear_shear); + RUN_TEST(test_positivity_huge_dt_sink); + RUN_TEST(test_positivity_robustness); + RUN_TEST(test_wall_distance); + return UNITY_END(); +} diff --git a/tests/solvers/turbulence/test_turbulence_wall_functions.c b/tests/solvers/turbulence/test_turbulence_wall_functions.c new file mode 100644 index 00000000..95c17cc3 --- /dev/null +++ b/tests/solvers/turbulence/test_turbulence_wall_functions.c @@ -0,0 +1,182 @@ +/** + * @file test_turbulence_wall_functions.c + * @brief Unit tests for the log-law wall functions + * + * Tests: + * 1. turbulence_wall_u_tau recovers a known friction velocity from a + * log-law-consistent (u_p, y_p) pair at y+ = 30 and y+ = 100. + * 2. Viscous sublayer branch (y+ = 5): u_tau = sqrt(nu*u_p/y_p). + * 3. Full BC application on a small grid: equilibrium k/eps at the first + * interior node and a wall-face viscosity that reproduces the log-law + * wall shear exactly: (nu + 0.5*(nu_t_w + nu_t_p)) * u_p/y_p = u_tau^2. + * ============================================================================ */ + +#include "cfd/core/cfd_init.h" +#include "cfd/core/cfd_status.h" +#include "cfd/core/grid.h" +#include "cfd/boundary/boundary_conditions.h" +#include "cfd/solvers/navier_stokes_solver.h" +#include "cfd/solvers/turbulence_solver.h" +#include "unity.h" + +#include + +/* Constants (must match turbulence_solver_internal.h) */ +#define WF_KAPPA 0.41 +#define WF_B 5.2 +#define WF_C_MU 0.09 + +void setUp(void) { cfd_init(); } +void tearDown(void) { cfd_finalize(); } + +/* ============================================================================ + * TEST 1: u_tau recovery in the log layer + * ============================================================================ */ + +static void check_u_tau_recovery(double ut_exact, double yplus) { + const double nu = 1.5e-5; + double y_p = yplus * nu / ut_exact; + double u_p = ut_exact * (log(yplus) / WF_KAPPA + WF_B); + + double ut = turbulence_wall_u_tau(u_p, y_p, nu); + TEST_ASSERT_DOUBLE_WITHIN(1e-8 * ut_exact, ut_exact, ut); +} + +static void test_u_tau_recovery_log_layer(void) { + check_u_tau_recovery(0.05, 30.0); + check_u_tau_recovery(0.05, 100.0); + check_u_tau_recovery(1.0, 30.0); + check_u_tau_recovery(1.0, 100.0); +} + +/* ============================================================================ + * TEST 2: viscous sublayer branch + * ============================================================================ */ + +static void test_u_tau_sublayer(void) { + const double nu = 1.5e-5; + const double ut_exact = 0.2; + const double yplus = 5.0; + double y_p = yplus * nu / ut_exact; + double u_p = ut_exact * yplus; /* linear law u+ = y+ */ + + double ut = turbulence_wall_u_tau(u_p, y_p, nu); + /* sqrt(nu*u_p/y_p) = sqrt(ut^2) = ut exactly */ + TEST_ASSERT_DOUBLE_WITHIN(1e-12, ut_exact, ut); + + /* Degenerate inputs return 0 */ + TEST_ASSERT_EQUAL_DOUBLE(0.0, turbulence_wall_u_tau(0.0, y_p, nu)); + TEST_ASSERT_EQUAL_DOUBLE(0.0, turbulence_wall_u_tau(u_p, 0.0, nu)); + TEST_ASSERT_EQUAL_DOUBLE(0.0, turbulence_wall_u_tau(u_p, y_p, 0.0)); +} + +/* ============================================================================ + * TEST 3: full wall-function BC application (bottom wall, k-epsilon) + * ============================================================================ */ + +static void test_wall_function_bc_application(void) { + const size_t nx = 8, ny = 8; + grid* g = grid_create(nx, ny, 1, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0); + TEST_ASSERT_NOT_NULL(g); + grid_initialize_uniform(g); + + flow_field* field = flow_field_create(nx, ny, 1); + TEST_ASSERT_NOT_NULL(field); + + const double u_p = 2.0; + const double mu = 1e-4; /* with rho=1: nu=1e-4, first node lands in log layer */ + for (size_t n = 0; n < nx * ny; n++) { + field->rho[n] = 1.0; + field->u[n] = u_p; + field->v[n] = 0.0; + } + + ns_solver_params_t params = ns_solver_params_default(); + params.mu = mu; + params.turb_model = TURB_MODEL_K_EPSILON; + params.turb_bc.bottom = BC_TYPE_NOSLIP; + params.turb_bc.left = BC_TYPE_NEUMANN; + params.turb_bc.right = BC_TYPE_NEUMANN; + params.turb_bc.top = BC_TYPE_NEUMANN; + + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_apply_bcs(field, g, ¶ms)); + + const double y_p = g->y[1] - g->y[0]; + const double nu = mu; /* rho = 1 */ + const double ut = turbulence_wall_u_tau(u_p, y_p, nu); + TEST_ASSERT_TRUE(ut > 0.0); + /* Sanity: this configuration must exercise the log-law branch */ + TEST_ASSERT_TRUE(ut * y_p / nu > 11.63); + + size_t i = nx / 2; + size_t idx_w = i; /* wall node (j=0) */ + size_t idx_p = i + nx; /* first interior node (j=1) */ + + /* Equilibrium values at the first interior node */ + TEST_ASSERT_DOUBLE_WITHIN(1e-10, ut * ut / sqrt(WF_C_MU), field->turb_k[idx_p]); + TEST_ASSERT_DOUBLE_WITHIN(1e-10, ut * ut * ut / (WF_KAPPA * y_p), + field->turb_eps[idx_p]); + TEST_ASSERT_EQUAL_DOUBLE(0.0, field->turb_k[idx_w]); + TEST_ASSERT_DOUBLE_WITHIN(1e-12, field->turb_eps[idx_p], field->turb_eps[idx_w]); + + /* Discrete wall shear must reproduce the log law exactly: + * (nu + 0.5*(nu_t_w + nu_t_p)) * u_p/y_p == u_tau^2 */ + double nu_face = nu + 0.5 * (field->nu_t[idx_w] + field->nu_t[idx_p]); + double shear = nu_face * u_p / y_p; + TEST_ASSERT_DOUBLE_WITHIN(1e-10, ut * ut, shear); + + flow_field_destroy(field); + grid_destroy(g); +} + +/* ============================================================================ + * TEST 4: sublayer wall function degenerates to laminar shear + * ============================================================================ */ + +static void test_wall_function_sublayer_laminar(void) { + const size_t nx = 8, ny = 8; + grid* g = grid_create(nx, ny, 1, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0); + TEST_ASSERT_NOT_NULL(g); + grid_initialize_uniform(g); + + flow_field* field = flow_field_create(nx, ny, 1); + TEST_ASSERT_NOT_NULL(field); + + /* Large viscosity puts the first node deep in the viscous sublayer */ + const double u_p = 0.01; + const double mu = 0.1; + for (size_t n = 0; n < nx * ny; n++) { + field->rho[n] = 1.0; + field->u[n] = u_p; + } + + ns_solver_params_t params = ns_solver_params_default(); + params.mu = mu; + params.turb_model = TURB_MODEL_K_EPSILON; + params.turb_bc.bottom = BC_TYPE_NOSLIP; + params.turb_bc.left = BC_TYPE_NEUMANN; + params.turb_bc.right = BC_TYPE_NEUMANN; + params.turb_bc.top = BC_TYPE_NEUMANN; + + TEST_ASSERT_EQUAL(CFD_SUCCESS, turbulence_apply_bcs(field, g, ¶ms)); + + /* In the sublayer u_tau^2 = nu*u_p/y_p, so the wall-face eddy viscosity + * must vanish (pure laminar shear) */ + size_t i = nx / 2; + TEST_ASSERT_DOUBLE_WITHIN(1e-12, 0.0, field->nu_t[i]); /* wall node */ + TEST_ASSERT_DOUBLE_WITHIN(1e-12, 0.0, field->nu_t[i + nx]); /* first interior */ + + flow_field_destroy(field); + grid_destroy(g); +} + +/* ============================================================================ */ + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_u_tau_recovery_log_layer); + RUN_TEST(test_u_tau_sublayer); + RUN_TEST(test_wall_function_bc_application); + RUN_TEST(test_wall_function_sublayer_laminar); + return UNITY_END(); +} diff --git a/tests/validation/test_turbulent_channel.c b/tests/validation/test_turbulent_channel.c new file mode 100644 index 00000000..fbe2d178 --- /dev/null +++ b/tests/validation/test_turbulent_channel.c @@ -0,0 +1,229 @@ +/** + * @file test_turbulent_channel.c + * @brief Turbulent channel flow validation for the RANS models (k-eps and SA) + * + * Fully-developed turbulent channel flow at Re_tau = 395: + * - half-height delta = 1, channel y in [0, 2], x in [0, 4] (x-uniform flow) + * - rho = 1, nu = 1/Re_tau, constant streamwise body force f_x = u_tau^2/delta + * - exact steady force balance: u_tau = sqrt(f_x * delta) = 1 + * + * The flow is streamwise-uniform, so this exercises the wall-normal RANS + * balance: 0 = d/dy[(nu + nu_t) du/dy] + f_x with log-law wall functions. + * + * Assertions (for BOTH k-epsilon and Spalart-Allmaras, projection solver): + * 1. First-node y+ lies in the wall-function validity window [30, 100] + * (guards against silent grid/parameter drift). + * 2. Recovered friction velocity within 10% of the exact value 1.0 + * (steady momentum balance — the strongest check). + * 3. u+ matches the log law ln(y+)/kappa + B within 15% for nodes with + * 30 < y+ < 0.3*Re_tau. + * 4. Velocity profile symmetric about the centerline within 2%. + */ + +#include "cfd/core/cfd_init.h" +#include "cfd/core/cfd_status.h" +#include "cfd/core/grid.h" +#include "cfd/boundary/boundary_conditions.h" +#include "cfd/solvers/navier_stokes_solver.h" +#include "cfd/solvers/turbulence_solver.h" +#include "unity.h" + +#include +#include + +void setUp(void) { cfd_init(); } +void tearDown(void) { cfd_finalize(); } + +/* Channel configuration */ +#define CH_RE_TAU 395.0 +#define CH_DELTA 1.0 +#define CH_NX 16 +#define CH_NY 21 +#define CH_LX 4.0 +#define CH_LY 2.0 +#define CH_DT 0.002 +#define CH_MIN_STEPS 5000 +#define CH_MAX_STEPS 40000 +#define CH_STEADY_TOL 1e-6 + +/* Log-law constants (must match turbulence_solver_internal.h) */ +#define CH_KAPPA 0.41 +#define CH_B 5.2 + +/* Constant streamwise body force f_x = u_tau^2/delta = 1 */ +static void channel_body_force(double x, double y, double z, double t, void* ctx, + double* su, double* sv, double* sw) { + (void)x; (void)y; (void)z; (void)t; (void)ctx; + *su = 1.0; + *sv = 0.0; + *sw = 0.0; +} + +/* Impose channel BCs directly on the boundary nodes: periodic in x, + * no-slip walls at y=0 and y=Ly (the projection solver preserves + * caller-set boundary values). */ +static void apply_channel_bc(flow_field* field) { + size_t nx = field->nx, ny = field->ny; + for (size_t j = 0; j < ny; j++) { + field->u[j * nx] = field->u[j * nx + (nx - 2)]; + field->v[j * nx] = field->v[j * nx + (nx - 2)]; + field->u[j * nx + (nx - 1)] = field->u[j * nx + 1]; + field->v[j * nx + (nx - 1)] = field->v[j * nx + 1]; + } + for (size_t i = 0; i < nx; i++) { + field->u[i] = 0.0; + field->v[i] = 0.0; + field->u[(ny - 1) * nx + i] = 0.0; + field->v[(ny - 1) * nx + i] = 0.0; + } +} + +static double compute_ke(const flow_field* field) { + double ke = 0.0; + size_t total = field->nx * field->ny; + for (size_t n = 0; n < total; n++) { + ke += field->u[n] * field->u[n] + field->v[n] * field->v[n]; + } + return 0.5 * ke; +} + +static void run_channel(turbulence_model_t model, const char* label) { + const double nu = 1.0 / CH_RE_TAU; + + grid* g = grid_create(CH_NX, CH_NY, 1, 0.0, CH_LX, 0.0, CH_LY, 0.0, 0.0); + TEST_ASSERT_NOT_NULL(g); + grid_initialize_uniform(g); + + flow_field* field = flow_field_create(CH_NX, CH_NY, 1); + TEST_ASSERT_NOT_NULL(field); + + const double y_p = g->y[1] - g->y[0]; + const double yplus_first = y_p / nu; /* u_tau = 1 */ + /* Assertion 1: wall-function validity window */ + TEST_ASSERT_TRUE_MESSAGE(yplus_first >= 30.0 && yplus_first <= 100.0, + "first-node y+ outside [30, 100]"); + + /* IC: plug profile near the expected turbulent bulk velocity, zero at walls */ + const double u_bulk0 = 15.0; + for (size_t j = 0; j < CH_NY; j++) { + for (size_t i = 0; i < CH_NX; i++) { + size_t idx = j * CH_NX + i; + double y = g->y[j]; + int at_wall = (j == 0 || j == CH_NY - 1); + field->u[idx] = at_wall ? 0.0 : u_bulk0 * (1.0 - pow(fabs(y - CH_DELTA), 8.0)); + field->v[idx] = 0.0; + field->p[idx] = 1.0; + field->rho[idx] = 1.0; + field->T[idx] = 300.0; + } + } + + ns_solver_params_t params = ns_solver_params_default(); + params.dt = CH_DT; + params.max_iter = 1; + params.mu = nu; /* rho = 1: dynamic == kinematic */ + params.source_func = channel_body_force; + params.turb_model = model; + params.turb_bc.bottom = BC_TYPE_NOSLIP; + params.turb_bc.top = BC_TYPE_NOSLIP; + /* left/right stay PERIODIC (zero-init) */ + + /* Turbulence IC: ~5% intensity of the expected bulk velocity */ + double k0 = 1.5 * pow(0.05 * u_bulk0, 2.0); + double eps0 = pow(0.09, 0.75) * pow(k0, 1.5) / (0.07 * CH_DELTA); + double nu_tilde0 = 3.0 * nu; + TEST_ASSERT_EQUAL(CFD_SUCCESS, + turbulence_init_uniform(field, ¶ms, k0, eps0, nu_tilde0)); + + ns_solver_registry_t* registry = cfd_registry_create(); + TEST_ASSERT_NOT_NULL(registry); + cfd_registry_register_defaults(registry); + ns_solver_t* slv = cfd_solver_create(registry, NS_SOLVER_TYPE_PROJECTION); + TEST_ASSERT_NOT_NULL(slv); + TEST_ASSERT_EQUAL(CFD_SUCCESS, solver_init(slv, g, ¶ms)); + + /* March to steady state (kinetic-energy residual) */ + double prev_ke = compute_ke(field); + int converged = 0; + int step = 0; + for (step = 0; step < CH_MAX_STEPS; step++) { + apply_channel_bc(field); + ns_solver_stats_t stats; + cfd_status_t status = solver_step(slv, field, g, ¶ms, &stats); + TEST_ASSERT_EQUAL_MESSAGE(CFD_SUCCESS, status, "solver step failed"); + + double ke = compute_ke(field); + double residual = fabs(ke - prev_ke) / (prev_ke + 1e-10); + prev_ke = ke; + if (residual < CH_STEADY_TOL && step > CH_MIN_STEPS) { + converged = 1; + break; + } + } + apply_channel_bc(field); + + /* Assertion 2: recovered u_tau from both walls within 10% of exact 1.0 */ + size_t i_mid = CH_NX / 2; + double u_p_bot = fabs(field->u[1 * CH_NX + i_mid]); + double u_p_top = fabs(field->u[(CH_NY - 2) * CH_NX + i_mid]); + double ut_bot = turbulence_wall_u_tau(u_p_bot, y_p, nu); + double ut_top = turbulence_wall_u_tau(u_p_top, y_p, nu); + + printf("[%s] steps=%d converged=%d u_tau_bot=%.4f u_tau_top=%.4f " + "u_p=%.3f y+=%.1f\n", + label, step, converged, ut_bot, ut_top, u_p_bot, yplus_first); + + TEST_ASSERT_TRUE_MESSAGE(fabs(ut_bot - 1.0) < 0.10, + "bottom-wall u_tau deviates >10% from force balance"); + TEST_ASSERT_TRUE_MESSAGE(fabs(ut_top - 1.0) < 0.10, + "top-wall u_tau deviates >10% from force balance"); + + /* Assertion 3: log-law profile for 30 < y+ < 0.3*Re_tau (bottom half) */ + for (size_t j = 1; j < CH_NY / 2; j++) { + double y = g->y[j]; + double yplus = y / nu; /* u_tau = 1 */ + if (yplus <= 30.0 || yplus >= 0.3 * CH_RE_TAU) { + continue; + } + double u_plus = field->u[j * CH_NX + i_mid] / ut_bot; + double u_plus_log = log(yplus) / CH_KAPPA + CH_B; + double rel_err = fabs(u_plus - u_plus_log) / u_plus_log; + printf("[%s] y+=%.1f u+=%.2f log-law=%.2f err=%.1f%%\n", + label, yplus, u_plus, u_plus_log, 100.0 * rel_err); + TEST_ASSERT_TRUE_MESSAGE(rel_err < 0.15, + "u+ deviates >15% from the log law"); + } + + /* Assertion 4: symmetry about the centerline within 2% */ + double u_max = 0.0; + for (size_t j = 0; j < CH_NY; j++) { + double u = fabs(field->u[j * CH_NX + i_mid]); + if (u > u_max) u_max = u; + } + for (size_t j = 1; j < CH_NY / 2; j++) { + double u_lo = field->u[j * CH_NX + i_mid]; + double u_hi = field->u[(CH_NY - 1 - j) * CH_NX + i_mid]; + TEST_ASSERT_TRUE_MESSAGE(fabs(u_lo - u_hi) / u_max < 0.02, + "velocity profile asymmetric >2%"); + } + + solver_destroy(slv); + cfd_registry_destroy(registry); + flow_field_destroy(field); + grid_destroy(g); +} + +static void test_channel_kepsilon(void) { + run_channel(TURB_MODEL_K_EPSILON, "k-epsilon"); +} + +static void test_channel_spalart_allmaras(void) { + run_channel(TURB_MODEL_SPALART_ALLMARAS, "SA"); +} + +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_channel_kepsilon); + RUN_TEST(test_channel_spalart_allmaras); + return UNITY_END(); +}