Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
be593ad
Create initial draft for OpenImpala paper
jameslehoux Mar 29, 2026
e889e25
Add bibliography entries for various research articles
jameslehoux Mar 29, 2026
d756f42
Revise software architecture and capabilities section
jameslehoux Mar 29, 2026
395b8ec
Add performance profiling and CI benchmark workflow (#31)
jameslehoux Mar 29, 2026
23c770c
Fix PyPI version rejection: add setuptools_scm config
jameslehoux Mar 29, 2026
3c87678
Fix GPU wheels versioned as 4.0.3.dev0 instead of release tag version
Mar 30, 2026
1af3272
Add pre-existing paper and figure files
Mar 30, 2026
79c85c8
Polish JOSS paper for submission readiness
Mar 30, 2026
9a3a7df
Make version extraction robust for workflow_dispatch triggers
Mar 30, 2026
03ea250
Debug GPU wheel PyPI upload: enable verbose, disable attestations
Mar 30, 2026
9c8b9f0
Add synthetic REV study test to cover REVStudy.cpp (0% → ~60%)
Mar 31, 2026
f02485f
Update authorship and acknowledgements in paper.md
jameslehoux Mar 31, 2026
64da791
Update maintainer email in Singularity.deps.def
jameslehoux Mar 31, 2026
21d8a72
Update OpenImpala repository and branch in Dockerfile
jameslehoux Mar 31, 2026
c61c1a0
Add Diffusion.cpp and RawReader test coverage variants
Mar 31, 2026
60f3440
Upload GPU wheels to GitHub Release instead of PyPI
Mar 31, 2026
18ad3ea
Update GPU install instructions to use GitHub Releases
Mar 31, 2026
169e23b
Fix clang-format violations in tSyntheticREVStudy.cpp
Mar 31, 2026
361b3b9
Add percolation pre-flight check and memory estimator (#224)
Mar 31, 2026
ba57b4d
Add yt visualization tutorial for AMReX plotfiles (#215)
Mar 31, 2026
aabe8ca
Add sphere packing V&V script for HS bound validation
Apr 1, 2026
766149a
Fix tDiffusion_tortuosity test: use valid calculation_method value
Apr 1, 2026
25d8094
Add Berea sandstone experimental validation script
Apr 1, 2026
a6df125
Fix gcovr crash on suspicious hit counts from GCC bug
Apr 1, 2026
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
306 changes: 306 additions & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
# .github/workflows/benchmark.yml
# Performance regression tracking for OpenImpala.
#
# Runs a fixed set of synthetic benchmarks and posts timing results
# as a PR comment or job summary. Triggered manually or on PRs that
# touch solver code.

name: Performance Benchmark

on:
pull_request:
paths:
- 'src/props/**'
- 'python/bindings/**'
- 'python/openimpala/**'
- 'CMakeLists.txt'
workflow_dispatch:
inputs:
grid_sizes:
description: 'Comma-separated grid sizes to benchmark (e.g. 64,128)'
required: false
default: '64,128'

concurrency:
group: benchmark-${{ github.ref }}
cancel-in-progress: true

jobs:
benchmark:
name: Run Benchmarks
runs-on: ubuntu-latest
permissions:
pull-requests: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Apptainer
uses: eWaterCycle/setup-apptainer@v2
with:
apptainer-version: 1.2.5

- name: Restore Dependency SIF Cache
id: cache-restore-sif
uses: actions/cache/restore@v4
with:
path: dependency_image.sif
key: ${{ runner.os }}-apptainer-sif-${{ hashFiles('containers/Singularity.deps.def') }}
restore-keys: |
${{ runner.os }}-apptainer-sif-

- name: Build Dependency SIF (if cache miss)
if: steps.cache-restore-sif.outputs.cache-hit != 'true'
run: |
sudo apptainer build --force dependency_image.sif containers/Singularity.deps.def

- name: Build OpenImpala with Python bindings
run: |
sudo apptainer exec \
--writable-tmpfs \
--bind $PWD:/src \
./dependency_image.sif \
bash -c '
source /opt/rh/gcc-toolset-11/enable
cd /src
rm -rf cmake_build_bench && mkdir cmake_build_bench && cd cmake_build_bench

PYBIND11_DIR=$(python3.11 -m pybind11 --cmakedir)

cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=$(which mpicc) \
-DCMAKE_CXX_COMPILER=$(which mpicxx) \
-DPython_EXECUTABLE=/usr/bin/python3.11 \
-Dpybind11_DIR=${PYBIND11_DIR} \
-DCMAKE_PREFIX_PATH="/opt/amrex/25.03;/opt/hypre/v2.32.0;/opt/hdf5/1.12.3;/opt/libtiff/4.6.0" \
-DBUILD_TESTING=OFF \
-DOPENIMPALA_PYTHON=ON

make -j$(nproc)
'

- name: Run benchmark suite
id: benchmark
run: |
# Determine grid sizes
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
SIZES="${{ github.event.inputs.grid_sizes }}"
else
SIZES="64,128"
fi

cat > /tmp/bench.py << 'BENCHEOF'
import sys
import os
import json
import time
import numpy as np

# Add the build directory to Python path
sys.path.insert(0, "/src/cmake_build_bench")

import openimpala as oi

sizes = [int(s) for s in os.environ.get("BENCH_SIZES", "64,128").split(",")]
solvers = ["pcg", "flexgmres", "pfmg", "mlmg"]
n_repeats = 3
results = []

with oi.Session():
for size in sizes:
print(f"\n=== Benchmarking {size}³ ===", flush=True)

# Generate uniform block (analytical solution: tau = (N-1)/N)
data = np.zeros((size, size, size), dtype=np.int32)

for solver_name in solvers:
times = []
res = None
failed = False

for trial in range(n_repeats):
try:
t0 = time.perf_counter()
res = oi.tortuosity(
data, phase=0, direction="z",
solver=solver_name, max_grid_size=32, verbose=0
)
t1 = time.perf_counter()
times.append(t1 - t0)
except Exception as e:
print(f" {solver_name}: FAILED — {e}")
failed = True
break

if not failed and res is not None:
median_t = float(np.median(times))
expected_tau = (size - 1) / size
tau_error = abs(res.tortuosity - expected_tau) / expected_tau

record = {
"size": size,
"solver": solver_name,
"wall_time_s": round(median_t, 4),
"tortuosity": round(res.tortuosity, 6),
"expected_tau": round(expected_tau, 6),
"relative_error": round(tau_error, 8),
"iterations": res.iterations,
"converged": res.solver_converged,
}
results.append(record)
print(f" {solver_name:12s} {median_t:.4f}s "
f"τ={res.tortuosity:.6f} "
f"err={tau_error:.2e} "
f"iters={res.iterations}")
else:
results.append({
"size": size, "solver": solver_name,
"wall_time_s": None, "tortuosity": None,
"expected_tau": (size - 1) / size,
"relative_error": None, "iterations": None,
"converged": False,
})

# Write results to JSON
with open("/tmp/benchmark_results.json", "w") as f:
json.dump(results, f, indent=2)

print(f"\n{len(results)} benchmark results written.")
BENCHEOF

sudo apptainer exec \
--writable-tmpfs \
--bind $PWD:/src \
./dependency_image.sif \
bash -c "
source /opt/rh/gcc-toolset-11/enable
export PYTHONPATH=/src/cmake_build_bench
export BENCH_SIZES=$SIZES
cd /src && python3.11 /tmp/bench.py
"

# Copy results out for the next step
cp /tmp/benchmark_results.json ./benchmark_results.json 2>/dev/null || true

- name: Generate benchmark report
id: report
run: |
python3 << 'REPORTEOF'
import json
import os

with open("benchmark_results.json") as f:
results = json.load(f)

# Build markdown table
lines = []
lines.append("## Performance Benchmark Results")
lines.append("")
lines.append("| Size | Solver | Wall Time (s) | Tortuosity | Expected | Rel. Error | Iters | Status |")
lines.append("|------|--------|--------------|------------|----------|-----------|-------|--------|")

any_failed = False
any_regression = False

for r in results:
if r["converged"]:
err_str = f'{r["relative_error"]:.2e}'
status = "PASS" if r["relative_error"] < 1e-4 else "WARN"
if r["relative_error"] >= 1e-4:
any_regression = True
status = "WARN"
else:
err_str = "N/A"
status = "FAIL"
any_failed = True

time_str = f'{r["wall_time_s"]:.4f}' if r["wall_time_s"] else "N/A"
tau_str = f'{r["tortuosity"]:.6f}' if r["tortuosity"] else "N/A"
exp_str = f'{r["expected_tau"]:.6f}'
iter_str = str(r["iterations"]) if r["iterations"] else "N/A"

lines.append(f'| {r["size"]}³ | {r["solver"]} | {time_str} | {tau_str} | {exp_str} | {err_str} | {iter_str} | {status} |')

lines.append("")

# Summary
converged = [r for r in results if r["converged"]]
if converged:
fastest = min(converged, key=lambda r: r["wall_time_s"])
lines.append(f'**Fastest solver:** {fastest["solver"]} at {fastest["size"]}³ '
f'({fastest["wall_time_s"]:.4f}s)')
if any_failed:
lines.append("**Warning:** Some solvers failed to converge.")
if any_regression:
lines.append("**Warning:** Some results exceed expected error tolerance (>1e-4).")

lines.append("")
lines.append("*Benchmark: uniform block (analytical τ = (N-1)/N)*")

report = "\n".join(lines)

# Write to step summary
with open(os.environ.get("GITHUB_STEP_SUMMARY", "/dev/null"), "a") as f:
f.write(report)

# Write to file for PR comment
with open("benchmark_report.md", "w") as f:
f.write(report)

print(report)
REPORTEOF

- name: Post benchmark results to PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let report = '';
try {
report = fs.readFileSync('benchmark_report.md', 'utf8');
} catch (e) {
report = 'Benchmark report not available.';
}

const marker = '<!-- benchmark-report -->';
const body = marker + '\n' + report;

const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const existing = comments.find(c => c.body.includes(marker));

if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body,
});
}

