LCOV - code coverage report
Current view: top level - solvers/energy/omp - energy_solver_omp.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 63.2 % 68 43
Test Date: 2026-06-23 13:41:07 Functions: 100.0 % 1 1

            Line data    Source code
       1              : /**
       2              :  * @file energy_solver_omp.c
       3              :  * @brief OpenMP-parallelized energy equation solver (advection-diffusion step)
       4              :  *
       5              :  * Solves: dT/dt + u*nabla(T) = alpha * nabla^2(T) + Q
       6              :  * using explicit Euler time integration and central differences.
       7              :  *
       8              :  * Same numerics as the scalar reference (energy/cpu/energy_solver.c); the
       9              :  * interior stencil loop is parallelized over j. Read/write separation
      10              :  * (reads field->T, writes T_new) makes the loop race-free. Buoyancy and
      11              :  * thermal BC application are shared with the scalar backend.
      12              :  *
      13              :  * Branch-free 3D: when nz==1, stride_z=0 and inv_2dz/inv_dz2=0.0 cause all
      14              :  * z-terms to vanish, producing identical results to a 2D code path.
      15              :  */
      16              : 
      17              : #include "../energy_solver_internal.h"
      18              : 
      19              : #include "cfd/core/indexing.h"
      20              : #include "cfd/core/memory.h"
      21              : 
      22              : #include <math.h>
      23              : #include <omp.h>
      24              : #include <string.h>
      25              : 
      26          606 : cfd_status_t energy_step_explicit_omp_with_workspace(
      27              :     flow_field* field, const grid* grid,
      28              :     const ns_solver_params_t* params,
      29              :     double dt, double time,
      30              :     double* T_workspace, size_t workspace_size) {
      31          606 :     if (!field || !grid || !params) {
      32            0 :         cfd_set_error(CFD_ERROR_INVALID,
      33              :                       "energy_solver_omp: field, grid, and params must be non-NULL");
      34            0 :         return CFD_ERROR_INVALID;
      35              :     }
      36          606 :     if (!field->T) {
      37            0 :         cfd_set_error(CFD_ERROR_INVALID,
      38              :                       "energy_solver_omp: missing temperature field");
      39            0 :         return CFD_ERROR_INVALID;
      40              :     }
      41              : 
      42              :     /* Skip when energy equation is disabled */
      43          606 :     if (params->alpha <= 0.0) {
      44              :         return CFD_SUCCESS;
      45              :     }
      46              : 
      47           60 :     size_t nx = field->nx;
      48           60 :     size_t ny = field->ny;
      49           60 :     size_t nz = field->nz;
      50              : 
      51           60 :     if (!grid->dx || !grid->dy || nx < 3 || ny < 3) {
      52            0 :         cfd_set_error(CFD_ERROR_INVALID,
      53              :                       "energy_solver_omp: grid too small or missing dx/dy");
      54            0 :         return CFD_ERROR_INVALID;
      55              :     }
      56           60 :     size_t plane = nx * ny;
      57           60 :     size_t total = plane * nz;
      58           60 :     double alpha = params->alpha;
      59              : 
      60              :     /* Validate uniform spacing (central-difference stencil assumes it) */
      61           60 :     const double dx0 = grid->dx[0];
      62           60 :     const double dy0 = grid->dy[0];
      63              :     {
      64           60 :         const double tol_x = 1e-12 * fmax(1.0, fabs(dx0));
      65          480 :         for (size_t i = 1; i < nx - 1; i++) {
      66          420 :             if (fabs(grid->dx[i] - dx0) > tol_x) {
      67            0 :                 cfd_set_error(CFD_ERROR_UNSUPPORTED,
      68              :                               "energy_solver_omp: non-uniform dx not supported");
      69            0 :                 return CFD_ERROR_UNSUPPORTED;
      70              :             }
      71              :         }
      72           60 :         const double tol_y = 1e-12 * fmax(1.0, fabs(dy0));
      73          480 :         for (size_t j = 1; j < ny - 1; j++) {
      74          420 :             if (fabs(grid->dy[j] - dy0) > tol_y) {
      75            0 :                 cfd_set_error(CFD_ERROR_UNSUPPORTED,
      76              :                               "energy_solver_omp: non-uniform dy not supported");
      77            0 :                 return CFD_ERROR_UNSUPPORTED;
      78              :             }
      79              :         }
      80              :     }
      81           60 :     if (nz > 1) {
      82            0 :         if (!grid->dz) {
      83            0 :             cfd_set_error(CFD_ERROR_INVALID,
      84              :                           "energy_solver_omp: missing dz for 3D energy solve");
      85            0 :             return CFD_ERROR_INVALID;
      86              :         }
      87            0 :         const double dz0 = grid->dz[0];
      88            0 :         const double tol_z = 1e-12 * fmax(1.0, fabs(dz0));
      89            0 :         for (size_t k = 1; k < nz - 1; k++) {
      90            0 :             if (fabs(grid->dz[k] - dz0) > tol_z) {
      91            0 :                 cfd_set_error(CFD_ERROR_UNSUPPORTED,
      92              :                               "energy_solver_omp: non-uniform dz not supported");
      93            0 :                 return CFD_ERROR_UNSUPPORTED;
      94              :             }
      95              :         }
      96              :     }
      97              : 
      98              :     /* Precomputed constants for uniform-grid stencil */
      99           60 :     double inv_2dx = 1.0 / (2.0 * dx0);
     100           60 :     double inv_2dy = 1.0 / (2.0 * dy0);
     101           60 :     double inv_dx2 = 1.0 / (dx0 * dx0);
     102           60 :     double inv_dy2 = 1.0 / (dy0 * dy0);
     103              : 
     104              :     /* Branch-free 3D constants */
     105           60 :     size_t stride_z = (nz > 1) ? plane : 0;
     106           60 :     size_t k_start  = (nz > 1) ? 1 : 0;
     107           60 :     size_t k_end    = (nz > 1) ? (nz - 1) : 1;
     108           60 :     double inv_2dz  = (nz > 1 && grid->dz) ? 1.0 / (2.0 * grid->dz[0]) : 0.0;
     109            0 :     double inv_dz2  = (nz > 1 && grid->dz) ? 1.0 / (grid->dz[0] * grid->dz[0]) : 0.0;
     110              : 
     111              :     /* Use caller's workspace or allocate internally */
     112           60 :     int owns_buffer = 0;
     113           60 :     double* T_new;
     114           60 :     if (T_workspace && workspace_size >= total) {
     115              :         T_new = T_workspace;
     116              :     } else {
     117            0 :         T_new = (double*)cfd_calloc(total, sizeof(double));
     118            0 :         if (!T_new) {
     119              :             return CFD_ERROR_NOMEM;
     120              :         }
     121              :         owns_buffer = 1;
     122              :     }
     123           60 :     memcpy(T_new, field->T, total * sizeof(double));
     124              : 
     125          120 :     for (size_t k = k_start; k < k_end; k++) {
     126           60 :         size_t k_offset = k * stride_z;
     127           60 :         int j;
     128           60 : #pragma omp parallel for schedule(static)
     129              :         for (j = 1; j < (int)ny - 1; j++) {
     130              :             for (int i = 1; i < (int)nx - 1; i++) {
     131              :                 size_t idx = k_offset + IDX_2D((size_t)i, (size_t)j, nx);
     132              : 
     133              :                 double T_c = field->T[idx];
     134              :                 double u_c = field->u[idx];
     135              :                 double v_c = field->v[idx];
     136              :                 double w_c = field->w[idx];
     137              : 
     138              :                 /* Advection: u * dT/dx + v * dT/dy + w * dT/dz */
     139              :                 double dT_dx = (field->T[idx + 1] - field->T[idx - 1]) * inv_2dx;
     140              :                 double dT_dy = (field->T[idx + nx] - field->T[idx - nx]) * inv_2dy;
     141              :                 double dT_dz = (field->T[idx + stride_z] - field->T[idx - stride_z]) * inv_2dz;
     142              : 
     143              :                 double advection = u_c * dT_dx + v_c * dT_dy + w_c * dT_dz;
     144              : 
     145              :                 /* Diffusion: alpha * (d2T/dx2 + d2T/dy2 + d2T/dz2) */
     146              :                 double d2T_dx2 = (field->T[idx + 1] - 2.0 * T_c + field->T[idx - 1]) * inv_dx2;
     147              :                 double d2T_dy2 = (field->T[idx + nx] - 2.0 * T_c + field->T[idx - nx]) * inv_dy2;
     148              :                 double d2T_dz2 = (field->T[idx + stride_z] - 2.0 * T_c +
     149              :                                    field->T[idx - stride_z]) * inv_dz2;
     150              : 
     151              :                 double diffusion = alpha * (d2T_dx2 + d2T_dy2 + d2T_dz2);
     152              : 
     153              :                 /* Heat source term */
     154              :                 double Q = 0.0;
     155              :                 if (params->heat_source_func) {
     156              :                     double x = grid->x[i];
     157              :                     double y = grid->y[j];
     158              :                     double z = (nz > 1 && grid->z) ? grid->z[k] : 0.0;
     159              :                     Q = params->heat_source_func(x, y, z, time,
     160              :                                                   params->heat_source_context);
     161              :                 }
     162              : 
     163              :                 /* Explicit Euler update */
     164              :                 double dT = dt * (-advection + diffusion + Q);
     165              :                 T_new[idx] = T_c + dT;
     166              :             }
     167              :         }
     168              :     }
     169              : 
     170              :     /* Check for NaN/Inf */
     171           60 :     int has_nan = 0;
     172           60 :     ptrdiff_t total_int = (ptrdiff_t)total;
     173           60 :     ptrdiff_t n;
     174           60 : #pragma omp parallel for reduction(| : has_nan) schedule(static)
     175              :     for (n = 0; n < total_int; n++) {
     176              :         if (!isfinite(T_new[n])) {
     177              :             has_nan = 1;
     178              :         }
     179              :     }
     180           60 :     if (has_nan) {
     181            0 :         cfd_set_error(CFD_ERROR_DIVERGED,
     182              :                       "NaN/Inf detected in energy_step_explicit_omp");
     183            0 :         if (owns_buffer) cfd_free(T_new);
     184            0 :         return CFD_ERROR_DIVERGED;
     185              :     }
     186              : 
     187           60 :     memcpy(field->T, T_new, total * sizeof(double));
     188           60 :     if (owns_buffer) cfd_free(T_new);
     189              : 
     190              :     return CFD_SUCCESS;
     191              : }
        

Generated by: LCOV version 2.0-1