LCOV - code coverage report
Current view: top level - solvers/navier_stokes/avx2 - solver_explicit_euler_avx2.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 88.0 % 184 162
Test Date: 2026-06-23 13:41:07 Functions: 100.0 % 4 4

            Line data    Source code
       1              : /**
       2              :  * Optimized Explicit Euler NSSolver with SIMD + OpenMP
       3              :  *
       4              :  * This implementation combines SIMD vectorization (AVX2) with OpenMP
       5              :  * parallelization for maximum performance on multi-core CPUs.
       6              :  *
       7              :  * - Outer loops are parallelized with OpenMP
       8              :  * - Inner loops use AVX2 SIMD intrinsics for vectorization
       9              :  *
      10              :  * Note: When AVX2 is not enabled at compile time (CFD_ENABLE_AVX2=OFF),
      11              :  * this solver uses scalar code paths. Use the base explicit_euler solver
      12              :  * for guaranteed scalar-only execution.
      13              :  */
      14              : 
      15              : // Enable C11 features for aligned_alloc
      16              : #define _POSIX_C_SOURCE 200809L
      17              : #define _ISOC11_SOURCE
      18              : #define _USE_MATH_DEFINES
      19              : 
      20              : #include "cfd/core/cfd_status.h"
      21              : #include "cfd/core/grid.h"
      22              : #include "cfd/core/indexing.h"
      23              : #include "cfd/core/logging.h"
      24              : #include "cfd/core/memory.h"
      25              : #include "cfd/solvers/navier_stokes_solver.h"
      26              : #include "cfd/solvers/energy_solver.h"
      27              : #include "../../energy/energy_solver_internal.h"
      28              : 
      29              : #include "../boundary_copy_utils.h"
      30              : 
      31              : #include <math.h>
      32              : 
      33              : #ifndef M_PI
      34              : #define M_PI 3.14159265358979323846
      35              : #endif
      36              : #include <stdio.h>
      37              : #include <stdlib.h>
      38              : #include <string.h>
      39              : 
      40              : #ifdef _OPENMP
      41              : #include <omp.h>
      42              : #endif
      43              : 
      44              : /* AVX2 detection
      45              :  * CFD_HAS_AVX2 is set by CMake when -DCFD_ENABLE_AVX2=ON.
      46              :  * This works consistently across all compilers (GCC, Clang, MSVC).
      47              :  */
      48              : #if defined(CFD_HAS_AVX2)
      49              : #include <immintrin.h>
      50              : #define USE_AVX 1
      51              : #else
      52              : #define USE_AVX 0
      53              : #endif
      54              : 
      55              : // Physical stability limits
      56              : #define MAX_DERIVATIVE_LIMIT        100.0
      57              : #define MAX_SECOND_DERIVATIVE_LIMIT 1000.0
      58              : #define MAX_VELOCITY_LIMIT          100.0
      59              : #define MAX_DIVERGENCE_LIMIT        10.0
      60              : #define DT_CONSERVATIVE_LIMIT       0.0001
      61              : #define UPDATE_LIMIT                1.0
      62              : #define PRESSURE_UPDATE_FACTOR      0.1
      63              : 
      64              : typedef struct {
      65              :     double* u_new;
      66              :     double* v_new;
      67              :     double* w_new;
      68              :     double* p_new;
      69              :     double* T_ws;  /* Reusable scratch for the energy step (avoids per-step alloc) */
      70              :     double* dx_inv;
      71              :     double* dy_inv;
      72              :     size_t nx;
      73              :     size_t ny;
      74              :     size_t nz;
      75              :     size_t stride_z;
      76              :     size_t k_start;
      77              :     size_t k_end;
      78              :     double inv_2dz;
      79              :     double inv_dz2;
      80              :     int initialized;
      81              :     int iter_count;  /* Step counter for advancing time-dependent thermal terms */
      82              : } explicit_euler_simd_context;
      83              : 
      84              : // Public API functions
      85              : cfd_status_t explicit_euler_simd_init(struct NSSolver* solver, const grid* grid,
      86              :                                       const ns_solver_params_t* params);
      87              : void explicit_euler_simd_destroy(struct NSSolver* solver);
      88              : cfd_status_t explicit_euler_simd_step(struct NSSolver* solver, flow_field* field, const grid* grid,
      89              :                                       const ns_solver_params_t* params, ns_solver_stats_t* stats);
      90              : 
      91           11 : cfd_status_t explicit_euler_simd_init(struct NSSolver* solver, const grid* grid,
      92              :                                       const ns_solver_params_t* params) {
      93           11 :     (void)params;  // Unused
      94           11 :     if (!solver || !grid) {
      95              :         return CFD_ERROR_INVALID;
      96              :     }
      97           11 :     if (grid->nx < 3 || grid->ny < 3 || (grid->nz > 1 && grid->nz < 3)) {
      98              :         return CFD_ERROR_INVALID;
      99              :     }
     100              : 
     101           11 :     explicit_euler_simd_context* ctx =
     102           11 :         (explicit_euler_simd_context*)cfd_calloc(1, sizeof(explicit_euler_simd_context));
     103           11 :     if (!ctx) {
     104              :         return CFD_ERROR_NOMEM;
     105              :     }
     106              : 
     107           11 :     ctx->nx = grid->nx;
     108           11 :     ctx->ny = grid->ny;
     109           11 :     ctx->nz = grid->nz;
     110           11 :     size_t field_size = ctx->nx * ctx->ny * ctx->nz * sizeof(double);
     111              : 
     112              :     /* Reject non-uniform z-spacing (solver uses constant inv_2dz/inv_dz2) */
     113           11 :     if (grid->nz > 1 && grid->dz) {
     114           14 :         for (size_t kk = 1; kk < grid->nz - 1; kk++) {
     115           12 :             if (fabs(grid->dz[kk] - grid->dz[0]) > 1e-14) {
     116            0 :                 cfd_free(ctx);
     117            0 :                 return CFD_ERROR_INVALID;
     118              :             }
     119              :         }
     120              :     }
     121              : 
     122           11 :     size_t plane = ctx->nx * ctx->ny;
     123           11 :     ctx->stride_z = (grid->nz > 1) ? plane : 0;
     124           11 :     ctx->k_start  = (grid->nz > 1) ? 1 : 0;
     125           11 :     ctx->k_end    = (grid->nz > 1) ? (grid->nz - 1) : 1;
     126           11 :     ctx->inv_2dz  = (grid->nz > 1 && grid->dz) ? 1.0 / (2.0 * grid->dz[0]) : 0.0;
     127           11 :     ctx->inv_dz2  = (grid->nz > 1 && grid->dz) ? 1.0 / (grid->dz[0] * grid->dz[0]) : 0.0;
     128              : 
     129           11 :     ctx->u_new = (double*)cfd_aligned_malloc(field_size);
     130           11 :     ctx->v_new = (double*)cfd_aligned_malloc(field_size);
     131           11 :     ctx->w_new = (double*)cfd_aligned_malloc(field_size);
     132           11 :     ctx->p_new = (double*)cfd_aligned_malloc(field_size);
     133           11 :     ctx->T_ws  = (double*)cfd_aligned_malloc(field_size);
     134           11 :     ctx->dx_inv = (double*)cfd_aligned_malloc(ctx->nx * sizeof(double));
     135           11 :     ctx->dy_inv = (double*)cfd_aligned_malloc(ctx->ny * sizeof(double));
     136              : 
     137           11 :     if (!ctx->u_new || !ctx->v_new || !ctx->w_new || !ctx->p_new || !ctx->T_ws ||
     138           11 :         !ctx->dx_inv || !ctx->dy_inv) {
     139            0 :         if (ctx->u_new) {
     140            0 :             cfd_aligned_free(ctx->u_new);
     141              :         }
     142            0 :         if (ctx->v_new) {
     143            0 :             cfd_aligned_free(ctx->v_new);
     144              :         }
     145            0 :         if (ctx->w_new) {
     146            0 :             cfd_aligned_free(ctx->w_new);
     147              :         }
     148            0 :         if (ctx->p_new) {
     149            0 :             cfd_aligned_free(ctx->p_new);
     150              :         }
     151            0 :         if (ctx->T_ws) {
     152            0 :             cfd_aligned_free(ctx->T_ws);
     153              :         }
     154            0 :         if (ctx->dx_inv) {
     155            0 :             cfd_aligned_free(ctx->dx_inv);
     156              :         }
     157            0 :         if (ctx->dy_inv) {
     158            0 :             cfd_aligned_free(ctx->dy_inv);
     159              :         }
     160            0 :         cfd_free(ctx);
     161            0 :         return CFD_ERROR_NOMEM;
     162              :     }
     163              : 
     164              :     // Pre-compute inverses
     165          273 :     for (size_t i = 0; i < ctx->nx; i++) {
     166          262 :         ctx->dx_inv[i] = (i < ctx->nx - 1) ? 1.0 / (2.0 * grid->dx[i]) : 0.0;
     167              :     }
     168          271 :     for (size_t j = 0; j < ctx->ny; j++) {
     169          260 :         ctx->dy_inv[j] = (j < ctx->ny - 1) ? 1.0 / (2.0 * grid->dy[j]) : 0.0;
     170              :     }
     171              : 
     172           11 :     ctx->initialized = 1;
     173           11 :     solver->context = ctx;
     174              : 
     175              : #if USE_AVX
     176              :     #ifdef _OPENMP
     177              :     CFD_LOG_INFO("solver", "Explicit Euler SIMD: AVX2 + OpenMP enabled (%d threads)", omp_get_max_threads());
     178              :     #else
     179              :     CFD_LOG_INFO("solver", "Explicit Euler SIMD: AVX2 enabled (OpenMP disabled)");
     180              :     #endif
     181              : #else
     182              :     #ifdef _OPENMP
     183           11 :     CFD_LOG_INFO("solver", "Explicit Euler OMP: Scalar + OpenMP enabled (%d threads)", omp_get_max_threads());
     184              :     #else
     185              :     CFD_LOG_INFO("solver", "Explicit Euler: Scalar fallback (no SIMD or OpenMP)");
     186              :     #endif
     187              : #endif
     188              : 
     189           11 :     return CFD_SUCCESS;
     190              : }
     191              : 
     192           15 : void explicit_euler_simd_destroy(struct NSSolver* solver) {
     193           15 :     if (solver && solver->context) {
     194           11 :         explicit_euler_simd_context* ctx = (explicit_euler_simd_context*)solver->context;
     195           11 :         if (ctx->initialized) {
     196           11 :             cfd_aligned_free(ctx->u_new);
     197           11 :             cfd_aligned_free(ctx->v_new);
     198           11 :             cfd_aligned_free(ctx->w_new);
     199           11 :             cfd_aligned_free(ctx->p_new);
     200           11 :             cfd_aligned_free(ctx->T_ws);
     201           11 :             cfd_aligned_free(ctx->dx_inv);
     202           11 :             cfd_aligned_free(ctx->dy_inv);
     203              :         }
     204           11 :         cfd_free(ctx);
     205           11 :         solver->context = NULL;
     206              :     }
     207           15 : }
     208              : 
     209              : #if USE_AVX
     210              : static inline __m256d vector_fmax(__m256d a, __m256d b) {
     211              :     return _mm256_max_pd(a, b);
     212              : }
     213              : static inline __m256d vector_fmin(__m256d a, __m256d b) {
     214              :     return _mm256_min_pd(a, b);
     215              : }
     216              : 
     217              : typedef struct {
     218              :     __m256d dt_vec;
     219              :     __m256d max_deriv;
     220              :     __m256d min_deriv;
     221              :     __m256d max_diverg;
     222              :     __m256d min_diverg;
     223              :     __m256d max_vel_limit;
     224              :     __m256d min_vel_limit;
     225              :     __m256d one_vec;
     226              :     __m256d neg_one_vec;
     227              :     __m256d pressure_factor;
     228              :     __m256d two;
     229              :     __m256d four;
     230              :     __m256d epsilon;
     231              :     __m256d mu_vec;
     232              :     __m256d zero;
     233              :     __m256d inv_2dz_vec;
     234              :     __m256d inv_dz2_vec;
     235              :     /* Boussinesq buoyancy: accel = -beta*(T - T_ref)*g. Zero beta -> no-op. */
     236              :     __m256d neg_beta_vec;
     237              :     __m256d t_ref_vec;
     238              :     __m256d gx_vec;
     239              :     __m256d gy_vec;
     240              :     __m256d gz_vec;
     241              : } simd_constants;
     242              : 
     243              : static void init_simd_constants(simd_constants* c, const ns_solver_params_t* params,
     244              :                                 double conservative_dt, double inv_2dz, double inv_dz2) {
     245              :     c->dt_vec = _mm256_set1_pd(conservative_dt);
     246              :     c->max_deriv = _mm256_set1_pd(MAX_DERIVATIVE_LIMIT);
     247              :     c->min_deriv = _mm256_set1_pd(-MAX_DERIVATIVE_LIMIT);
     248              :     c->max_diverg = _mm256_set1_pd(MAX_DIVERGENCE_LIMIT);
     249              :     c->min_diverg = _mm256_set1_pd(-MAX_DIVERGENCE_LIMIT);
     250              :     c->max_vel_limit = _mm256_set1_pd(MAX_VELOCITY_LIMIT);
     251              :     c->min_vel_limit = _mm256_set1_pd(-MAX_VELOCITY_LIMIT);
     252              :     c->one_vec = _mm256_set1_pd(UPDATE_LIMIT);
     253              :     c->neg_one_vec = _mm256_set1_pd(-UPDATE_LIMIT);
     254              :     c->pressure_factor = _mm256_set1_pd(-PRESSURE_UPDATE_FACTOR);
     255              :     c->two = _mm256_set1_pd(2.0);
     256              :     c->four = _mm256_set1_pd(4.0);
     257              :     c->epsilon = _mm256_set1_pd(1e-10);
     258              :     c->mu_vec = _mm256_set1_pd(params->mu);
     259              :     c->zero = _mm256_setzero_pd();
     260              :     c->inv_2dz_vec = _mm256_set1_pd(inv_2dz);
     261              :     c->inv_dz2_vec = _mm256_set1_pd(inv_dz2);
     262              :     c->neg_beta_vec = _mm256_set1_pd(-params->beta);
     263              :     c->t_ref_vec = _mm256_set1_pd(params->T_ref);
     264              :     c->gx_vec = _mm256_set1_pd(params->gravity[0]);
     265              :     c->gy_vec = _mm256_set1_pd(params->gravity[1]);
     266              :     c->gz_vec = _mm256_set1_pd(params->gravity[2]);
     267              : }
     268              : 
     269              : static void process_simd_row(explicit_euler_simd_context* ctx, flow_field* field, const grid* grid,
     270              :                              size_t j, const simd_constants* sc,
     271              :                              size_t stride_z, size_t k_offset) {
     272              :     double dy2 = grid->dy[j] * grid->dy[j];
     273              :     __m256d dy_inv_val = _mm256_set1_pd(ctx->dy_inv[j]);
     274              :     __m256d dy2_val = _mm256_set1_pd(dy2);
     275              :     __m256d dy2_recip = _mm256_div_pd(sc->one_vec, dy2_val);
     276              : 
     277              :     for (size_t i = 1; i + 3 < ctx->nx - 1; i += 4) {
     278              :         size_t idx = k_offset + IDX_2D(i, j, ctx->nx);
     279              : 
     280              :         __m256d u = _mm256_loadu_pd(&field->u[idx]);
     281              :         __m256d v = _mm256_loadu_pd(&field->v[idx]);
     282              :         __m256d rho = _mm256_loadu_pd(&field->rho[idx]);
     283              :         __m256d rho_inv = _mm256_div_pd(sc->one_vec, _mm256_max_pd(rho, sc->epsilon));
     284              :         __m256d dx_inv_val = _mm256_loadu_pd(&ctx->dx_inv[i]);
     285              : 
     286              :         __m256d u_xp = _mm256_loadu_pd(&field->u[idx + 1]);
     287              :         __m256d u_xm = _mm256_loadu_pd(&field->u[idx - 1]);
     288              :         __m256d u_yp = _mm256_loadu_pd(&field->u[idx + ctx->nx]);
     289              :         __m256d u_ym = _mm256_loadu_pd(&field->u[idx - ctx->nx]);
     290              :         __m256d u_zp = _mm256_loadu_pd(&field->u[idx + stride_z]);
     291              :         __m256d u_zm = _mm256_loadu_pd(&field->u[idx - stride_z]);
     292              : 
     293              :         __m256d du_dx = _mm256_mul_pd(_mm256_sub_pd(u_xp, u_xm), dx_inv_val);
     294              :         __m256d du_dy = _mm256_mul_pd(_mm256_sub_pd(u_yp, u_ym), dy_inv_val);
     295              :         __m256d du_dz = _mm256_mul_pd(_mm256_sub_pd(u_zp, u_zm), sc->inv_2dz_vec);
     296              : 
     297              :         du_dx = vector_fmax(sc->min_deriv, vector_fmin(sc->max_deriv, du_dx));
     298              :         du_dy = vector_fmax(sc->min_deriv, vector_fmin(sc->max_deriv, du_dy));
     299              :         du_dz = vector_fmax(sc->min_deriv, vector_fmin(sc->max_deriv, du_dz));
     300              : 
     301              :         __m256d v_xp = _mm256_loadu_pd(&field->v[idx + 1]);
     302              :         __m256d v_xm = _mm256_loadu_pd(&field->v[idx - 1]);
     303              :         __m256d v_yp = _mm256_loadu_pd(&field->v[idx + ctx->nx]);
     304              :         __m256d v_ym = _mm256_loadu_pd(&field->v[idx - ctx->nx]);
     305              :         __m256d v_zp = _mm256_loadu_pd(&field->v[idx + stride_z]);
     306              :         __m256d v_zm = _mm256_loadu_pd(&field->v[idx - stride_z]);
     307              : 
     308              :         __m256d dv_dx = _mm256_mul_pd(_mm256_sub_pd(v_xp, v_xm), dx_inv_val);
     309              :         __m256d dv_dy = _mm256_mul_pd(_mm256_sub_pd(v_yp, v_ym), dy_inv_val);
     310              :         __m256d dv_dz = _mm256_mul_pd(_mm256_sub_pd(v_zp, v_zm), sc->inv_2dz_vec);
     311              : 
     312              :         dv_dx = vector_fmax(sc->min_deriv, vector_fmin(sc->max_deriv, dv_dx));
     313              :         dv_dy = vector_fmax(sc->min_deriv, vector_fmin(sc->max_deriv, dv_dy));
     314              :         dv_dz = vector_fmax(sc->min_deriv, vector_fmin(sc->max_deriv, dv_dz));
     315              : 
     316              :         __m256d p_xp = _mm256_loadu_pd(&field->p[idx + 1]);
     317              :         __m256d p_xm = _mm256_loadu_pd(&field->p[idx - 1]);
     318              :         __m256d p_yp = _mm256_loadu_pd(&field->p[idx + ctx->nx]);
     319              :         __m256d p_ym = _mm256_loadu_pd(&field->p[idx - ctx->nx]);
     320              :         __m256d p_zp = _mm256_loadu_pd(&field->p[idx + stride_z]);
     321              :         __m256d p_zm = _mm256_loadu_pd(&field->p[idx - stride_z]);
     322              : 
     323              :         __m256d dp_dx = _mm256_mul_pd(_mm256_sub_pd(p_xp, p_xm), dx_inv_val);
     324              :         __m256d dp_dy = _mm256_mul_pd(_mm256_sub_pd(p_yp, p_ym), dy_inv_val);
     325              :         __m256d dp_dz = _mm256_mul_pd(_mm256_sub_pd(p_zp, p_zm), sc->inv_2dz_vec);
     326              : 
     327              :         dp_dx = vector_fmax(sc->min_deriv, vector_fmin(sc->max_deriv, dp_dx));
     328              :         dp_dy = vector_fmax(sc->min_deriv, vector_fmin(sc->max_deriv, dp_dy));
     329              :         dp_dz = vector_fmax(sc->min_deriv, vector_fmin(sc->max_deriv, dp_dz));
     330              : 
     331              :         __m256d w = _mm256_loadu_pd(&field->w[idx]);
     332              :         __m256d w_xp = _mm256_loadu_pd(&field->w[idx + 1]);
     333              :         __m256d w_xm = _mm256_loadu_pd(&field->w[idx - 1]);
     334              :         __m256d w_yp = _mm256_loadu_pd(&field->w[idx + ctx->nx]);
     335              :         __m256d w_ym = _mm256_loadu_pd(&field->w[idx - ctx->nx]);
     336              :         __m256d w_zp = _mm256_loadu_pd(&field->w[idx + stride_z]);
     337              :         __m256d w_zm = _mm256_loadu_pd(&field->w[idx - stride_z]);
     338              : 
     339              :         __m256d dw_dx = _mm256_mul_pd(_mm256_sub_pd(w_xp, w_xm), dx_inv_val);
     340              :         __m256d dw_dy = _mm256_mul_pd(_mm256_sub_pd(w_yp, w_ym), dy_inv_val);
     341              :         __m256d dw_dz = _mm256_mul_pd(_mm256_sub_pd(w_zp, w_zm), sc->inv_2dz_vec);
     342              : 
     343              :         dw_dx = vector_fmax(sc->min_deriv, vector_fmin(sc->max_deriv, dw_dx));
     344              :         dw_dy = vector_fmax(sc->min_deriv, vector_fmin(sc->max_deriv, dw_dy));
     345              :         dw_dz = vector_fmax(sc->min_deriv, vector_fmin(sc->max_deriv, dw_dz));
     346              : 
     347              :         __m256d inv_dx_sq = _mm256_mul_pd(sc->four, _mm256_mul_pd(dx_inv_val, dx_inv_val));
     348              : 
     349              :         __m256d d2u_dx2 = _mm256_mul_pd(
     350              :             _mm256_sub_pd(_mm256_add_pd(u_xp, u_xm), _mm256_mul_pd(sc->two, u)), inv_dx_sq);
     351              :         __m256d d2u_dy2 = _mm256_mul_pd(
     352              :             _mm256_sub_pd(_mm256_add_pd(u_yp, u_ym), _mm256_mul_pd(sc->two, u)), dy2_recip);
     353              :         __m256d d2u_dz2 = _mm256_mul_pd(
     354              :             _mm256_sub_pd(_mm256_add_pd(u_zp, u_zm), _mm256_mul_pd(sc->two, u)), sc->inv_dz2_vec);
     355              : 
     356              :         __m256d d2v_dx2 = _mm256_mul_pd(
     357              :             _mm256_sub_pd(_mm256_add_pd(v_xp, v_xm), _mm256_mul_pd(sc->two, v)), inv_dx_sq);
     358              :         __m256d d2v_dy2 = _mm256_mul_pd(
     359              :             _mm256_sub_pd(_mm256_add_pd(v_yp, v_ym), _mm256_mul_pd(sc->two, v)), dy2_recip);
     360              :         __m256d d2v_dz2 = _mm256_mul_pd(
     361              :             _mm256_sub_pd(_mm256_add_pd(v_zp, v_zm), _mm256_mul_pd(sc->two, v)), sc->inv_dz2_vec);
     362              : 
     363              :         __m256d d2w_dx2 = _mm256_mul_pd(
     364              :             _mm256_sub_pd(_mm256_add_pd(w_xp, w_xm), _mm256_mul_pd(sc->two, w)), inv_dx_sq);
     365              :         __m256d d2w_dy2 = _mm256_mul_pd(
     366              :             _mm256_sub_pd(_mm256_add_pd(w_yp, w_ym), _mm256_mul_pd(sc->two, w)), dy2_recip);
     367              :         __m256d d2w_dz2 = _mm256_mul_pd(
     368              :             _mm256_sub_pd(_mm256_add_pd(w_zp, w_zm), _mm256_mul_pd(sc->two, w)), sc->inv_dz2_vec);
     369              : 
     370              :         __m256d nu = _mm256_min_pd(sc->one_vec, _mm256_mul_pd(sc->mu_vec, rho_inv));
     371              : 
     372              :         __m256d term_pres_x = _mm256_mul_pd(dp_dx, rho_inv);
     373              :         __m256d term_visc_u = _mm256_mul_pd(nu, _mm256_add_pd(_mm256_add_pd(d2u_dx2, d2u_dy2), d2u_dz2));
     374              :         __m256d conv_u = _mm256_add_pd(
     375              :             _mm256_add_pd(_mm256_mul_pd(u, du_dx), _mm256_mul_pd(v, du_dy)),
     376              :             _mm256_mul_pd(w, du_dz));
     377              :         __m256d du =
     378              :             _mm256_mul_pd(sc->dt_vec, _mm256_add_pd(_mm256_sub_pd(term_visc_u, term_pres_x),
     379              :                                                     _mm256_sub_pd(sc->zero, conv_u)));
     380              : 
     381              :         __m256d term_pres_y = _mm256_mul_pd(dp_dy, rho_inv);
     382              :         __m256d term_visc_v = _mm256_mul_pd(nu, _mm256_add_pd(_mm256_add_pd(d2v_dx2, d2v_dy2), d2v_dz2));
     383              :         __m256d conv_v = _mm256_add_pd(
     384              :             _mm256_add_pd(_mm256_mul_pd(u, dv_dx), _mm256_mul_pd(v, dv_dy)),
     385              :             _mm256_mul_pd(w, dv_dz));
     386              :         __m256d dv =
     387              :             _mm256_mul_pd(sc->dt_vec, _mm256_add_pd(_mm256_sub_pd(term_visc_v, term_pres_y),
     388              :                                                     _mm256_sub_pd(sc->zero, conv_v)));
     389              : 
     390              :         __m256d term_pres_z = _mm256_mul_pd(dp_dz, rho_inv);
     391              :         __m256d term_visc_w = _mm256_mul_pd(nu, _mm256_add_pd(_mm256_add_pd(d2w_dx2, d2w_dy2), d2w_dz2));
     392              :         __m256d conv_w = _mm256_add_pd(
     393              :             _mm256_add_pd(_mm256_mul_pd(u, dw_dx), _mm256_mul_pd(v, dw_dy)),
     394              :             _mm256_mul_pd(w, dw_dz));
     395              :         __m256d dw =
     396              :             _mm256_mul_pd(sc->dt_vec, _mm256_add_pd(_mm256_sub_pd(term_visc_w, term_pres_z),
     397              :                                                     _mm256_sub_pd(sc->zero, conv_w)));
     398              : 
     399              :         /* Boussinesq buoyancy: dvel += dt * (-beta*(T - T_ref)) * g.
     400              :          * When beta == 0 this adds exactly 0.0, matching the scalar path. */
     401              :         __m256d Tcell = _mm256_loadu_pd(&field->T[idx]);
     402              :         __m256d buoy = _mm256_mul_pd(sc->neg_beta_vec, _mm256_sub_pd(Tcell, sc->t_ref_vec));
     403              :         __m256d dt_buoy = _mm256_mul_pd(sc->dt_vec, buoy);
     404              :         du = _mm256_add_pd(du, _mm256_mul_pd(dt_buoy, sc->gx_vec));
     405              :         dv = _mm256_add_pd(dv, _mm256_mul_pd(dt_buoy, sc->gy_vec));
     406              :         dw = _mm256_add_pd(dw, _mm256_mul_pd(dt_buoy, sc->gz_vec));
     407              : 
     408              :         du = vector_fmin(sc->one_vec, vector_fmax(sc->neg_one_vec, du));
     409              :         dv = vector_fmin(sc->one_vec, vector_fmax(sc->neg_one_vec, dv));
     410              :         dw = vector_fmin(sc->one_vec, vector_fmax(sc->neg_one_vec, dw));
     411              : 
     412              :         __m256d u_next = _mm256_add_pd(u, du);
     413              :         __m256d v_next = _mm256_add_pd(v, dv);
     414              :         __m256d w_next = _mm256_add_pd(w, dw);
     415              : 
     416              :         u_next = vector_fmin(sc->max_vel_limit, vector_fmax(sc->min_vel_limit, u_next));
     417              :         v_next = vector_fmin(sc->max_vel_limit, vector_fmax(sc->min_vel_limit, v_next));
     418              :         w_next = vector_fmin(sc->max_vel_limit, vector_fmax(sc->min_vel_limit, w_next));
     419              : 
     420              :         __m256d divergence = _mm256_add_pd(_mm256_add_pd(du_dx, dv_dy), dw_dz);
     421              :         divergence = vector_fmin(sc->max_diverg, vector_fmax(sc->min_diverg, divergence));
     422              :         __m256d p = _mm256_loadu_pd(&field->p[idx]);
     423              :         __m256d dp = _mm256_mul_pd(
     424              :             sc->dt_vec, _mm256_mul_pd(sc->pressure_factor, _mm256_mul_pd(rho, divergence)));
     425              :         dp = vector_fmin(sc->one_vec, vector_fmax(sc->neg_one_vec, dp));
     426              :         __m256d p_next = _mm256_add_pd(p, dp);
     427              : 
     428              :         _mm256_storeu_pd(&ctx->u_new[idx], u_next);
     429              :         _mm256_storeu_pd(&ctx->v_new[idx], v_next);
     430              :         _mm256_storeu_pd(&ctx->w_new[idx], w_next);
     431              :         _mm256_storeu_pd(&ctx->p_new[idx], p_next);
     432              :     }
     433              : }
     434              : #endif
     435              : 
     436              : #if !USE_AVX
     437        15676 : static void process_scalar_row(explicit_euler_simd_context* ctx, flow_field* field,
     438              :                                const grid* grid, const ns_solver_params_t* params, size_t j,
     439              :                                double conservative_dt, double t, size_t stride_z, size_t k_offset) {
     440       877570 :     for (size_t i = 1; i < ctx->nx - 1; i++) {
     441       861894 :         size_t idx = k_offset + IDX_2D(i, j, ctx->nx);
     442              : 
     443       861894 :         double du_dx = (field->u[idx + 1] - field->u[idx - 1]) / (2.0 * grid->dx[i]);
     444       861894 :         double du_dy = (field->u[idx + ctx->nx] - field->u[idx - ctx->nx]) / (2.0 * grid->dy[j]);
     445       861894 :         double du_dz = (field->u[idx + stride_z] - field->u[idx - stride_z]) * ctx->inv_2dz;
     446       861894 :         double dv_dx = (field->v[idx + 1] - field->v[idx - 1]) / (2.0 * grid->dx[i]);
     447       861894 :         double dv_dy = (field->v[idx + ctx->nx] - field->v[idx - ctx->nx]) / (2.0 * grid->dy[j]);
     448       861894 :         double dv_dz = (field->v[idx + stride_z] - field->v[idx - stride_z]) * ctx->inv_2dz;
     449       861894 :         double dw_dx = (field->w[idx + 1] - field->w[idx - 1]) / (2.0 * grid->dx[i]);
     450       861894 :         double dw_dy = (field->w[idx + ctx->nx] - field->w[idx - ctx->nx]) / (2.0 * grid->dy[j]);
     451       861894 :         double dw_dz = (field->w[idx + stride_z] - field->w[idx - stride_z]) * ctx->inv_2dz;
     452              : 
     453       861894 :         double dp_dx = (field->p[idx + 1] - field->p[idx - 1]) / (2.0 * grid->dx[i]);
     454       861894 :         double dp_dy = (field->p[idx + ctx->nx] - field->p[idx - ctx->nx]) / (2.0 * grid->dy[j]);
     455       861894 :         double dp_dz = (field->p[idx + stride_z] - field->p[idx - stride_z]) * ctx->inv_2dz;
     456              : 
     457       861894 :         double d2u_dx2 = (field->u[idx + 1] - 2.0 * field->u[idx] + field->u[idx - 1]) /
     458       861894 :                          (grid->dx[i] * grid->dx[i]);
     459       861894 :         double d2u_dy2 = (field->u[idx + ctx->nx] - 2.0 * field->u[idx] + field->u[idx - ctx->nx]) /
     460       861894 :                          (grid->dy[j] * grid->dy[j]);
     461       861894 :         double d2u_dz2 = (field->u[idx + stride_z] - 2.0 * field->u[idx] + field->u[idx - stride_z]) *
     462       861894 :                          ctx->inv_dz2;
     463       861894 :         double d2v_dx2 = (field->v[idx + 1] - 2.0 * field->v[idx] + field->v[idx - 1]) /
     464              :                          (grid->dx[i] * grid->dx[i]);
     465       861894 :         double d2v_dy2 = (field->v[idx + ctx->nx] - 2.0 * field->v[idx] + field->v[idx - ctx->nx]) /
     466              :                          (grid->dy[j] * grid->dy[j]);
     467       861894 :         double d2v_dz2 = (field->v[idx + stride_z] - 2.0 * field->v[idx] + field->v[idx - stride_z]) *
     468              :                          ctx->inv_dz2;
     469       861894 :         double d2w_dx2 = (field->w[idx + 1] - 2.0 * field->w[idx] + field->w[idx - 1]) /
     470              :                          (grid->dx[i] * grid->dx[i]);
     471       861894 :         double d2w_dy2 = (field->w[idx + ctx->nx] - 2.0 * field->w[idx] + field->w[idx - ctx->nx]) /
     472              :                          (grid->dy[j] * grid->dy[j]);
     473       861894 :         double d2w_dz2 = (field->w[idx + stride_z] - 2.0 * field->w[idx] + field->w[idx - stride_z]) *
     474              :                          ctx->inv_dz2;
     475              : 
     476       861894 :         double rho = fmax(field->rho[idx], 1e-10);
     477              :         // Using manual define for M_PI just in case it is missed in fallback
     478              : #ifndef M_PI
     479              : #define M_PI 3.14159265358979323846
     480              : #endif
     481       861894 :         double nu = fmin(params->mu / rho, 1.0);
     482              : 
     483       861894 :         du_dx = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, du_dx));
     484       861894 :         du_dy = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, du_dy));
     485       861894 :         du_dz = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, du_dz));
     486       861894 :         dv_dx = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dv_dx));
     487       861894 :         dv_dy = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dv_dy));
     488       861894 :         dv_dz = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dv_dz));
     489       861894 :         dw_dx = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dw_dx));
     490       861894 :         dw_dy = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dw_dy));
     491       861894 :         dw_dz = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dw_dz));
     492       861894 :         dp_dx = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dp_dx));
     493       861894 :         dp_dy = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dp_dy));
     494       861894 :         dp_dz = fmax(-MAX_DERIVATIVE_LIMIT, fmin(MAX_DERIVATIVE_LIMIT, dp_dz));
     495              : 
     496       861894 :         double source_u = 0.0;
     497       861894 :         double source_v = 0.0;
     498       861894 :         double source_w = 0.0;
     499       861894 :         if (params->source_func) {
     500            0 :             params->source_func(grid->x[i], grid->y[j], 0.0, t,
     501            0 :                                 params->source_context,
     502              :                                 &source_u, &source_v, &source_w);
     503       861894 :         } else if (params->source_amplitude_u > 0) {
     504       861462 :             source_u = params->source_amplitude_u * sin(M_PI * grid->y[j]);
     505       861462 :             source_v = params->source_amplitude_v * sin(2.0 * M_PI * grid->x[i]);
     506              :         }
     507              : 
     508              :         /* Boussinesq buoyancy source (no-op when beta == 0) */
     509       861894 :         energy_compute_buoyancy(field->T[idx], params, &source_u, &source_v, &source_w);
     510              : 
     511       861894 :         double u_c = field->u[idx];
     512       861894 :         double v_c = field->v[idx];
     513       861894 :         double w_c = field->w[idx];
     514              : 
     515       861894 :         double du = conservative_dt * (-u_c * du_dx - v_c * du_dy - w_c * du_dz -
     516       861894 :                                        dp_dx / rho + nu * (d2u_dx2 + d2u_dy2 + d2u_dz2) + source_u);
     517       861894 :         double dv = conservative_dt * (-u_c * dv_dx - v_c * dv_dy - w_c * dv_dz -
     518       861894 :                                        dp_dy / rho + nu * (d2v_dx2 + d2v_dy2 + d2v_dz2) + source_v);
     519       861894 :         double dw = conservative_dt * (-u_c * dw_dx - v_c * dw_dy - w_c * dw_dz -
     520       861894 :                                        dp_dz / rho + nu * (d2w_dx2 + d2w_dy2 + d2w_dz2) + source_w);
     521              : 
     522       861894 :         du = fmax(-UPDATE_LIMIT, fmin(UPDATE_LIMIT, du));
     523       861894 :         dv = fmax(-UPDATE_LIMIT, fmin(UPDATE_LIMIT, dv));
     524       861894 :         dw = fmax(-UPDATE_LIMIT, fmin(UPDATE_LIMIT, dw));
     525              : 
     526       861894 :         ctx->u_new[idx] = fmax(-MAX_VELOCITY_LIMIT, fmin(MAX_VELOCITY_LIMIT, u_c + du));
     527       861894 :         ctx->v_new[idx] = fmax(-MAX_VELOCITY_LIMIT, fmin(MAX_VELOCITY_LIMIT, v_c + dv));
     528       861894 :         ctx->w_new[idx] = fmax(-MAX_VELOCITY_LIMIT, fmin(MAX_VELOCITY_LIMIT, w_c + dw));
     529              : 
     530       861894 :         double divergence = du_dx + dv_dy + dw_dz;
     531       861894 :         divergence = fmax(-MAX_DIVERGENCE_LIMIT, fmin(MAX_DIVERGENCE_LIMIT, divergence));
     532       861894 :         double dp = -PRESSURE_UPDATE_FACTOR * conservative_dt * rho * divergence;
     533       861894 :         dp = fmax(-UPDATE_LIMIT, fmin(UPDATE_LIMIT, dp));
     534       861894 :         ctx->p_new[idx] = field->p[idx] + dp;
     535              :     }
     536        15676 : }
     537              : #endif
     538              : 
     539          325 : cfd_status_t explicit_euler_simd_step(struct NSSolver* solver, flow_field* field, const grid* grid,
     540              :                                       const ns_solver_params_t* params, ns_solver_stats_t* stats) {
     541          325 :     if (!solver || !solver->context || !field || !grid || !params) {
     542              :         return CFD_ERROR_INVALID;
     543              :     }
     544              : 
     545          325 :     explicit_euler_simd_context* ctx = (explicit_euler_simd_context*)solver->context;
     546              : 
     547          325 :     if (field->nx < 3 || field->ny < 3 || (field->nz > 1 && field->nz < 3)) {
     548              :         return CFD_ERROR_INVALID;
     549              :     }
     550              : 
     551          325 :     if (field->nx != ctx->nx || field->ny != ctx->ny || field->nz != ctx->nz) {
     552              :         return CFD_ERROR_INVALID;
     553              :     }
     554              : 
     555              :     // Use conservative time step to match basic solver stability
     556          325 :     double conservative_dt = fmin(params->dt, DT_CONSERVATIVE_LIMIT);
     557              : 
     558              :     // Copy current state to temp buffers
     559          325 :     size_t size = ctx->nx * ctx->ny * ctx->nz;
     560          325 :     memcpy(ctx->u_new, field->u, size * sizeof(double));
     561          325 :     memcpy(ctx->v_new, field->v, size * sizeof(double));
     562          325 :     memcpy(ctx->w_new, field->w, size * sizeof(double));
     563          325 :     memcpy(ctx->p_new, field->p, size * sizeof(double));
     564              : 
     565          325 :     size_t nx = ctx->nx;
     566          325 :     size_t ny = ctx->ny;
     567              : 
     568              : #if USE_AVX
     569              :     simd_constants sc;
     570              :     init_simd_constants(&sc, params, conservative_dt, ctx->inv_2dz, ctx->inv_dz2);
     571              : 
     572              :     int ny_int = (int)(ctx->ny);
     573              :     int j;
     574              :     for (size_t k = ctx->k_start; k < ctx->k_end; k++) {
     575              :         size_t k_offset = k * ctx->stride_z;
     576              : #ifdef _OPENMP
     577              :         #pragma omp parallel for schedule(static)
     578              : #endif
     579              :         for (j = 1; j < ny_int - 1; j++) {
     580              :             process_simd_row(ctx, field, grid, (size_t)j, &sc, ctx->stride_z, k_offset);
     581              :         }
     582              :     }
     583              : #else
     584          325 :     int ny_int = (int)(ctx->ny);
     585          325 :     int j;
     586          660 :     for (size_t k = ctx->k_start; k < ctx->k_end; k++) {
     587          335 :         size_t k_offset = k * ctx->stride_z;
     588              : #ifdef _OPENMP
     589          335 :         #pragma omp parallel for schedule(static)
     590              : #endif
     591              :         for (j = 1; j < ny_int - 1; j++) {
     592              :             process_scalar_row(ctx, field, grid, params, (size_t)j, conservative_dt, 0.0,
     593              :                                ctx->stride_z, k_offset);
     594              :         }
     595              :     }
     596              : #endif
     597              : 
     598              :     // Apply boundary, check NaNs, etc.
     599          325 :     memcpy(field->u, ctx->u_new, size * sizeof(double));
     600          325 :     memcpy(field->v, ctx->v_new, size * sizeof(double));
     601          325 :     memcpy(field->w, ctx->w_new, size * sizeof(double));
     602          325 :     memcpy(field->p, ctx->p_new, size * sizeof(double));
     603              : 
     604              :     // Energy equation: advance temperature using updated velocity
     605              :     {
     606          650 :         cfd_status_t energy_status = energy_step_explicit_avx2_with_workspace(
     607              :             field, grid, params, conservative_dt,
     608          325 :             ctx->iter_count * conservative_dt, ctx->T_ws, size);
     609          325 :         if (energy_status != CFD_SUCCESS) {
     610              :             return energy_status;
     611              :         }
     612              :     }
     613              : 
     614              :     // Store caller-set boundary values before apply_boundary_conditions overwrites them,
     615              :     // then restore them. Then apply configured thermal BCs.
     616          325 :     copy_boundary_velocities_3d(ctx->u_new, ctx->v_new, ctx->w_new,
     617          325 :                                 field->u, field->v, field->w, nx, ny, ctx->nz);
     618          325 :     apply_boundary_conditions(field, grid);
     619          325 :     copy_boundary_velocities_3d(field->u, field->v, field->w,
     620          325 :                                 ctx->u_new, ctx->v_new, ctx->w_new, nx, ny, ctx->nz);
     621              :     {
     622          325 :         cfd_status_t bc_status = energy_apply_thermal_bcs(field, params);
     623          325 :         if (bc_status != CFD_SUCCESS) {
     624              :             return bc_status;
     625              :         }
     626              :     }
     627              : 
     628          325 :     if (stats) {
     629          305 :         stats->iterations = 1;
     630              :     }
     631              : 
     632              :     // NaN/Inf check
     633          325 :     int has_nan = 0;
     634       926487 :     for (size_t n = 0; n < size; n++) {
     635       926162 :         if (!isfinite(field->u[n]) || !isfinite(field->v[n]) ||
     636       926162 :             !isfinite(field->w[n]) || !isfinite(field->p[n])) {
     637              :             has_nan = 1;
     638              :             break;
     639              :         }
     640              :     }
     641          325 :     if (has_nan) {
     642            0 :         cfd_set_error(CFD_ERROR_DIVERGED,
     643              :                       "NaN/Inf detected in explicit_euler_simd step");
     644            0 :         return CFD_ERROR_DIVERGED;
     645              :     }
     646              : 
     647          325 :     ctx->iter_count++;
     648          325 :     return CFD_SUCCESS;
     649              : }
        

Generated by: LCOV version 2.0-1