- name: Upload benchmark artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: benchmark-results-${{ github.run_id }}
path: |
benchmark_results.json
benchmark_report.md
retention-days: 90
if-no-files-found: warn
1 change: 1 addition & 0 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ jobs:
gcovr --root . \
--filter "src/" \
--print-summary \
--gcov-ignore-parse-errors=suspicious_hits.warn_once_per_file \
--xml coverage.xml \
--txt coverage.txt \
cmake_build_cov/
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/pypi-wheels-cpu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ jobs:
submodules: recursive # Fetches Catch2, nlohmann/json, or pybind11 if needed
fetch-depth: 0

- name: Extract version from tag
id: version
run: |
# For release events, GITHUB_REF_NAME is the tag (e.g. v4.0.2).
# For workflow_dispatch, fall back to git describe.
if [[ "$GITHUB_REF_NAME" == v* ]]; then
VERSION="${GITHUB_REF_NAME#v}"
else
VERSION="$(git describe --tags --abbrev=0 | sed 's/^v//')"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Resolved version: ${VERSION}"

- name: Set up Python
uses: actions/setup-python@v5
with:
Expand Down Expand Up @@ -130,6 +143,7 @@ jobs:
CMAKE_CXX_COMPILER="mpicxx"
CMAKE_PREFIX_PATH="/usr/local"
CMAKE_GENERATOR="Unix Makefiles"
SETUPTOOLS_SCM_PRETEND_VERSION="${{ steps.version.outputs.version }}"

# Vendor libraries into the wheel, but exclude host-specific MPI and
# runtime libraries that users must provide on their system.
Expand Down
Loading
Loading