Skip to content

Architecture

shaia edited this page Mar 4, 2026 · 2 revisions

Architecture

This page describes the internal architecture of the CFD library.

Project Structure

cfd/
├── lib/
│   ├── include/cfd/              # Public header files
│   │   ├── api/
│   │   │   └── simulation_api.h        # High-level simulation API
│   │   ├── core/
│   │   │   ├── grid.h                  # Grid data structures
│   │   │   ├── cfd_status.h            # Status/error codes
│   │   │   ├── cfd_init.h              # Library init/finalize
│   │   │   ├── cfd_version.h           # Version constants
│   │   │   ├── derived_fields.h        # Computed field quantities
│   │   │   ├── logging.h              # Logging utilities
│   │   │   ├── cpu_features.h          # Runtime CPU feature detection
│   │   │   ├── filesystem.h            # Filesystem helpers
│   │   │   ├── gpu_device.h            # GPU device query
│   │   │   └── memory.h               # Memory allocation helpers
│   │   ├── solvers/
│   │   │   ├── navier_stokes_solver.h  # NS solver interface and types
│   │   │   └── poisson_solver.h        # Poisson solver interface
│   │   ├── boundary/
│   │   │   └── boundary_conditions.h   # Boundary condition API
│   │   └── io/
│   │       ├── vtk_output.h            # VTK file generation
│   │       ├── csv_output.h            # CSV file generation
│   │       └── output_registry.h       # Output registration API
│   └── src/
│       ├── api/                  # API implementations
│       │   ├── simulation_api.c
│       │   └── solver_registry.c
│       ├── core/                 # Core functionality
│       │   ├── grid.c
│       │   ├── flow_field.c
│       │   └── derived_fields.c
│       ├── io/                   # Input/Output
│       │   ├── vtk_output.c
│       │   ├── csv_output.c
│       │   └── output_registry.c
│       ├── boundary/             # Boundary conditions
│       │   ├── boundary_conditions.c   # Dispatcher (technology-agnostic)
│       │   ├── cpu/
│       │   ├── simd/
│       │   ├── omp/
│       │   └── gpu/
│       └── solvers/              # Solver implementations
│           ├── navier_stokes/
│           │   ├── cpu/
│           │   ├── avx2/
│           │   ├── omp/
│           │   └── gpu/
│           └── linear/
│               ├── cpu/
│               ├── avx2/
│               ├── neon/
│               └── omp/
├── examples/                 # 19 example programs
└── tests/                    # Unit tests
    ├── core/
    ├── math/
    ├── validation/
    ├── solvers/
    ├── simulation/
    └── io/

Component Overview

Simulation API Layer

The simulation_api.h provides the high-level interface:

simulation_data
├── grid*                  # Computational mesh (2D or 3D)
├── flow_field*            # Solution variables (u, v, w, p, rho, T)
├── ns_solver_params_t     # Configuration (dt, CFL, viscosity, etc.)
├── ns_solver_t*           # Active solver instance
├── ns_solver_registry_t*  # Solver registry
├── ns_solver_stats_t      # Statistics from last step
├── output_registry_t*     # Registered outputs
├── run_prefix             # Output directory prefix
└── current_time           # Accumulated simulation time

Grid System

The grid structure holds mesh information for both 2D and 3D:

typedef struct {
    double *x, *y, *z;               // Cell-center coordinates
    double *dx, *dy, *dz;            // Cell spacing
    double *inv_dx2, *inv_dy2, *inv_dz2; // Precomputed 1/dx^2
    size_t nx, ny, nz;               // Grid dimensions (nz=1 for 2D)
    size_t stride_z;                 // nx*ny for 3D, 0 for 2D
    double xmin, xmax, ymin, ymax, zmin, zmax; // Domain bounds
    int i_start, i_end;              // Interior x-range
    int j_start, j_end;              // Interior y-range
    int k_start, k_end;              // Interior z-range
} grid;

Flow Field

Solution variables are stored in flow_field:

typedef struct {
    double* u;      // x-velocity
    double* v;      // y-velocity
    double* w;      // z-velocity (allocated but zero for 2D)
    double* p;      // pressure
    double* rho;    // density
    double* T;      // temperature
    size_t nx, ny, nz;  // Dimensions (nz=1 for 2D)
} flow_field;

Solver Interface

The solver system uses a vtable (function-pointer) architecture:

ns_solver_t
├── name, description, version
├── capabilities (bitflags)
├── backend (SCALAR / SIMD / OMP / CUDA)
├── context (opaque solver state)
└── Function pointers:
    ├── init()
    ├── destroy()
    ├── step()
    ├── solve()
    ├── apply_boundary()
    └── compute_dt()

Output Registry

The output system uses a registry pattern:

output_registry_t
└── entries[]
    ├── field_type (output_field_type enum)
    ├── interval   (steps between outputs)
    └── prefix     (filename prefix)

Key Patterns

Factory Pattern (Solver Creation)

Solvers are created through a named registry:

// Registration (done internally by the library)
ns_solver_registry_t* registry = cfd_registry_create();
cfd_registry_register_defaults(registry);

// Create a solver instance by name
ns_solver_t* solver = cfd_solver_create(registry, "explicit_euler");

Strategy Pattern (Solver Selection)

All solvers implement the same interface via function pointers:

solver_init(solver, grid, params);
solver_step(solver, field, grid, params, stats);
solver_destroy(solver);

