Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/verification.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Numerical verification

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
verification:
strategy:
fail-fast: false
matrix:
compiler: [gcc, clang]
build_type: [Debug, Release]
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Select compiler
shell: bash
run: |
if [[ "${{ matrix.compiler }}" == "gcc" ]]; then
echo "CC=gcc" >> "$GITHUB_ENV"
echo "CXX=g++" >> "$GITHUB_ENV"
else
echo "CC=clang" >> "$GITHUB_ENV"
echo "CXX=clang++" >> "$GITHUB_ENV"
fi

- name: Configure
run: >-
cmake -S . -B build/verification
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
-DLIDCAVITY_ENABLE_SANITIZERS=${{ matrix.build_type == 'Debug' && 'ON' || 'OFF' }}

- name: Build
run: cmake --build build/verification --parallel

- name: Test
run: ctest --test-dir build/verification --output-on-failure
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Build artifacts
bin/
build/
cmake-build-*/
CMakeFiles/
CMakeCache.txt
cmake_install.cmake
CTestTestfile.cmake
Makefile
*.o
*.out
*.exe
Expand Down
41 changes: 41 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
cmake_minimum_required(VERSION 3.16)
project(LidCavityCPPVerification LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

option(LIDCAVITY_ENABLE_SANITIZERS "Enable AddressSanitizer and UndefinedBehaviorSanitizer" OFF)

add_library(lidcavity_verification
src/verification/operators.cpp
src/verification/poisson.cpp
src/verification/convergence.cpp
)

target_include_directories(lidcavity_verification PUBLIC include)
target_compile_options(lidcavity_verification PRIVATE
-Wall -Wextra -Wpedantic -Wconversion -Wshadow
)

if(LIDCAVITY_ENABLE_SANITIZERS AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(lidcavity_verification PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
target_link_options(lidcavity_verification PRIVATE -fsanitize=address,undefined)
endif()

enable_testing()

function(add_lidcavity_test target source)
add_executable(${target} ${source})
target_link_libraries(${target} PRIVATE lidcavity_verification)
target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic -Wconversion -Wshadow)
if(LIDCAVITY_ENABLE_SANITIZERS AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${target} PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
target_link_options(${target} PRIVATE -fsanitize=address,undefined)
endif()
add_test(NAME ${target} COMMAND ${target})
endfunction()

add_lidcavity_test(test_operators tests/test_operators.cpp)
add_lidcavity_test(test_poisson tests/test_poisson.cpp)
add_lidcavity_test(test_convergence tests/test_convergence.cpp)
92 changes: 92 additions & 0 deletions docs/PHASE2_VERIFICATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Phase 2: Verification and Convergence Contract

This phase adds a verification layer around the standalone C++ lid-driven-cavity project before the existing production solver is numerically changed.

## Why this is needed

The original 36-case study completed its configured executions, but those runs reached their maximum outer-iteration limits. Execution completion, benchmark-profile agreement, and iterative convergence are different results and must remain separate.

## Convergence contract

A future cavity run may report `converged` only when all of the following hold:

- the velocity-update Linf residual is below its dimensionless tolerance;
- the local divergence Linf residual is below its dimensionless tolerance;
- the local divergence L2 residual is below its dimensionless tolerance;
- the integrated global mass imbalance is below its tolerance;
- the pressure equation converged for the current outer iteration;
- all conditions remain satisfied for a configured number of consecutive iterations;
- all fields and metrics are finite.

The explicit solver statuses are:

- `running`
- `converged`
- `max_iterations`
- `pressure_not_converged`
- `stagnated`
- `diverged`
- `non_finite`

The `max_iterations` status must never be interpreted as convergence.

## Verification tests added

### Operator compatibility

The verification library uses a forward pressure gradient and backward divergence pair. In the interior, their composition is checked against the standard five-point Laplacian:

```text
D(G(phi)) = L(phi)
```

The test also checks constant-field gradients, zero-field divergence, and non-finite-value detection.

### Manufactured Poisson solution

The Poisson verification problem uses

```text
phi(x,y) = sin(pi x) sin(pi y)
```

with homogeneous Dirichlet boundaries and

```text
laplacian(phi) = -2 pi^2 sin(pi x) sin(pi y).
```

The tests verify:

- convergence on 17x17, 33x33, and 65x65 grids;
- approximately second-order spatial convergence;
- agreement between RBGS and RBSOR solutions;
- true equation-residual reduction.

### Convergence-state logic

The convergence tracker is tested independently for:

- minimum-iteration protection;
- consecutive-pass requirements;
- repeated pressure-solver failures;
- non-finite residuals;
- maximum-iteration termination.

## Running the checks

```bash
bash scripts/run_phase2_verification.sh
```

Or directly:

```bash
cmake -S . -B build/phase2-verification -DCMAKE_BUILD_TYPE=Release
cmake --build build/phase2-verification --parallel
ctest --test-dir build/phase2-verification --output-on-failure
```

## Next integration step

The verification library intentionally does not silently change the existing full-study results. The next step is to integrate the convergence tracker and dimensionless residual definitions into `src/lid_cavity.cpp`, then tune one canonical case (`N=32`, `Re=100`, upwind, RBSOR) before regenerating the 36-case study.
60 changes: 60 additions & 0 deletions include/lidcavity/convergence.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#pragma once

#include <cstddef>
#include <string>

namespace lidcavity {

enum class SolverStatus {
running,
converged,
max_iterations,
pressure_not_converged,
stagnated,
diverged,
non_finite
};

struct ConvergenceCriteria {
double velocity_update_linf = 5e-7;
double divergence_linf = 2e-3;
double divergence_l2 = 5e-4;
double global_mass_imbalance = 1e-8;
std::size_t minimum_iterations = 100;
std::size_t consecutive_passes = 10;
std::size_t maximum_consecutive_pressure_failures = 3;
double diverged_limit = 1e6;
};

struct ConvergenceMetrics {
double velocity_update_linf = 0.0;
double divergence_linf = 0.0;
double divergence_l2 = 0.0;
double global_mass_imbalance = 0.0;
bool pressure_converged = true;
bool finite = true;
};

class ConvergenceTracker {
public:
explicit ConvergenceTracker(ConvergenceCriteria criteria = {});

SolverStatus update(std::size_t iteration, const ConvergenceMetrics& metrics);
SolverStatus finish_at_max_iterations();

[[nodiscard]] SolverStatus status() const noexcept { return status_; }
[[nodiscard]] std::size_t consecutive_passes() const noexcept { return consecutive_passes_; }
[[nodiscard]] std::size_t consecutive_pressure_failures() const noexcept {
return consecutive_pressure_failures_;
}

private:
ConvergenceCriteria criteria_;
SolverStatus status_ = SolverStatus::running;
std::size_t consecutive_passes_ = 0;
std::size_t consecutive_pressure_failures_ = 0;
};

[[nodiscard]] std::string to_string(SolverStatus status);

} // namespace lidcavity
40 changes: 40 additions & 0 deletions include/lidcavity/field.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#pragma once

#include <cstddef>
#include <stdexcept>
#include <vector>

namespace lidcavity {

class Field2D {
public:
Field2D() = default;
Field2D(std::size_t ny, std::size_t nx, double value = 0.0)
: ny_(ny), nx_(nx), values_(ny * nx, value) {
if (ny < 2 || nx < 2) {
throw std::invalid_argument("Field2D requires at least 2x2 points");
}
}

[[nodiscard]] std::size_t nx() const noexcept { return nx_; }
[[nodiscard]] std::size_t ny() const noexcept { return ny_; }
[[nodiscard]] std::size_t size() const noexcept { return values_.size(); }

double& operator()(std::size_t i, std::size_t j) {
return values_.at(i * nx_ + j);
}

double operator()(std::size_t i, std::size_t j) const {
return values_.at(i * nx_ + j);
}

[[nodiscard]] const std::vector<double>& values() const noexcept { return values_; }
[[nodiscard]] std::vector<double>& values() noexcept { return values_; }

private:
std::size_t ny_ = 0;
std::size_t nx_ = 0;
std::vector<double> values_;
};

} // namespace lidcavity
21 changes: 21 additions & 0 deletions include/lidcavity/operators.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include "lidcavity/field.hpp"

namespace lidcavity {

struct VectorField2D {
Field2D x;
Field2D y;
};

[[nodiscard]] VectorField2D forward_gradient(const Field2D& scalar, double dx, double dy);
[[nodiscard]] Field2D backward_divergence(const VectorField2D& vector, double dx, double dy);
[[nodiscard]] Field2D compatible_laplacian(const Field2D& scalar, double dx, double dy);
[[nodiscard]] Field2D five_point_laplacian(const Field2D& scalar, double dx, double dy);

[[nodiscard]] double linf_norm(const Field2D& field);
[[nodiscard]] double l2_norm(const Field2D& field);
[[nodiscard]] bool all_finite(const Field2D& field);

} // namespace lidcavity
45 changes: 45 additions & 0 deletions include/lidcavity/poisson.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#pragma once

#include <cstddef>
#include <string>

#include "lidcavity/field.hpp"

namespace lidcavity {

enum class PoissonMethod { rbgs, rbsor };

struct PoissonOptions {
PoissonMethod method = PoissonMethod::rbsor;
std::size_t max_iterations = 20000;
double absolute_tolerance = 1e-12;
double relative_tolerance = 1e-10;
double omega = 1.7;
std::size_t check_every = 10;
};

struct PoissonResult {
Field2D solution;
std::size_t iterations = 0;
bool converged = false;
double absolute_residual = 0.0;
double relative_residual = 0.0;
};

[[nodiscard]] PoissonResult solve_poisson_dirichlet(
const Field2D& rhs,
double dx,
double dy,
const PoissonOptions& options = {}
);

[[nodiscard]] double poisson_residual_linf(
const Field2D& solution,
const Field2D& rhs,
double dx,
double dy
);

[[nodiscard]] std::string to_string(PoissonMethod method);

} // namespace lidcavity
7 changes: 7 additions & 0 deletions scripts/run_phase2_verification.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail

build_dir="${1:-build/phase2-verification}"
cmake -S . -B "$build_dir" -DCMAKE_BUILD_TYPE=Release
cmake --build "$build_dir" --parallel
ctest --test-dir "$build_dir" --output-on-failure
Loading
Loading