LCOV - code coverage report
Current view: top level - solvers/navier_stokes/avx2 - solver_rk2_avx2.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 29.6 % 27 8
Test Date: 2026-06-23 13:41:07 Functions: 50.0 % 4 2

            Line data    Source code
       1              : /**
       2              :  * @file solver_rk2_avx2.c
       3              :  * @brief RK2 (Heun's method) - AVX2 + OpenMP implementation
       4              :  *
       5              :  * AVX2-only backend. Returns CFD_ERROR_UNSUPPORTED if AVX2 is unavailable.
       6              :  * Uses persistent aligned buffers in context to avoid per-step allocation.
       7              :  *
       8              :  * SIMD strategy per row (fixed j, k):
       9              :  *   i = 1       : scalar (periodic il wrap)
      10              :  *   i = 2..nx-4 : AVX2 vectorized (interior, no wrapping)
      11              :  *   i = nx-2    : scalar (periodic ir wrap)
      12              :  *   Remainder between AVX2 end and nx-2: scalar (no wrapping)
      13              :  *
      14              :  * Algorithm: identical to scalar/OMP RK2.
      15              :  *   k1 = RHS(Q^n)
      16              :  *   Q_pred = Q^n + dt*k1
      17              :  *   k2 = RHS(Q_pred)
      18              :  *   Q^{n+1} = Q^n + (dt/2)*(k1 + k2)
      19              :  *   BCs applied to final state only.
      20              :  *
      21              :  * Branch-free 3D: when nz==1, stride_z=0 and inv_2dz/inv_dz2=0.0 cause all
      22              :  * z-terms to vanish, producing bit-identical results to the 2D code path.
      23              :  */
      24              : 
      25              : #define _POSIX_C_SOURCE 200809L
      26              : #define _ISOC11_SOURCE
      27              : #define _USE_MATH_DEFINES
      28              : 
      29              : #include "cfd/core/cfd_status.h"
      30              : #include "cfd/core/grid.h"
      31              : #include "cfd/core/indexing.h"
      32              : #include "cfd/core/logging.h"
      33              : #include "cfd/core/memory.h"
      34              : #include "cfd/solvers/navier_stokes_solver.h"
      35              : #include "cfd/solvers/energy_solver.h"
      36              : #include "../../energy/energy_solver_internal.h"
      37              : 
      38              : #include <math.h>
      39              : #include <stddef.h>
      40              : #include <stdio.h>
      41              : #include <string.h>
      42              : 
      43              : #ifndef M_PI
      44              : #define M_PI 3.14159265358979323846
      45              : #endif
      46              : 
      47              : #ifdef _OPENMP
      48              : #include <omp.h>
      49              : #endif
      50              : 
      51              : #if defined(CFD_HAS_AVX2)
      52              : #include <immintrin.h>
      53              : #define USE_AVX2 1
      54              : #else
      55              : #define USE_AVX2 0
      56              : #endif
      57              : 
      58              : /* Physical stability limits (identical to scalar/OMP RK2) */
      59              : #define MAX_DERIVATIVE_LIMIT        100.0
      60              : #define MAX_SECOND_DERIVATIVE_LIMIT 1000.0
      61              : #define MAX_VELOCITY_LIMIT          100.0
      62              : #define MAX_DIVERGENCE_LIMIT        10.0
      63              : #define PRESSURE_UPDATE_FACTOR      0.1
      64              : 
      65              : /* ============================================================================
      66              :  * CONTEXT
      67              :  * ============================================================================ */
      68              : 
      69              : typedef struct {
      70              :     double* k1_u; double* k1_v; double* k1_w; double* k1_p;
      71              :     double* k2_u; double* k2_v; double* k2_w; double* k2_p;
      72              :     double* u0;   double* v0;   double* w0;   double* p0;
      73              :     double* T_ws;    /* Reusable scratch for the energy step (avoids per-step alloc) */
      74              :     double* dx_inv;  /* 1/(2*dx[i]) for i = 0..nx-1 */
      75              :     double* dy_inv;  /* 1/(2*dy[j]) for j = 0..ny-1 */
      76              :     size_t nx, ny, nz;
      77              :     size_t stride_z;
      78              :     size_t k_start, k_end;
      79              :     double inv_2dz, inv_dz2;
      80              :     int initialized;
      81              : } rk2_avx2_context_t;
      82              : 
      83              : /* ============================================================================
      84              :  * PUBLIC API DECLARATIONS
      85              :  * ============================================================================ */
      86              : 
      87              : cfd_status_t rk2_avx2_init(ns_solver_t* solver, const grid* g,
      88              :                              const ns_solver_params_t* params);
      89              : void         rk2_avx2_destroy(ns_solver_t* solver);
      90              : cfd_status_t rk2_avx2_step(ns_solver_t* solver, flow_field* field, const grid* g,
      91              :                              const ns_solver_params_t* params, ns_solver_stats_t* stats);
      92              : 
      93              : /* ============================================================================
      94              :  * SCALAR POINT HELPER
      95              :  * Handles a single interior grid point given explicit stencil indices.
      96              :  * Used for periodic-edge columns (i=1 with il wrap, i=nx-2 with ir wrap)
      97              :  * and for scalar remainder after the AVX2 group.
      98              :  * ============================================================================ */
      99              : 
     100              : static void rk2_rhs_point(
     101              :     const double* u, const double* v, const double* w,
     102              :     const double* p, const double* rho, const double* T,
     103              :     double* rhs_u, double* rhs_v, double* rhs_w, double* rhs_p,
     104              :     const grid* g, const ns_solver_params_t* params,
     105              :     size_t idx, size_t il, size_t ir, size_t jd, size_t ju,
     106              :     size_t kd, size_t ku,
     107              :     double inv_2dz, double inv_dz2,
     108              :     size_t i, size_t j, size_t k, size_t nz,
     109              :     int iter, double dt)
     110              : {
     111              :     if (rho[idx] <= 1e-10 || fabs(g->dx[i]) < 1e-10 || fabs(g->dy[j]) < 1e-10) {
     112              :         rhs_u[idx] = 0.0;
     113              :         rhs_v[idx] = 0.0;
     114              :         rhs_w[idx] = 0.0;
     115              :         rhs_p[idx] = 0.0;
     116              :         return;
     117              :     }
     118              : 
     119              :     double dx = g->dx[i], dy = g->dy[j];
     120              :     double dx2 = dx * dx, dy2 = dy * dy;
     121              : 
     122              :     double du_dx = (u[ir] - u[il]) / (2.0 * dx);
     123              :     double du_dy = (u[ju] - u[jd]) / (2.0 * dy);
     124              :     double du_dz = (u[ku] - u[kd]) * inv_2dz;
     125              :     double dv_dx = (v[ir] - v[il]) / (2.0 * dx);
     126              :     double dv_dy = (v[ju] - v[jd]) / (2.0 * dy);
     127              :     double dv_dz = (v[ku] - v[kd]) * inv_2dz;
     128              :     double dw_dx = (w[ir] - w[il]) / (2.0 * dx);
     129              :     double dw_dy = (w[ju] - w[jd]) / (2.0 * dy);
     130              :     double dw_dz = (w[ku] - w[kd]) * inv_2dz;
     131              :     double dp_dx = (p[ir] - p[il]) / (2.0 * dx);
     132              :     double dp_dy = (p[ju] - p[jd]) / (2.0 * dy);
     133              :     double dp_dz = (p[ku] - p[kd]) * inv_2dz;
     134              : 
     135              :     double d2u_dx2 = (u[ir] - 2.0 * u[idx] + u[il]) / dx2;
     136              :     double d2u_dy2 = (u[ju] - 2.0 * u[idx] + u[jd]) / dy2;
     137              :     double d2u_dz2 = (u[ku] - 2.0 * u[idx] + u[kd]) * inv_dz2;
     138              :     double d2v_dx2 = (v[ir] - 2.0 * v[idx] + v[il]) / dx2;
     139              :     double d2v_dy2 = (v[ju] - 2.0 * v[idx] + v[jd]) / dy2;
     140              :     double d2v_dz2 = (v[ku] - 2.0 * v[idx] + v[kd]) * inv_dz2;
     141              :     double d2w_dx2 = (w[ir] - 2.0 * w[idx] + w[il]) / dx2;
     142              :     double d2w_dy2 = (w[ju] - 2.0 * w[idx] + w[jd]) / dy2;
     143              :     double d2w_dz2 = (w[ku] - 2.0 * w[idx] + w[kd]) * inv_dz2;
     144              : 
     145              :     double nu = params->mu / fmax(rho[idx], 1e-10);
     146              :     nu = fmin(nu, 1.0);
     147              : 
     148              :     du_dx = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, du_dx));
     149              :     du_dy = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, du_dy));
     150              :     du_dz = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, du_dz));
     151              :     dv_dx = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dv_dx));
     152              :     dv_dy = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dv_dy));
     153              :     dv_dz = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dv_dz));
     154              :     dw_dx = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dw_dx));
     155              :     dw_dy = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dw_dy));
     156              :     dw_dz = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dw_dz));
     157              :     dp_dx = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dp_dx));
     158              :     dp_dy = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dp_dy));
     159              :     dp_dz = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dp_dz));
     160              :     d2u_dx2 = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, d2u_dx2));
     161              :     d2u_dy2 = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, d2u_dy2));
     162              :     d2u_dz2 = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, d2u_dz2));
     163              :     d2v_dx2 = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, d2v_dx2));
     164              :     d2v_dy2 = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, d2v_dy2));
     165              :     d2v_dz2 = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, d2v_dz2));
     166              :     d2w_dx2 = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, d2w_dx2));
     167              :     d2w_dy2 = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, d2w_dy2));
     168              :     d2w_dz2 = fmax(-MAX_SECOND_DERIVATIVE_LIMIT, fmin(MAX_SECOND_DERIVATIVE_LIMIT, d2w_dz2));
     169              : 
     170              :     double source_u = 0.0, source_v = 0.0, source_w = 0.0;
     171              :     double z_coord = (nz > 1 && g->z) ? g->z[k] : 0.0;
     172              :     compute_source_terms(g->x[i], g->y[j], z_coord, iter, dt, params,
     173              :                          &source_u, &source_v, &source_w);
     174              : 
     175              :     /* Boussinesq buoyancy source (no-op when beta == 0) */
     176              :     if (T) {
     177              :         energy_compute_buoyancy(T[idx], params, &source_u, &source_v, &source_w);
     178              :     }
     179              : 
     180              :     rhs_u[idx] = -(u[idx] * du_dx) - (v[idx] * du_dy) - (w[idx] * du_dz)
     181              :                  - dp_dx / rho[idx]
     182              :                  + (nu * (d2u_dx2 + d2u_dy2 + d2u_dz2))
     183              :                  + source_u;
     184              : 
     185              :     rhs_v[idx] = -(u[idx] * dv_dx) - (v[idx] * dv_dy) - (w[idx] * dv_dz)
     186              :                  - dp_dy / rho[idx]
     187              :                  + (nu * (d2v_dx2 + d2v_dy2 + d2v_dz2))
     188              :                  + source_v;
     189              : 
     190              :     rhs_w[idx] = -(u[idx] * dw_dx) - (v[idx] * dw_dy) - (w[idx] * dw_dz)
     191              :                  - dp_dz / rho[idx]
     192              :                  + (nu * (d2w_dx2 + d2w_dy2 + d2w_dz2))
     193              :                  + source_w;
     194              : 
     195              :     double divergence = du_dx + dv_dy + dw_dz;
     196              :     divergence = fmax(-MAX_DIVERGENCE_LIMIT, fmin(MAX_DIVERGENCE_LIMIT, divergence));
     197              :     rhs_p[idx] = -PRESSURE_UPDATE_FACTOR * rho[idx] * divergence;
     198              : }
     199              : 
     200              : /* ============================================================================
     201              :  * AVX2 ROW-LEVEL RHS  (compiled only when AVX2 is available)
     202              :  * ============================================================================ */
     203              : 
     204              : #if USE_AVX2
     205              : 
     206              : static inline __m256d avx2_clamp(__m256d x, __m256d lo, __m256d hi) {
     207              :     return _mm256_max_pd(lo, _mm256_min_pd(hi, x));
     208              : }
     209              : 
     210              : /*
     211              :  * Compute RHS for one row j at z-plane k, dispatching:
     212              :  *   i=1        : scalar (periodic il wrap)
     213              :  *   i=2..nx-4  : AVX2 (4-wide, no wrapping)
     214              :  *   i=nx-2     : scalar (periodic ir wrap), plus any scalar remainder
     215              :  */
     216              : static void compute_rhs_row(
     217              :     const double* u, const double* v, const double* w,
     218              :     const double* p, const double* rho, const double* T,
     219              :     double* rhs_u, double* rhs_v, double* rhs_w, double* rhs_p,
     220              :     const rk2_avx2_context_t* ctx, const grid* g,
     221              :     const ns_solver_params_t* params, size_t j, size_t k, int iter, double dt)
     222              : {
     223              :     size_t nx       = ctx->nx;
     224              :     size_t ny       = ctx->ny;
     225              :     size_t nz       = ctx->nz;
     226              :     size_t stride_z = ctx->stride_z;
     227              :     double inv_2dz  = ctx->inv_2dz;
     228              :     double inv_dz2  = ctx->inv_dz2;
     229              : 
     230              :     size_t k_off  = k * stride_z;
     231              :     size_t j_off  = k_off + j * nx;
     232              : 
     233              :     /* Degenerate dy: zero RHS for entire row (matches scalar path) */
     234              :     if (fabs(g->dy[j]) < 1e-10) {
     235              :         return;  /* rhs arrays already memset to 0 before this call */
     236              :     }
     237              : 
     238              :     /* j-direction stencil row offsets: uniform across entire row */
     239              :     size_t jd_row = (j > 1)      ? k_off + (j - 1) * nx : k_off + (ny - 2) * nx;
     240              :     size_t ju_row = (j < ny - 2) ? k_off + (j + 1) * nx : k_off + nx;
     241              : 
     242              :     /* z-direction stencil plane offsets: uniform across entire row.
     243              :      * When nz==1: stride_z=0, so kd_off=ku_off=0 and z-terms vanish. */
     244              :     size_t kd_off = (k > 1)      ? (k - 1) * stride_z : (nz - 2) * stride_z;
     245              :     size_t ku_off = (k < nz - 2) ? (k + 1) * stride_z : 1 * stride_z;
     246              : 
     247              :     /* --- Scalar: i = 1 (il wraps periodically) --- */
     248              :     {
     249              :         size_t si  = 1;
     250              :         size_t idx = j_off + si;
     251              :         size_t il  = j_off + (nx - 2);  /* periodic wrap */
     252              :         size_t ir  = idx + 1;
     253              :         size_t kd  = kd_off + j * nx + si;
     254              :         size_t ku  = ku_off + j * nx + si;
     255              :         rk2_rhs_point(u, v, w, p, rho, T, rhs_u, rhs_v, rhs_w, rhs_p, g, params,
     256              :                        idx, il, ir, jd_row + si, ju_row + si, kd, ku,
     257              :                        inv_2dz, inv_dz2, si, j, k, nz, iter, dt);
     258              :     }
     259              : 
     260              :     /* --- AVX2: i = 2 while i+3 <= nx-3 (interior, no wrapping needed) --- */
     261              :     {
     262              :         __m256d dy_inv_v  = _mm256_set1_pd(ctx->dy_inv[j]);
     263              :         /* 1/dy^2 = 4 * (1/(2*dy))^2, derived from pre-guarded dy_inv */
     264              :         __m256d dy2_inv_v = _mm256_set1_pd(4.0 * ctx->dy_inv[j] * ctx->dy_inv[j]);
     265              :         __m256d dz_inv_v  = _mm256_set1_pd(inv_2dz);
     266              :         __m256d dz2_inv_v = _mm256_set1_pd(inv_dz2);
     267              :         __m256d two       = _mm256_set1_pd(2.0);
     268              :         __m256d four      = _mm256_set1_pd(4.0);
     269              :         __m256d max_d1    = _mm256_set1_pd( MAX_DERIVATIVE_LIMIT);
     270              :         __m256d min_d1    = _mm256_set1_pd(-MAX_DERIVATIVE_LIMIT);
     271              :         __m256d max_d2    = _mm256_set1_pd( MAX_SECOND_DERIVATIVE_LIMIT);
     272              :         __m256d min_d2    = _mm256_set1_pd(-MAX_SECOND_DERIVATIVE_LIMIT);
     273              :         __m256d max_div   = _mm256_set1_pd( MAX_DIVERGENCE_LIMIT);
     274              :         __m256d min_div   = _mm256_set1_pd(-MAX_DIVERGENCE_LIMIT);
     275              :         __m256d mu_v      = _mm256_set1_pd(params->mu);
     276              :         __m256d eps       = _mm256_set1_pd(1e-10);
     277              :         __m256d one       = _mm256_set1_pd(1.0);
     278              :         __m256d neg_puf   = _mm256_set1_pd(-PRESSURE_UPDATE_FACTOR);
     279              : 
     280              :         ptrdiff_t i;
     281              :         for (i = 2; i + 3 <= (ptrdiff_t)(nx - 3); i += 4) {
     282              :             size_t idx = j_off + (size_t)i;
     283              : 
     284              :             /* Load u stencil */
     285              :             __m256d u_c  = _mm256_loadu_pd(&u[idx]);
     286              :             __m256d u_il = _mm256_loadu_pd(&u[idx - 1]);
     287              :             __m256d u_ir = _mm256_loadu_pd(&u[idx + 1]);
     288              :             __m256d u_jd = _mm256_loadu_pd(&u[jd_row + (size_t)i]);
     289              :             __m256d u_ju = _mm256_loadu_pd(&u[ju_row + (size_t)i]);
     290              :             __m256d u_kd = _mm256_loadu_pd(&u[kd_off + j * nx + (size_t)i]);
     291              :             __m256d u_ku = _mm256_loadu_pd(&u[ku_off + j * nx + (size_t)i]);
     292              : 
     293              :             /* Load v stencil */
     294              :             __m256d v_c  = _mm256_loadu_pd(&v[idx]);
     295              :             __m256d v_il = _mm256_loadu_pd(&v[idx - 1]);
     296              :             __m256d v_ir = _mm256_loadu_pd(&v[idx + 1]);
     297              :             __m256d v_jd = _mm256_loadu_pd(&v[jd_row + (size_t)i]);
     298              :             __m256d v_ju = _mm256_loadu_pd(&v[ju_row + (size_t)i]);
     299              :             __m256d v_kd = _mm256_loadu_pd(&v[kd_off + j * nx + (size_t)i]);
     300              :             __m256d v_ku = _mm256_loadu_pd(&v[ku_off + j * nx + (size_t)i]);
     301              : 
     302              :             /* Load w stencil */
     303              :             __m256d w_c  = _mm256_loadu_pd(&w[idx]);
     304              :             __m256d w_il = _mm256_loadu_pd(&w[idx - 1]);
     305              :             __m256d w_ir = _mm256_loadu_pd(&w[idx + 1]);
     306              :             __m256d w_jd = _mm256_loadu_pd(&w[jd_row + (size_t)i]);
     307              :             __m256d w_ju = _mm256_loadu_pd(&w[ju_row + (size_t)i]);
     308              :             __m256d w_kd = _mm256_loadu_pd(&w[kd_off + j * nx + (size_t)i]);
     309              :             __m256d w_ku = _mm256_loadu_pd(&w[ku_off + j * nx + (size_t)i]);
     310              : 
     311              :             /* Load p stencil */
     312              :             __m256d p_il = _mm256_loadu_pd(&p[idx - 1]);
     313              :             __m256d p_ir = _mm256_loadu_pd(&p[idx + 1]);
     314              :             __m256d p_jd = _mm256_loadu_pd(&p[jd_row + (size_t)i]);
     315              :             __m256d p_ju = _mm256_loadu_pd(&p[ju_row + (size_t)i]);
     316              :             __m256d p_kd = _mm256_loadu_pd(&p[kd_off + j * nx + (size_t)i]);
     317              :             __m256d p_ku = _mm256_loadu_pd(&p[ku_off + j * nx + (size_t)i]);
     318              : 
     319              :             /* Density: clamp for safe division, but track validity */
     320              :             __m256d rho_raw   = _mm256_loadu_pd(&rho[idx]);
     321              :             __m256d rho_valid = _mm256_cmp_pd(rho_raw, eps, _CMP_GT_OQ);
     322              :             __m256d rho_c     = _mm256_max_pd(rho_raw, eps);
     323              :             __m256d rho_inv   = _mm256_div_pd(one, rho_c);
     324              : 
     325              :             /* dx stencil: dx_inv[i] = 1/(2*dx[i]), 0 when dx < 1e-10 */
     326              :             __m256d dx_inv_v  = _mm256_loadu_pd(&ctx->dx_inv[(size_t)i]);
     327              :             __m256d dx_valid  = _mm256_cmp_pd(dx_inv_v, _mm256_setzero_pd(), _CMP_GT_OQ);
     328              : 
     329              :             /* Combined validity: rho > eps AND dx > 0 (dy already checked at row level) */
     330              :             __m256d valid = _mm256_and_pd(rho_valid, dx_valid);
     331              :             /* 1/dx^2 = 4 * (1/(2*dx))^2 */
     332              :             __m256d dx2_inv_v = _mm256_mul_pd(four, _mm256_mul_pd(dx_inv_v, dx_inv_v));
     333              : 
     334              :             /* First derivatives — u */
     335              :             __m256d du_dx = avx2_clamp(
     336              :                 _mm256_mul_pd(_mm256_sub_pd(u_ir, u_il), dx_inv_v), min_d1, max_d1);
     337              :             __m256d du_dy = avx2_clamp(
     338              :                 _mm256_mul_pd(_mm256_sub_pd(u_ju, u_jd), dy_inv_v), min_d1, max_d1);
     339              :             __m256d du_dz = avx2_clamp(
     340              :                 _mm256_mul_pd(_mm256_sub_pd(u_ku, u_kd), dz_inv_v), min_d1, max_d1);
     341              : 
     342              :             /* First derivatives — v */
     343              :             __m256d dv_dx = avx2_clamp(
     344              :                 _mm256_mul_pd(_mm256_sub_pd(v_ir, v_il), dx_inv_v), min_d1, max_d1);
     345              :             __m256d dv_dy = avx2_clamp(
     346              :                 _mm256_mul_pd(_mm256_sub_pd(v_ju, v_jd), dy_inv_v), min_d1, max_d1);
     347              :             __m256d dv_dz = avx2_clamp(
     348              :                 _mm256_mul_pd(_mm256_sub_pd(v_ku, v_kd), dz_inv_v), min_d1, max_d1);
     349              : 
     350              :             /* First derivatives — w */
     351              :             __m256d dw_dx = avx2_clamp(
     352              :                 _mm256_mul_pd(_mm256_sub_pd(w_ir, w_il), dx_inv_v), min_d1, max_d1);
     353              :             __m256d dw_dy = avx2_clamp(
     354              :                 _mm256_mul_pd(_mm256_sub_pd(w_ju, w_jd), dy_inv_v), min_d1, max_d1);
     355              :             __m256d dw_dz = avx2_clamp(
     356              :                 _mm256_mul_pd(_mm256_sub_pd(w_ku, w_kd), dz_inv_v), min_d1, max_d1);
     357              : 
     358              :             /* Pressure gradients */
     359              :             __m256d dp_dx = avx2_clamp(
     360              :                 _mm256_mul_pd(_mm256_sub_pd(p_ir, p_il), dx_inv_v), min_d1, max_d1);
     361              :             __m256d dp_dy = avx2_clamp(
     362              :                 _mm256_mul_pd(_mm256_sub_pd(p_ju, p_jd), dy_inv_v), min_d1, max_d1);
     363              :             __m256d dp_dz = avx2_clamp(
     364              :                 _mm256_mul_pd(_mm256_sub_pd(p_ku, p_kd), dz_inv_v), min_d1, max_d1);
     365              : 
     366              :             /* Second derivatives — u */
     367              :             __m256d d2u_dx2 = avx2_clamp(
     368              :                 _mm256_mul_pd(
     369              :                     _mm256_sub_pd(_mm256_add_pd(u_ir, u_il), _mm256_mul_pd(two, u_c)),
     370              :                     dx2_inv_v),
     371              :                 min_d2, max_d2);
     372              :             __m256d d2u_dy2 = avx2_clamp(
     373              :                 _mm256_mul_pd(
     374              :                     _mm256_sub_pd(_mm256_add_pd(u_ju, u_jd), _mm256_mul_pd(two, u_c)),
     375              :                     dy2_inv_v),
     376              :                 min_d2, max_d2);
     377              :             __m256d d2u_dz2 = avx2_clamp(
     378              :                 _mm256_mul_pd(
     379              :                     _mm256_sub_pd(_mm256_add_pd(u_ku, u_kd), _mm256_mul_pd(two, u_c)),
     380              :                     dz2_inv_v),
     381              :                 min_d2, max_d2);
     382              : 
     383              :             /* Second derivatives — v */
     384              :             __m256d d2v_dx2 = avx2_clamp(
     385              :                 _mm256_mul_pd(
     386              :                     _mm256_sub_pd(_mm256_add_pd(v_ir, v_il), _mm256_mul_pd(two, v_c)),
     387              :                     dx2_inv_v),
     388              :                 min_d2, max_d2);
     389              :             __m256d d2v_dy2 = avx2_clamp(
     390              :                 _mm256_mul_pd(
     391              :                     _mm256_sub_pd(_mm256_add_pd(v_ju, v_jd), _mm256_mul_pd(two, v_c)),
     392              :                     dy2_inv_v),
     393              :                 min_d2, max_d2);
     394              :             __m256d d2v_dz2 = avx2_clamp(
     395              :                 _mm256_mul_pd(
     396              :                     _mm256_sub_pd(_mm256_add_pd(v_ku, v_kd), _mm256_mul_pd(two, v_c)),
     397              :                     dz2_inv_v),
     398              :                 min_d2, max_d2);
     399              : 
     400              :             /* Second derivatives — w */
     401              :             __m256d d2w_dx2 = avx2_clamp(
     402              :                 _mm256_mul_pd(
     403              :                     _mm256_sub_pd(_mm256_add_pd(w_ir, w_il), _mm256_mul_pd(two, w_c)),
     404              :                     dx2_inv_v),
     405              :                 min_d2, max_d2);
     406              :             __m256d d2w_dy2 = avx2_clamp(
     407              :                 _mm256_mul_pd(
     408              :                     _mm256_sub_pd(_mm256_add_pd(w_ju, w_jd), _mm256_mul_pd(two, w_c)),
     409              :                     dy2_inv_v),
     410              :                 min_d2, max_d2);
     411              :             __m256d d2w_dz2 = avx2_clamp(
     412              :                 _mm256_mul_pd(
     413              :                     _mm256_sub_pd(_mm256_add_pd(w_ku, w_kd), _mm256_mul_pd(two, w_c)),
     414              :                     dz2_inv_v),
     415              :                 min_d2, max_d2);
     416              : 
     417              :             /* Kinematic viscosity: nu = min(mu/rho, 1.0) */
     418              :             __m256d nu = _mm256_min_pd(one, _mm256_mul_pd(mu_v, rho_inv));
     419              : 
     420              :             /* Source terms: computed scalar per lane and packed */
     421              :             double src_u_arr[4] = {0.0, 0.0, 0.0, 0.0};
     422              :             double src_v_arr[4] = {0.0, 0.0, 0.0, 0.0};
     423              :             double src_w_arr[4] = {0.0, 0.0, 0.0, 0.0};
     424              :             for (int lane = 0; lane < 4; lane++) {
     425              :                 double z_coord = (nz > 1 && g->z) ? g->z[k] : 0.0;
     426              :                 compute_source_terms(g->x[(size_t)i + (size_t)lane], g->y[j],
     427              :                                      z_coord, iter, dt, params,
     428              :                                      &src_u_arr[lane], &src_v_arr[lane],
     429              :                                      &src_w_arr[lane]);
     430              :                 /* Boussinesq buoyancy source (no-op when beta == 0) */
     431              :                 if (T) {
     432              :                     energy_compute_buoyancy(T[j_off + (size_t)i + (size_t)lane], params,
     433              :                                             &src_u_arr[lane], &src_v_arr[lane],
     434              :                                             &src_w_arr[lane]);
     435              :                 }
     436              :             }
     437              :             __m256d src_u_v = _mm256_loadu_pd(src_u_arr);
     438              :             __m256d src_v_v = _mm256_loadu_pd(src_v_arr);
     439              :             __m256d src_w_v = _mm256_loadu_pd(src_w_arr);
     440              : 
     441              :             /* RHS u: nu*(d2u_dx2+d2u_dy2+d2u_dz2) - dp_dx/rho - conv_u + src_u */
     442              :             __m256d conv_u = _mm256_add_pd(
     443              :                 _mm256_add_pd(
     444              :                     _mm256_mul_pd(u_c, du_dx),
     445              :                     _mm256_mul_pd(v_c, du_dy)),
     446              :                 _mm256_mul_pd(w_c, du_dz));
     447              :             __m256d rhs_u_v = _mm256_add_pd(
     448              :                 _mm256_sub_pd(
     449              :                     _mm256_sub_pd(
     450              :                         _mm256_mul_pd(nu, _mm256_add_pd(_mm256_add_pd(d2u_dx2, d2u_dy2), d2u_dz2)),
     451              :                         _mm256_mul_pd(dp_dx, rho_inv)),
     452              :                     conv_u),
     453              :                 src_u_v);
     454              : 
     455              :             /* RHS v: nu*(d2v_dx2+d2v_dy2+d2v_dz2) - dp_dy/rho - conv_v + src_v */
     456              :             __m256d conv_v = _mm256_add_pd(
     457              :                 _mm256_add_pd(
     458              :                     _mm256_mul_pd(u_c, dv_dx),
     459              :                     _mm256_mul_pd(v_c, dv_dy)),
     460              :                 _mm256_mul_pd(w_c, dv_dz));
     461              :             __m256d rhs_v_v = _mm256_add_pd(
     462              :                 _mm256_sub_pd(
     463              :                     _mm256_sub_pd(
     464              :                         _mm256_mul_pd(nu, _mm256_add_pd(_mm256_add_pd(d2v_dx2, d2v_dy2), d2v_dz2)),
     465              :                         _mm256_mul_pd(dp_dy, rho_inv)),
     466              :                     conv_v),
     467              :                 src_v_v);
     468              : 
     469              :             /* RHS w: nu*(d2w_dx2+d2w_dy2+d2w_dz2) - dp_dz/rho - conv_w + src_w */
     470              :             __m256d conv_w = _mm256_add_pd(
     471              :                 _mm256_add_pd(
     472              :                     _mm256_mul_pd(u_c, dw_dx),
     473              :                     _mm256_mul_pd(v_c, dw_dy)),
     474              :                 _mm256_mul_pd(w_c, dw_dz));
     475              :             __m256d rhs_w_v = _mm256_add_pd(
     476              :                 _mm256_sub_pd(
     477              :                     _mm256_sub_pd(
     478              :                         _mm256_mul_pd(nu, _mm256_add_pd(_mm256_add_pd(d2w_dx2, d2w_dy2), d2w_dz2)),
     479              :                         _mm256_mul_pd(dp_dz, rho_inv)),
     480              :                     conv_w),
     481              :                 src_w_v);
     482              : 
     483              :             /* Pressure RHS: -0.1 * rho * divergence */
     484              :             __m256d divergence = avx2_clamp(
     485              :                 _mm256_add_pd(_mm256_add_pd(du_dx, dv_dy), dw_dz), min_div, max_div);
     486              :             __m256d rhs_p_v = _mm256_mul_pd(
     487              :                 neg_puf, _mm256_mul_pd(rho_c, divergence));
     488              : 
     489              :             /* Zero RHS for invalid lanes (rho <= eps or dx degenerate) */
     490              :             rhs_u_v = _mm256_and_pd(rhs_u_v, valid);
     491              :             rhs_v_v = _mm256_and_pd(rhs_v_v, valid);
     492              :             rhs_w_v = _mm256_and_pd(rhs_w_v, valid);
     493              :             rhs_p_v = _mm256_and_pd(rhs_p_v, valid);
     494              : 
     495              :             _mm256_storeu_pd(&rhs_u[idx], rhs_u_v);
     496              :             _mm256_storeu_pd(&rhs_v[idx], rhs_v_v);
     497              :             _mm256_storeu_pd(&rhs_w[idx], rhs_w_v);
     498              :             _mm256_storeu_pd(&rhs_p[idx], rhs_p_v);
     499              :         }
     500              : 
     501              :         /* --- Scalar: remainder from end of AVX2 to nx-2 (inclusive) ---
     502              :          * Handles 0-3 leftover interior points plus the periodic ir-wrap edge. */
     503              :         for (; i <= (ptrdiff_t)(nx - 2); i++) {
     504              :             size_t si  = (size_t)i;
     505              :             size_t idx = j_off + si;
     506              :             size_t il  = idx - 1;
     507              :             size_t ir  = (si == nx - 2) ? j_off + 1 : idx + 1;  /* periodic at last point */
     508              :             size_t kd  = kd_off + j * nx + si;
     509              :             size_t ku  = ku_off + j * nx + si;
     510              :             rk2_rhs_point(u, v, w, p, rho, T, rhs_u, rhs_v, rhs_w, rhs_p, g, params,
     511              :                            idx, il, ir, jd_row + si, ju_row + si, kd, ku,
     512              :                            inv_2dz, inv_dz2, si, j, k, nz, iter, dt);
     513              :         }
     514              :     }
     515              : }
     516              : 
     517              : /*
     518              :  * Compute RHS for all interior rows and z-planes.
     519              :  * k-loop is outside the OMP parallel for j-loop (MSVC OMP 2.0 compatibility).
     520              :  */
     521              : static void compute_rhs_avx2(
     522              :     const double* u, const double* v, const double* w,
     523              :     const double* p, const double* rho, const double* T,
     524              :     double* rhs_u, double* rhs_v, double* rhs_w, double* rhs_p,
     525              :     const rk2_avx2_context_t* ctx, const grid* g,
     526              :     const ns_solver_params_t* params, int iter, double dt)
     527              : {
     528              :     ptrdiff_t ny_int = (ptrdiff_t)ctx->ny;
     529              :     for (size_t k = ctx->k_start; k < ctx->k_end; k++) {
     530              :         ptrdiff_t j;
     531              : #ifdef _OPENMP
     532              :         #pragma omp parallel for schedule(static)
     533              : #endif
     534              :         for (j = 1; j < ny_int - 1; j++) {
     535              :             compute_rhs_row(u, v, w, p, rho, T, rhs_u, rhs_v, rhs_w, rhs_p,
     536              :                             ctx, g, params, (size_t)j, k, iter, dt);
     537              :         }
     538              :     }
     539              : }
     540              : 
     541              : /* ============================================================================
     542              :  * RK2 AVX2 MAIN LOOP
     543              :  * ============================================================================ */
     544              : 
     545              : static cfd_status_t rk2_avx2_impl(flow_field* field, rk2_avx2_context_t* ctx,
     546              :                                     const grid* g, const ns_solver_params_t* params)
     547              : {
     548              :     size_t n     = ctx->nx * ctx->ny * ctx->nz;
     549              :     size_t bytes = n * sizeof(double);
     550              :     double dt    = params->dt;
     551              :     ptrdiff_t n_int = (ptrdiff_t)n;
     552              :     cfd_status_t status = CFD_SUCCESS;
     553              : 
     554              :     for (int iter = 0; iter < params->max_iter; iter++) {
     555              :         /* Save Q^n */
     556              :         memcpy(ctx->u0, field->u, bytes);
     557              :         memcpy(ctx->v0, field->v, bytes);
     558              :         memcpy(ctx->w0, field->w, bytes);
     559              :         memcpy(ctx->p0, field->p, bytes);
     560              : 
     561              :         /* ---- Stage 1: k1 = RHS(Q^n) ---- */
     562              :         memset(ctx->k1_u, 0, bytes);
     563              :         memset(ctx->k1_v, 0, bytes);
     564              :         memset(ctx->k1_w, 0, bytes);
     565              :         memset(ctx->k1_p, 0, bytes);
     566              :         compute_rhs_avx2(field->u, field->v, field->w, field->p, field->rho, field->T,
     567              :                          ctx->k1_u, ctx->k1_v, ctx->k1_w, ctx->k1_p,
     568              :                          ctx, g, params, iter, dt);
     569              : 
     570              :         /* ---- Intermediate: field = Q^n + dt*k1 ---- */
     571              :         {
     572              :             ptrdiff_t k;
     573              : #ifdef _OPENMP
     574              :             #pragma omp parallel for schedule(static)
     575              : #endif
     576              :             for (k = 0; k < n_int; k++) {
     577              :                 field->u[k] = fmax(-MAX_VELOCITY_LIMIT,
     578              :                               fmin( MAX_VELOCITY_LIMIT,
     579              :                                     ctx->u0[k] + dt * ctx->k1_u[k]));
     580              :                 field->v[k] = fmax(-MAX_VELOCITY_LIMIT,
     581              :                               fmin( MAX_VELOCITY_LIMIT,
     582              :                                     ctx->v0[k] + dt * ctx->k1_v[k]));
     583              :                 field->w[k] = fmax(-MAX_VELOCITY_LIMIT,
     584              :                               fmin( MAX_VELOCITY_LIMIT,
     585              :                                     ctx->w0[k] + dt * ctx->k1_w[k]));
     586              :                 field->p[k] = ctx->p0[k] + dt * ctx->k1_p[k];
     587              :             }
     588              :         }
     589              : 
     590              :         /* NOTE: Do NOT apply BCs between RK stages.
     591              :          * The ghost cells carry zero-derivative evolution (k1[ghost]=0),
     592              :          * consistent with the semi-discrete ODE. Applying BCs here would
     593              :          * reduce RK2 to first-order temporal accuracy. */
     594              : 
     595              :         /* ---- Stage 2: k2 = RHS(Q_pred) ---- */
     596              :         memset(ctx->k2_u, 0, bytes);
     597              :         memset(ctx->k2_v, 0, bytes);
     598              :         memset(ctx->k2_w, 0, bytes);
     599              :         memset(ctx->k2_p, 0, bytes);
     600              :         compute_rhs_avx2(field->u, field->v, field->w, field->p, field->rho, field->T,
     601              :                          ctx->k2_u, ctx->k2_v, ctx->k2_w, ctx->k2_p,
     602              :                          ctx, g, params, iter, dt);
     603              : 
     604              :         /* ---- Final update: Q^{n+1} = Q^n + (dt/2)*(k1 + k2) ---- */
     605              :         {
     606              :             double half_dt = 0.5 * dt;
     607              :             ptrdiff_t k;
     608              : #ifdef _OPENMP
     609              :             #pragma omp parallel for schedule(static)
     610              : #endif
     611              :             for (k = 0; k < n_int; k++) {
     612              :                 field->u[k] = fmax(-MAX_VELOCITY_LIMIT,
     613              :                               fmin( MAX_VELOCITY_LIMIT,
     614              :                                     ctx->u0[k] + half_dt * (ctx->k1_u[k] + ctx->k2_u[k])));
     615              :                 field->v[k] = fmax(-MAX_VELOCITY_LIMIT,
     616              :                               fmin( MAX_VELOCITY_LIMIT,
     617              :                                     ctx->v0[k] + half_dt * (ctx->k1_v[k] + ctx->k2_v[k])));
     618              :                 field->w[k] = fmax(-MAX_VELOCITY_LIMIT,
     619              :                               fmin( MAX_VELOCITY_LIMIT,
     620              :                                     ctx->w0[k] + half_dt * (ctx->k1_w[k] + ctx->k2_w[k])));
     621              :                 field->p[k] = ctx->p0[k] + half_dt * (ctx->k1_p[k] + ctx->k2_p[k]);
     622              :             }
     623              :         }
     624              : 
     625              :         /* Energy equation: advance temperature after the full RK2 step */
     626              :         {
     627              :             cfd_status_t energy_status = energy_step_explicit_avx2_with_workspace(
     628              :                 field, g, params, dt, iter * dt, ctx->T_ws, n);
     629              :             if (energy_status != CFD_SUCCESS) {
     630              :                 status = energy_status;
     631              :                 goto cleanup;
     632              :             }
     633              :         }
     634              : 
     635              :         /* Apply BCs to final state only, then configured thermal BCs */
     636              :         apply_boundary_conditions(field, g);
     637              :         status = energy_apply_thermal_bcs(field, params);
     638              :         if (status != CFD_SUCCESS) {
     639              :             goto cleanup;
     640              :         }
     641              : 
     642              :         /* NaN / Inf check (parallelized) */
     643              :         {
     644              :             int has_nan = 0;
     645              :             ptrdiff_t k;
     646              : #ifdef _OPENMP
     647              :             #pragma omp parallel for reduction(|:has_nan) schedule(static)
     648              : #endif
     649              :             for (k = 0; k < n_int; k++) {
     650              :                 if (!isfinite(field->u[k]) || !isfinite(field->v[k]) ||
     651              :                     !isfinite(field->w[k]) || !isfinite(field->p[k])) {
     652              :                     has_nan = 1;
     653              :                 }
     654              :             }
     655              :             if (has_nan) {
     656              :                 status = CFD_ERROR_DIVERGED;
     657              :                 goto cleanup;
     658              :             }
     659              :         }
     660              :     }
     661              : 
     662              : cleanup:
     663              :     return status;
     664              : }
     665              : 
     666              : #endif /* USE_AVX2 */
     667              : 
     668              : /* ============================================================================
     669              :  * PUBLIC API IMPLEMENTATIONS
     670              :  * ============================================================================ */
     671              : 
     672            7 : cfd_status_t rk2_avx2_init(ns_solver_t* solver, const grid* g,
     673              :                              const ns_solver_params_t* params)
     674              : {
     675            7 :     (void)params;
     676              : 
     677              : #if !USE_AVX2
     678            7 :     (void)solver;
     679            7 :     (void)g;
     680            7 :     cfd_set_error(CFD_ERROR_UNSUPPORTED, "AVX2 not available in this build");
     681            7 :     return CFD_ERROR_UNSUPPORTED;
     682              : #else
     683              :     if (!solver || !g) {
     684              :         return CFD_ERROR_INVALID;
     685              :     }
     686              :     if (g->nx < 3 || g->ny < 3 || (g->nz > 1 && g->nz < 3)) {
     687              :         return CFD_ERROR_INVALID;
     688              :     }
     689              : 
     690              :     rk2_avx2_context_t* ctx =
     691              :         (rk2_avx2_context_t*)cfd_calloc(1, sizeof(rk2_avx2_context_t));
     692              :     if (!ctx) {
     693              :         return CFD_ERROR_NOMEM;
     694              :     }
     695              : 
     696              :     ctx->nx = g->nx;
     697              :     ctx->ny = g->ny;
     698              :     ctx->nz = g->nz;
     699              :     size_t bytes = ctx->nx * ctx->ny * g->nz * sizeof(double);
     700              : 
     701              :     /* Reject non-uniform z-spacing (solver uses constant inv_2dz/inv_dz2) */
     702              :     if (g->nz > 1 && g->dz) {
     703              :         for (size_t kk = 1; kk < g->nz - 1; kk++) {
     704              :             if (fabs(g->dz[kk] - g->dz[0]) > 1e-14) {
     705              :                 cfd_free(ctx);
     706              :                 return CFD_ERROR_INVALID;
     707              :             }
     708              :         }
     709              :     }
     710              : 
     711              :     /* Branch-free 3D constants */
     712              :     size_t plane   = ctx->nx * ctx->ny;
     713              :     ctx->stride_z  = (g->nz > 1) ? plane : 0;
     714              :     ctx->k_start   = (g->nz > 1) ? 1 : 0;
     715              :     ctx->k_end     = (g->nz > 1) ? (g->nz - 1) : 1;
     716              :     ctx->inv_2dz   = (g->nz > 1 && g->dz) ? 1.0 / (2.0 * g->dz[0]) : 0.0;
     717              :     ctx->inv_dz2   = (g->nz > 1 && g->dz) ? 1.0 / (g->dz[0] * g->dz[0]) : 0.0;
     718              : 
     719              :     ctx->k1_u   = (double*)cfd_aligned_malloc(bytes);
     720              :     ctx->k1_v   = (double*)cfd_aligned_malloc(bytes);
     721              :     ctx->k1_w   = (double*)cfd_aligned_malloc(bytes);
     722              :     ctx->k1_p   = (double*)cfd_aligned_malloc(bytes);
     723              :     ctx->k2_u   = (double*)cfd_aligned_malloc(bytes);
     724              :     ctx->k2_v   = (double*)cfd_aligned_malloc(bytes);
     725              :     ctx->k2_w   = (double*)cfd_aligned_malloc(bytes);
     726              :     ctx->k2_p   = (double*)cfd_aligned_malloc(bytes);
     727              :     ctx->u0     = (double*)cfd_aligned_malloc(bytes);
     728              :     ctx->v0     = (double*)cfd_aligned_malloc(bytes);
     729              :     ctx->w0     = (double*)cfd_aligned_malloc(bytes);
     730              :     ctx->p0     = (double*)cfd_aligned_malloc(bytes);
     731              :     ctx->T_ws   = (double*)cfd_aligned_malloc(bytes);
     732              :     ctx->dx_inv = (double*)cfd_aligned_malloc(ctx->nx * sizeof(double));
     733              :     ctx->dy_inv = (double*)cfd_aligned_malloc(ctx->ny * sizeof(double));
     734              : 
     735              :     if (!ctx->k1_u || !ctx->k1_v || !ctx->k1_w || !ctx->k1_p ||
     736              :         !ctx->k2_u || !ctx->k2_v || !ctx->k2_w || !ctx->k2_p ||
     737              :         !ctx->u0   || !ctx->v0   || !ctx->w0   || !ctx->p0   ||
     738              :         !ctx->T_ws || !ctx->dx_inv || !ctx->dy_inv) {
     739              :         cfd_aligned_free(ctx->k1_u); cfd_aligned_free(ctx->k1_v);
     740              :         cfd_aligned_free(ctx->k1_w); cfd_aligned_free(ctx->k1_p);
     741              :         cfd_aligned_free(ctx->k2_u); cfd_aligned_free(ctx->k2_v);
     742              :         cfd_aligned_free(ctx->k2_w); cfd_aligned_free(ctx->k2_p);
     743              :         cfd_aligned_free(ctx->u0);   cfd_aligned_free(ctx->v0);
     744              :         cfd_aligned_free(ctx->w0);   cfd_aligned_free(ctx->p0);
     745              :         cfd_aligned_free(ctx->T_ws);
     746              :         cfd_aligned_free(ctx->dx_inv);
     747              :         cfd_aligned_free(ctx->dy_inv);
     748              :         cfd_free(ctx);
     749              :         return CFD_ERROR_NOMEM;
     750              :     }
     751              : 
     752              :     for (size_t i = 0; i < ctx->nx; i++) {
     753              :         ctx->dx_inv[i] = (g->dx[i] > 1e-10) ? 1.0 / (2.0 * g->dx[i]) : 0.0;
     754              :     }
     755              :     for (size_t j = 0; j < ctx->ny; j++) {
     756              :         ctx->dy_inv[j] = (g->dy[j] > 1e-10) ? 1.0 / (2.0 * g->dy[j]) : 0.0;
     757              :     }
     758              : 
     759              :     ctx->initialized = 1;
     760              :     solver->context  = ctx;
     761              : 
     762              : #ifdef _OPENMP
     763              :     CFD_LOG_INFO("solver", "RK2 SIMD: AVX2 + OpenMP enabled (%d threads)", omp_get_max_threads());
     764              : #else
     765              :     CFD_LOG_INFO("solver", "RK2 SIMD: AVX2 enabled (OpenMP disabled)");
     766              : #endif
     767              : 
     768              :     return CFD_SUCCESS;
     769              : #endif
     770              : }
     771              : 
     772            7 : void rk2_avx2_destroy(ns_solver_t* solver)
     773              : {
     774            7 :     if (!solver || !solver->context) {
     775              :         return;
     776              :     }
     777            0 :     rk2_avx2_context_t* ctx = (rk2_avx2_context_t*)solver->context;
     778            0 :     if (ctx->initialized) {
     779            0 :         cfd_aligned_free(ctx->k1_u); cfd_aligned_free(ctx->k1_v);
     780            0 :         cfd_aligned_free(ctx->k1_w); cfd_aligned_free(ctx->k1_p);
     781            0 :         cfd_aligned_free(ctx->k2_u); cfd_aligned_free(ctx->k2_v);
     782            0 :         cfd_aligned_free(ctx->k2_w); cfd_aligned_free(ctx->k2_p);
     783            0 :         cfd_aligned_free(ctx->u0);   cfd_aligned_free(ctx->v0);
     784            0 :         cfd_aligned_free(ctx->w0);   cfd_aligned_free(ctx->p0);
     785            0 :         cfd_aligned_free(ctx->T_ws);
     786            0 :         cfd_aligned_free(ctx->dx_inv);
     787            0 :         cfd_aligned_free(ctx->dy_inv);
     788              :     }
     789            0 :     cfd_free(ctx);
     790            0 :     solver->context = NULL;
     791              : }
     792              : 
     793            0 : cfd_status_t rk2_avx2_step(ns_solver_t* solver, flow_field* field, const grid* g,
     794              :                              const ns_solver_params_t* params, ns_solver_stats_t* stats)
     795              : {
     796              : #if !USE_AVX2
     797            0 :     (void)solver; (void)field; (void)g; (void)params; (void)stats;
     798            0 :     return CFD_ERROR_UNSUPPORTED;
     799              : #else
     800              :     if (!solver || !solver->context || !field || !g || !params) {
     801              :         return CFD_ERROR_INVALID;
     802              :     }
     803              :     if (field->nx < 3 || field->ny < 3 || (field->nz > 1 && field->nz < 3)) {
     804              :         return CFD_ERROR_INVALID;
     805              :     }
     806              : 
     807              :     rk2_avx2_context_t* ctx = (rk2_avx2_context_t*)solver->context;
     808              :     if (field->nx != ctx->nx || field->ny != ctx->ny || field->nz != ctx->nz) {
     809              :         return CFD_ERROR_INVALID;
     810              :     }
     811              : 
     812              :     ns_solver_params_t step_params = *params;
     813              :     step_params.max_iter = 1;
     814              : 
     815              :     cfd_status_t status = rk2_avx2_impl(field, ctx, g, &step_params);
     816              : 
     817              :     if (stats) {
     818              :         stats->iterations = 1;
     819              :         double max_vel = 0.0, max_p = 0.0;
     820              :         ptrdiff_t n_s = (ptrdiff_t)(field->nx * field->ny * field->nz);
     821              :         double max_t = (field->T && n_s > 0) ? field->T[0] : 0.0;
     822              :         ptrdiff_t ks;
     823              : #if defined(_OPENMP) && (_OPENMP >= 201107)
     824              :         #pragma omp parallel for reduction(max: max_vel, max_p, max_t) schedule(static)
     825              : #endif
     826              :         for (ks = 0; ks < n_s; ks++) {
     827              :             double vel = sqrt(field->u[ks] * field->u[ks] +
     828              :                               field->v[ks] * field->v[ks] +
     829              :                               field->w[ks] * field->w[ks]);
     830              :             if (vel > max_vel) max_vel = vel;
     831              :             double ap = fabs(field->p[ks]);
     832              :             if (ap > max_p) max_p = ap;
     833              :             if (field->T && field->T[ks] > max_t) max_t = field->T[ks];
     834              :         }
     835              :         stats->max_velocity = max_vel;
     836              :         stats->max_pressure = max_p;
     837              :         stats->max_temperature = max_t;
     838              :     }
     839              : 
     840              :     return status;
     841              : #endif
     842              : }
     843              : 
     844            0 : cfd_status_t rk2_avx2_solve(ns_solver_t* solver, flow_field* field, const grid* g,
     845              :                               const ns_solver_params_t* params, ns_solver_stats_t* stats)
     846              : {
     847              : #if !USE_AVX2
     848            0 :     (void)solver; (void)field; (void)g; (void)params; (void)stats;
     849            0 :     return CFD_ERROR_UNSUPPORTED;
     850              : #else
     851              :     if (!solver || !solver->context || !field || !g || !params) {
     852              :         return CFD_ERROR_INVALID;
     853              :     }
     854              :     if (field->nx < 3 || field->ny < 3 || (field->nz > 1 && field->nz < 3)) {
     855              :         return CFD_ERROR_INVALID;
     856              :     }
     857              : 
     858              :     rk2_avx2_context_t* ctx = (rk2_avx2_context_t*)solver->context;
     859              :     if (field->nx != ctx->nx || field->ny != ctx->ny || field->nz != ctx->nz) {
     860              :         return CFD_ERROR_INVALID;
     861              :     }
     862              : 
     863              :     /* Call impl directly so its internal loop runs iter=0..max_iter-1,
     864              :      * giving compute_source_terms the correct iteration index. */
     865              :     cfd_status_t status = rk2_avx2_impl(field, ctx, g, params);
     866              : 
     867              :     if (stats) {
     868              :         stats->iterations = params->max_iter;
     869              :         double max_vel = 0.0, max_p = 0.0;
     870              :         ptrdiff_t n_s = (ptrdiff_t)(field->nx * field->ny * field->nz);
     871              :         double max_t = (field->T && n_s > 0) ? field->T[0] : 0.0;
     872              :         ptrdiff_t ks;
     873              : #if defined(_OPENMP) && (_OPENMP >= 201107)
     874              :         #pragma omp parallel for reduction(max: max_vel, max_p, max_t) schedule(static)
     875              : #endif
     876              :         for (ks = 0; ks < n_s; ks++) {
     877              :             double vel = sqrt(field->u[ks] * field->u[ks] +
     878              :                               field->v[ks] * field->v[ks] +
     879              :                               field->w[ks] * field->w[ks]);
     880              :             if (vel > max_vel) max_vel = vel;
     881              :             double ap = fabs(field->p[ks]);
     882              :             if (ap > max_p) max_p = ap;
     883              :             if (field->T && field->T[ks] > max_t) max_t = field->T[ks];
     884              :         }
     885              :         stats->max_velocity = max_vel;
     886              :         stats->max_pressure = max_p;
     887              :         stats->max_temperature = max_t;
     888              :     }
     889              :     return status;
     890              : #endif
     891              : }
        

Generated by: LCOV version 2.0-1