Registry Pattern (Output System)

Outputs are registered once and processed automatically:

simulation_register_output(sim, OUTPUT_VELOCITY, 10, "vel");
simulation_register_output(sim, OUTPUT_CSV_TIMESERIES, 1, "ts");

// Writes all registered outputs at their intervals
simulation_write_outputs(sim, step);

Data Flow

User Code
    │
    ▼
Simulation API (simulation_api.c)
    │
    ├──► Output Registry (output_registry.c)
    │        │
    │        ├──► VTK Output (vtk_output.c)
    │        └──► CSV Output (csv_output.c)
    │
    └──► Solver Registry (solver_registry.c)
             │
             └──► Solver Implementations
                  ├── CPU      (navier_stokes/cpu/)
                  ├── AVX2     (navier_stokes/avx2/)
                  ├── OpenMP   (navier_stokes/omp/)
                  └── GPU      (navier_stokes/gpu/)

Memory Layout

Arrays use row-major ordering with a flat index:

2D indexing:

index = j * nx + i    (IDX_2D macro)

Where:
  i = column index (0 to nx-1)
  j = row index    (0 to ny-1)

3D indexing:

index = k * stride_z + j * nx + i    (IDX_3D macro)

Where:
  stride_z = nx * ny
  i = x-index (0 to nx-1)
  j = y-index (0 to ny-1)
  k = z-index (0 to nz-1)

Solver Implementations

Eleven solvers are registered. Each algorithm is available in multiple backends.

Explicit Euler

Basic forward-Euler time stepping:

  1. Compute convective fluxes
  2. Add diffusive terms
  3. Apply source terms
  4. Update solution: u^(n+1) = u^n + dt * RHS

Projection Method (Chorin's Algorithm)

Fractional-step method for incompressible flow:

  1. Compute intermediate velocity (without pressure)
  2. Solve pressure Poisson equation (using CG solver)
  3. Project velocity to divergence-free field

RK2 (Heun's Method)

Second-order Runge-Kutta time integration:

  1. Stage 1: forward-Euler predictor
  2. Stage 2: evaluate RHS at predicted state
  3. Combine with equal weights for O(dt^2) accuracy

Uses periodic stencil indexing in RHS evaluation; BCs applied only after the full step.

SIMD Optimized Backends

AVX2 intrinsics process 4 doubles simultaneously using 256-bit registers. Scalar tail loop handles remainder elements. Runtime CPU feature detection selects SIMD vs. scalar paths.

GPU Solvers

CUDA implementations with data transferred CPU → GPU at init, GPU → CPU for output. Returns CFD_ERROR_UNSUPPORTED if CUDA is unavailable.

Extension Points

Adding a New Solver

  1. Create solver source file:
// lib/src/solvers/navier_stokes/cpu/solver_mysolver.c
#include "cfd/solvers/navier_stokes_solver.h"

static cfd_status_t mysolver_init(ns_solver_t* s, const grid* g,
                                  const ns_solver_params_t* p) {
    // Allocate context, initialize buffers
    return CFD_SUCCESS;
}

static cfd_status_t mysolver_step(ns_solver_t* s, flow_field* f,
                                  const grid* g,
                                  const ns_solver_params_t* p,
                                  ns_solver_stats_t* stats) {
    // Implement time stepping
    return CFD_SUCCESS;
}

ns_solver_t* create_mysolver(void) {
    ns_solver_t* s = calloc(1, sizeof(ns_solver_t));
    s->name = "mysolver";
    s->init = mysolver_init;
    s->step = mysolver_step;
    s->destroy = mysolver_destroy;
    return s;
}
  1. Register in lib/src/api/solver_registry.c:
cfd_registry_register(registry, "mysolver", create_mysolver);

Adding a New Output Type

  1. Add enum value to output_field_type in lib/include/cfd/api/simulation_api.h
  2. Implement writer in lib/src/io/vtk_output.c or lib/src/io/csv_output.c
  3. Handle new type in lib/src/io/output_registry.c

Build System

CMake-based build with options:

Option Default Description
CFD_ENABLE_CUDA OFF Enable GPU solvers
CAVITY_FULL_VALIDATION OFF Expensive 129x129 cavity benchmark
ENABLE_ASAN OFF Address sanitizer (GCC/Clang)
ENABLE_UBSAN OFF Undefined-behavior sanitizer (GCC/Clang)
ENABLE_COVERAGE OFF Code coverage (GCC/Clang)

Modular Library Targets

Library CMake Target Contents
cfd_core CFD::Core Grid, memory, I/O, status, utilities
cfd_scalar CFD::Scalar Scalar CPU solvers
cfd_simd CFD::SIMD AVX2/NEON SIMD solvers
cfd_omp CFD::OMP OpenMP parallelized solvers
cfd_cuda CFD::CUDA CUDA GPU solvers
cfd_library CFD::Library Unified library (all backends)

Dependencies

  • Required: C99 compiler, CMake 3.10+
  • Optional:
    • CUDA Toolkit (for GPU solvers)
    • OpenMP (for parallel solvers, typically bundled with compiler)
    • Unity (for tests, fetched via CMake FetchContent)

Thread Safety

  • simulation_data instances are NOT thread-safe across threads
  • Each thread must use its own simulation_data
  • OpenMP solvers handle internal parallelization
  • GPU solvers manage CUDA synchronization internally
  • Function-static variables are prohibited; use context structs or _Thread_local

See Also