diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
new file mode 100644
index 00000000..acd1f3cd
--- /dev/null
+++ b/.github/release-drafter.yml
@@ -0,0 +1,133 @@
+# Configuration for Release Drafter
+# https://github.com/release-drafter/release-drafter
+#
+# Automatically drafts GitHub release notes from merged PRs.
+# PRs are categorized by their labels into changelog sections.
+
+name-template: 'v$RESOLVED_VERSION'
+tag-template: 'v$RESOLVED_VERSION'
+
+# Determine the next version bump from PR labels
+version-resolver:
+ major:
+ labels:
+ - 'breaking'
+ minor:
+ labels:
+ - 'enhancement'
+ - 'feature'
+ - 'physics'
+ patch:
+ labels:
+ - 'bug'
+ - 'fix'
+ - 'performance'
+ - 'documentation'
+ - 'devops'
+ - 'dependencies'
+ default: patch
+
+# Map PR labels to changelog sections
+categories:
+ - title: '๐ฌ Physics & Solvers'
+ labels:
+ - 'physics'
+ - 'solver'
+ - title: '๐ New Features'
+ labels:
+ - 'feature'
+ - 'enhancement'
+ - title: 'โก Performance'
+ labels:
+ - 'performance'
+ - 'gpu'
+ - title: '๐ Bug Fixes'
+ labels:
+ - 'bug'
+ - 'fix'
+ - title: '๐ Documentation'
+ labels:
+ - 'documentation'
+ - title: '๐ง DevOps & CI'
+ labels:
+ - 'devops'
+ - 'ci'
+ - title: '๐ฆ Dependencies'
+ labels:
+ - 'dependencies'
+ - title: 'โ ๏ธ Breaking Changes'
+ labels:
+ - 'breaking'
+ - title: '๐งน Maintenance'
+ labels:
+ - 'maintenance'
+ - 'refactor'
+
+# Exclude PRs with these labels from release notes
+exclude-labels:
+ - 'skip-changelog'
+
+# Template for the release body
+template: |
+ ## What's Changed
+
+ $CHANGES
+
+ **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
+
+# Auto-label PRs based on file paths
+autolabeler:
+ - label: 'physics'
+ files:
+ - 'src/props/**'
+ title:
+ - '/tortuosity/i'
+ - '/diffusiv/i'
+ - '/solver/i'
+ - '/HYPRE/i'
+ - '/MLMG/i'
+ - label: 'io'
+ files:
+ - 'src/io/**'
+ title:
+ - '/reader/i'
+ - '/TIFF/i'
+ - '/HDF5/i'
+ - label: 'documentation'
+ files:
+ - 'docs/**'
+ - '*.md'
+ - 'Doxyfile'
+ title:
+ - '/docs/i'
+ - '/documentation/i'
+ - label: 'devops'
+ files:
+ - '.github/**'
+ - 'containers/**'
+ - 'pyproject.toml'
+ - 'CMakeLists.txt'
+ title:
+ - '/CI/i'
+ - '/workflow/i'
+ - '/wheel/i'
+ - '/PyPI/i'
+ - label: 'gpu'
+ files:
+ - 'src/props/*GPU*'
+ - 'src/props/*CUDA*'
+ title:
+ - '/CUDA/i'
+ - '/GPU/i'
+ - '/NVCC/i'
+ - label: 'python'
+ files:
+ - 'python/**'
+ title:
+ - '/python/i'
+ - '/pybind/i'
+ - '/binding/i'
+ - label: 'tests'
+ files:
+ - 'tests/**'
+ - 'python/tests/**'
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 9e59ba82..0f45bb0c 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -1,11 +1,12 @@
# .github/workflows/docs.yml
-name: Deploy Doxygen Documentation
+name: Deploy Documentation
on:
push:
branches: [master]
paths:
- 'src/**'
+ - 'python/**'
- 'Doxyfile'
- 'docs/**'
- '.github/workflows/docs.yml'
@@ -24,7 +25,7 @@ concurrency:
jobs:
build-docs:
- name: Build Doxygen Documentation
+ name: Build Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -35,15 +36,28 @@ jobs:
sudo apt-get update
sudo apt-get install -y doxygen graphviz
echo "Doxygen version: $(doxygen --version)"
- echo "Dot version: $(dot -V 2>&1)"
- - name: Generate documentation
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Install Sphinx and dependencies
+ run: pip install -r docs/requirements.txt
+
+ - name: Generate Doxygen XML and HTML
run: doxygen Doxyfile
+ - name: Build Sphinx documentation
+ run: sphinx-build -b html docs/ docs/_build/html
+
+ - name: Copy Doxygen HTML into Sphinx output
+ run: cp -r docs/doxygen/html docs/_build/html/doxygen
+
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
- path: docs/doxygen/html
+ path: docs/_build/html
deploy:
name: Deploy to GitHub Pages
diff --git a/.github/workflows/pypi-wheels-cpu.yml b/.github/workflows/pypi-wheels-cpu.yml
index e3f85ec6..9fd6762b 100644
--- a/.github/workflows/pypi-wheels-cpu.yml
+++ b/.github/workflows/pypi-wheels-cpu.yml
@@ -16,6 +16,7 @@ jobs:
uses: actions/checkout@v4
with:
submodules: recursive # Fetches Catch2, nlohmann/json, or pybind11 if needed
+ fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
diff --git a/.github/workflows/pypi-wheels-gpu.yml b/.github/workflows/pypi-wheels-gpu.yml
index 82b22274..f7023bf7 100644
--- a/.github/workflows/pypi-wheels-gpu.yml
+++ b/.github/workflows/pypi-wheels-gpu.yml
@@ -16,6 +16,7 @@ jobs:
uses: actions/checkout@v4
with:
submodules: recursive
+ fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml
new file mode 100644
index 00000000..9d6097b9
--- /dev/null
+++ b/.github/workflows/release-drafter.yml
@@ -0,0 +1,21 @@
+# .github/workflows/release-drafter.yml
+name: Release Drafter
+
+on:
+ push:
+ branches:
+ - master
+ pull_request_target:
+ types: [opened, reopened, synchronize]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ update-release-draft:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: release-drafter/release-drafter@v6
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0c5db227..44e4f6c2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -15,6 +15,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- name: Set up Apptainer
uses: eWaterCycle/setup-apptainer@v2
@@ -121,10 +123,14 @@ jobs:
sudo apptainer build "$SIF_FILENAME" Singularity.final.def
echo "SIF_FILENAME=$SIF_FILENAME" >> $GITHUB_ENV
- - name: Create GitHub Release and Upload SIF
+ - name: Upload SIF to GitHub Release
uses: softprops/action-gh-release@v2
with:
files: ${{ env.SIF_FILENAME }}
+ # Release notes are pre-populated by Release Drafter.
+ # Only fall back to auto-generated notes if the body is empty
+ # (e.g. tag was pushed without a prior draft).
generate_release_notes: true
+ append_body: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 16aca611..a0e29311 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -15,8 +15,37 @@
cmake_minimum_required(VERSION 3.18)
+# ---------------------------------------------------------------------------
+# Derive project version from the latest Git tag (e.g. v4.0.1 โ 4.0.1).
+# Falls back to 0.0.0 when building outside a Git repository or when no
+# tag is reachable (e.g. shallow clone without --tags).
+# ---------------------------------------------------------------------------
+set(OPENIMPALA_FALLBACK_VERSION "4.0.1")
+
+find_package(Git QUIET)
+if(GIT_FOUND)
+ execute_process(
+ COMMAND "${GIT_EXECUTABLE}" describe --tags --match "v[0-9]*" --abbrev=0
+ WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
+ OUTPUT_VARIABLE _git_tag
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ ERROR_QUIET
+ RESULT_VARIABLE _git_result
+ )
+ if(_git_result EQUAL 0 AND _git_tag MATCHES "^v([0-9]+\\.[0-9]+\\.[0-9]+)")
+ set(_detected_version "${CMAKE_MATCH_1}")
+ endif()
+endif()
+
+if(NOT _detected_version)
+ set(_detected_version "${OPENIMPALA_FALLBACK_VERSION}")
+ message(STATUS "Git tag not found โ using fallback version ${_detected_version}")
+else()
+ message(STATUS "Version from Git tag: ${_detected_version}")
+endif()
+
project(OpenImpala
- VERSION 0.1.0
+ VERSION ${_detected_version}
LANGUAGES C CXX Fortran
DESCRIPTION "Image-based simulation of transport properties in porous media"
)
diff --git a/Doxyfile b/Doxyfile
index cced28a7..770d29ed 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -64,7 +64,8 @@ FULL_SIDEBAR = NO
GENERATE_LATEX = NO
GENERATE_RTF = NO
GENERATE_MAN = NO
-GENERATE_XML = NO
+GENERATE_XML = YES
+XML_OUTPUT = xml
#---------------------------------------------------------------------------
# Graphs (requires Graphviz dot)
diff --git a/docs/api/cpp.md b/docs/api/cpp.md
new file mode 100644
index 00000000..ef7be91d
--- /dev/null
+++ b/docs/api/cpp.md
@@ -0,0 +1,75 @@
+# C++ API Reference
+
+The C++ API reference is generated from Doxygen comments in the source code
+using [Breathe](https://breathe.readthedocs.io/).
+
+## Namespace
+
+All OpenImpala classes live in the `OpenImpala` namespace.
+
+## Key classes
+
+### I/O Readers
+
+```{eval-rst}
+.. doxygenclass:: OpenImpala::TiffReader
+ :members:
+ :outline:
+
+.. doxygenclass:: OpenImpala::HDF5Reader
+ :members:
+ :outline:
+
+.. doxygenclass:: OpenImpala::RawReader
+ :members:
+ :outline:
+```
+
+### Transport Solvers
+
+```{eval-rst}
+.. doxygenclass:: OpenImpala::TortuosityHypre
+ :members:
+ :outline:
+
+.. doxygenclass:: OpenImpala::TortuosityMLMG
+ :members:
+ :outline:
+
+.. doxygenclass:: OpenImpala::EffectiveDiffusivityHypre
+ :members:
+ :outline:
+```
+
+### Utilities
+
+```{eval-rst}
+.. doxygenclass:: OpenImpala::VolumeFraction
+ :members:
+ :outline:
+
+.. doxygenclass:: OpenImpala::PercolationCheck
+ :members:
+ :outline:
+
+.. doxygenclass:: OpenImpala::TortuositySolverBase
+ :members:
+ :outline:
+
+.. doxygenclass:: OpenImpala::HypreStructSolver
+ :members:
+ :outline:
+```
+
+### Configuration
+
+```{eval-rst}
+.. doxygenstruct:: OpenImpala::PhysicsConfig
+ :members:
+ :outline:
+```
+
+## Full Doxygen output
+
+For the complete class hierarchy, include dependency graphs, and file-level
+documentation, see the [Doxygen pages](../doxygen/html/index.html).
diff --git a/docs/api/python.rst b/docs/api/python.rst
new file mode 100644
index 00000000..415e12f1
--- /dev/null
+++ b/docs/api/python.rst
@@ -0,0 +1,50 @@
+Python API Reference
+====================
+
+High-level API
+--------------
+
+The recommended interface for most users. These functions accept NumPy arrays
+and return Python dataclasses.
+
+.. autofunction:: openimpala.facade.volume_fraction
+
+.. autofunction:: openimpala.facade.percolation_check
+
+.. autofunction:: openimpala.facade.tortuosity
+
+.. autofunction:: openimpala.facade.read_image
+
+
+Result types
+~~~~~~~~~~~~
+
+.. autoclass:: openimpala.facade.VolumeFractionResult
+ :members:
+
+.. autoclass:: openimpala.facade.PercolationResult
+ :members:
+
+.. autoclass:: openimpala.facade.TortuosityResult
+ :members:
+
+
+Session management
+------------------
+
+.. autoclass:: openimpala.Session
+ :members:
+ :special-members: __enter__, __exit__
+
+
+Exceptions
+----------
+
+.. autoclass:: openimpala.OpenImpalaError
+ :members:
+
+.. autoclass:: openimpala.ConvergenceError
+ :members:
+
+.. autoclass:: openimpala.PercolationError
+ :members:
diff --git a/docs/changelog.md b/docs/changelog.md
new file mode 100644
index 00000000..5727d94d
--- /dev/null
+++ b/docs/changelog.md
@@ -0,0 +1,43 @@
+# Changelog
+
+## v4.0.0 (2026-03-29)
+
+Major release introducing GPU acceleration, a new matrix-free solver,
+comprehensive architectural refactoring, and expanded tutorials.
+
+### Highlights
+
+- **CUDA GPU acceleration** via `openimpala-cuda` PyPI package
+- **TortuosityMLMG solver** โ matrix-free AMReX geometric multigrid
+- **Microstructural parameterisation engine** โ SSA, REV study, PSD, connected components
+- **Fortran-to-C++ kernel migration** โ all compute kernels now native C++ AMReX lambdas
+- **7-part tutorial series** with Google Colab support
+
+See the full [release notes on GitHub](https://github.com/BASE-Laboratory/OpenImpala/releases/tag/v4.0.0).
+
+## v3.1.0 (2026-03-10)
+
+- Replaced `pyamrex` dependency with native C++ NumPy ingestion via `VoxelImage`
+- Self-contained PyPI wheels โ `pip install openimpala` with zero compilation
+- Memory-safe workflows: ingest data, free Python array, then solve
+
+## v3.0.0 โ v3.0.2
+
+- Python bindings via pybind11
+- CMake build system modernisation
+- scikit-build-core + cibuildwheel integration
+- Multi-phase transport support
+
+## v2.0.0 โ v2.1.1
+
+- AMReX upgrade and CI/CD pipeline
+- Catch2 test framework integration
+- Code coverage with Codecov
+- clang-format and clang-tidy enforcement
+
+## v1.0.0 โ v1.1.1
+
+- Initial public release
+- HYPRE-based tortuosity and effective diffusivity solvers
+- TIFF, HDF5, RAW, DAT image readers
+- Apptainer container builds
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 00000000..073bf280
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,87 @@
+# Configuration file for the Sphinx documentation builder.
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+import os
+import sys
+
+# -- Path setup ---------------------------------------------------------------
+# Add the Python package to sys.path so autodoc can find it.
+sys.path.insert(0, os.path.abspath(os.path.join("..", "python")))
+
+# -- Project information ------------------------------------------------------
+project = "OpenImpala"
+copyright = "2024-2026, BASE Laboratory, University of Greenwich"
+author = "James Le Houx"
+
+# Version is read from pyproject.toml at build time; fallback for local builds.
+try:
+ from importlib.metadata import version as _version
+
+ release = _version("openimpala")
+except Exception:
+ release = "4.0.0"
+version = ".".join(release.split(".")[:2])
+
+# -- General configuration ----------------------------------------------------
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.autosummary",
+ "sphinx.ext.napoleon",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.viewcode",
+ "sphinx.ext.mathjax",
+ "breathe",
+ "myst_parser",
+]
+
+templates_path = ["_templates"]
+exclude_patterns = ["_build", "doxygen", "Thumbs.db", ".DS_Store"]
+
+# -- MyST (Markdown) settings -------------------------------------------------
+myst_enable_extensions = [
+ "colon_fence",
+ "deflist",
+ "fieldlist",
+]
+source_suffix = {
+ ".rst": "restructuredtext",
+ ".md": "markdown",
+}
+
+# -- Breathe (Doxygen bridge) -------------------------------------------------
+breathe_projects = {"OpenImpala": os.path.abspath("doxygen/xml")}
+breathe_default_project = "OpenImpala"
+
+# -- Autodoc settings ---------------------------------------------------------
+autodoc_mock_imports = ["openimpala._core", "mpi4py"]
+autodoc_member_order = "bysource"
+autodoc_typehints = "description"
+
+# -- Napoleon (Google/NumPy docstrings) ----------------------------------------
+napoleon_google_docstring = False
+napoleon_numpy_docstring = True
+napoleon_use_rtype = False
+
+# -- Intersphinx (cross-project links) ----------------------------------------
+intersphinx_mapping = {
+ "python": ("https://docs.python.org/3", None),
+ "numpy": ("https://numpy.org/doc/stable/", None),
+}
+
+# -- HTML output ---------------------------------------------------------------
+html_theme = "furo"
+html_title = "OpenImpala"
+html_static_path = ["_static"]
+
+html_theme_options = {
+ "source_repository": "https://github.com/BASE-Laboratory/OpenImpala",
+ "source_branch": "master",
+ "source_directory": "docs/",
+ "light_css_variables": {
+ "color-brand-primary": "#2962FF",
+ "color-brand-content": "#2962FF",
+ },
+}
+
+# -- Autosummary ---------------------------------------------------------------
+autosummary_generate = True
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 00000000..d1e6f2bd
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,69 @@
+# Contributing
+
+Contributions to OpenImpala are welcome. This guide covers the development
+workflow and coding standards.
+
+## Development setup
+
+```bash
+git clone https://github.com/BASE-Laboratory/OpenImpala.git
+cd OpenImpala
+git checkout -b my-feature
+
+# Build (inside the dependency container)
+apptainer exec --bind "$(pwd):/src" dependency_image.sif bash -c "cd /src && make all -j"
+
+# Run tests
+apptainer exec --bind "$(pwd):/src" dependency_image.sif bash -c "cd /src && make test"
+```
+
+## Pull request workflow
+
+1. Fork the repository and create a feature branch
+2. Make your changes, ensuring tests pass
+3. Run `clang-format` on modified files
+4. Submit a pull request against `master`
+
+## Code style
+
+- **C++17**, 100-column line limit, 4-space indentation
+- LLVM-based style enforced by `.clang-format`
+- All code in `namespace OpenImpala`
+- Headers use `#ifndef` include guards (not `#pragma once`)
+- Doxygen `@file` / `@brief` / `@param` comments on all public APIs
+- Fortran files are **not** processed by clang-format
+
+### Formatting check
+
+```bash
+# Check formatting (CI runs this automatically)
+find src/ tests/ python/bindings/ -type f \( -name "*.cpp" -o -name "*.H" \) \
+ | xargs clang-format --dry-run --Werror
+
+# Auto-format
+find src/ tests/ python/bindings/ -type f \( -name "*.cpp" -o -name "*.H" \) \
+ | xargs clang-format -i
+```
+
+## Testing
+
+- **C++ tests:** CTest with Catch2 (run via `ctest --output-on-failure`)
+- **Python tests:** pytest (`python -m pytest python/tests/`)
+- **Analytical benchmarks:** Uniform block, series layers, parallel layers with
+ exact solutions
+
+## Building documentation
+
+```bash
+# Install doc dependencies
+pip install -r docs/requirements.txt
+
+# Generate Doxygen XML (needed by Breathe)
+doxygen Doxyfile
+
+# Build Sphinx HTML
+sphinx-build -b html docs/ docs/_build/html
+
+# View locally
+open docs/_build/html/index.html
+```
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 00000000..a0ba9d08
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,114 @@
+# Getting Started
+
+## Installation
+
+### Python (recommended)
+
+OpenImpala is available on PyPI as pre-compiled wheels โ no compilation required.
+
+```bash
+# CPU version (works everywhere)
+pip install openimpala
+
+# GPU version (requires NVIDIA CUDA runtime)
+pip install openimpala-cuda
+```
+
+**Requirements:** Python 3.8+ and NumPy. Optional: `mpi4py` for MPI parallelism.
+
+### Container (HPC)
+
+For HPC clusters, download the pre-built Apptainer/Singularity container from
+[GitHub Releases](https://github.com/BASE-Laboratory/OpenImpala/releases):
+
+```bash
+# Download the latest .sif file
+wget https://github.com/BASE-Laboratory/OpenImpala/releases/latest/download/openimpala-v4.0.0.sif
+
+# Run interactively
+apptainer shell openimpala-v4.0.0.sif
+
+# Run a simulation
+apptainer exec openimpala-v4.0.0.sif /opt/OpenImpala/build/Diffusion3d inputs
+```
+
+### From source (developers)
+
+```bash
+git clone https://github.com/BASE-Laboratory/OpenImpala.git
+cd OpenImpala
+mkdir build && cd build
+cmake .. -DCMAKE_BUILD_TYPE=Release \
+ -DCMAKE_CXX_COMPILER=$(which mpicxx) \
+ -DCMAKE_Fortran_COMPILER=$(which mpif90)
+make -j$(nproc)
+ctest --output-on-failure
+```
+
+Dependencies: AMReX, HYPRE, HDF5, LibTIFF. See the
+[README](https://github.com/BASE-Laboratory/OpenImpala#native-installation-advanced)
+for full details.
+
+## Your first simulation
+
+```python
+import numpy as np
+import openimpala as oi
+
+# Create a simple porous medium (random 50/50 mix)
+data = np.random.choice([0, 1], size=(64, 64, 64), dtype=np.int32)
+
+with oi.Session():
+ # Volume fraction
+ vf = oi.volume_fraction(data, phase=1)
+ print(f"Volume fraction: {vf.fraction:.4f}")
+
+ # Percolation check
+ perc = oi.percolation_check(data, phase=1, direction="z")
+ print(f"Percolates: {perc.percolates}")
+
+ # Tortuosity (only if phase percolates)
+ if perc.percolates:
+ result = oi.tortuosity(data, phase=1, direction="z")
+ print(f"Tortuosity: {result.tortuosity:.4f}")
+```
+
+All computation happens inside the `oi.Session()` context manager, which
+manages the AMReX and MPI lifecycle.
+
+## Working with real images
+
+OpenImpala reads TIFF stacks, HDF5, and raw binary files:
+
+```python
+import openimpala as oi
+
+with oi.Session():
+ reader, img = oi.read_image("sample.tiff", threshold=128)
+ result = oi.tortuosity(img, phase=1, direction="z")
+```
+
+## Memory-safe workflows
+
+For large datasets, free the Python array before solving:
+
+```python
+import gc
+import numpy as np
+import openimpala as oi
+
+with oi.Session():
+ arr = np.load("large_volume.npy")
+ dataset = oi.core.VoxelImage.from_numpy(arr)
+
+ del arr
+ gc.collect() # Free Python memory
+
+ result = oi.tortuosity(dataset, phase=1, direction="z")
+```
+
+## Next steps
+
+- {doc}`user-guide/concepts` โ Understand tortuosity, effective diffusivity, and the mathematics
+- {doc}`user-guide/solvers` โ Choose the right solver for your problem
+- {doc}`tutorials/index` โ Interactive Colab notebooks
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 00000000..8c542460
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,76 @@
+OpenImpala Documentation
+========================
+
+**OpenImpala** is a high-performance framework for computing effective transport
+properties (diffusivity, conductivity, tortuosity) directly on 3D voxel images
+of porous microstructures.
+
+It solves steady-state transport equations on the voxel grid using finite
+differences, parallelised via MPI through the `AMReX `_
+library, with `HYPRE `_
+or AMReX MLMG for linear solves.
+
+.. code-block:: python
+
+ import numpy as np
+ import openimpala as oi
+
+ data = np.random.choice([0, 1], size=(64, 64, 64), dtype=np.int32)
+
+ with oi.Session():
+ result = oi.tortuosity(data, phase=1, direction="z")
+ print(f"Tortuosity: {result.tortuosity:.4f}")
+
+Install from PyPI
+-----------------
+
+.. code-block:: bash
+
+ # CPU version
+ pip install openimpala
+
+ # GPU version (NVIDIA CUDA)
+ pip install openimpala-cuda
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Getting Started
+
+ getting-started
+
+.. toctree::
+ :maxdepth: 2
+ :caption: User Guide
+
+ user-guide/concepts
+ user-guide/solvers
+ user-guide/input-files
+ user-guide/gpu
+ user-guide/hpc
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Tutorials
+
+ tutorials/index
+
+.. toctree::
+ :maxdepth: 2
+ :caption: API Reference
+
+ api/python
+ api/cpp
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Development
+
+ contributing
+ changelog
+
+
+Indices and tables
+------------------
+
+* :ref:`genindex`
+* :ref:`search`
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 00000000..07acf003
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,5 @@
+sphinx>=7.0
+furo
+breathe
+myst-parser
+sphinx-autodoc-typehints
diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md
new file mode 100644
index 00000000..1dfdc108
--- /dev/null
+++ b/docs/tutorials/index.md
@@ -0,0 +1,25 @@
+# Tutorials
+
+Interactive Jupyter notebooks that run directly in Google Colab โ no local
+installation required.
+
+## Tutorial series
+
+| # | Topic | Colab |
+|---|-------|-------|
+| 01 | [Hello OpenImpala](https://github.com/BASE-Laboratory/OpenImpala/blob/master/tutorials/01_hello_openimpala.ipynb) โ Installation, first tortuosity calculation | [](https://colab.research.google.com/github/BASE-Laboratory/OpenImpala/blob/master/tutorials/01_hello_openimpala.ipynb) |
+| 02 | [Digital Twin with PyBaMM](https://github.com/BASE-Laboratory/OpenImpala/blob/master/tutorials/02_digital_twin.ipynb) โ Battery electrode parameterisation | [](https://colab.research.google.com/github/BASE-Laboratory/OpenImpala/blob/master/tutorials/02_digital_twin.ipynb) |
+| 03 | [REV & Uncertainty](https://github.com/BASE-Laboratory/OpenImpala/blob/master/tutorials/03_rev_and_uncertainty.ipynb) โ Representative volume element analysis | [](https://colab.research.google.com/github/BASE-Laboratory/OpenImpala/blob/master/tutorials/03_rev_and_uncertainty.ipynb) |
+| 04 | [Multi-Phase Transport](https://github.com/BASE-Laboratory/OpenImpala/blob/master/tutorials/04_multi_phase_transport.ipynb) โ Heterogeneous media with multiple phases | [](https://colab.research.google.com/github/BASE-Laboratory/OpenImpala/blob/master/tutorials/04_multi_phase_transport.ipynb) |
+| 05 | [Surrogate Modelling](https://github.com/BASE-Laboratory/OpenImpala/blob/master/tutorials/05_surrogate_modelling.ipynb) โ ML surrogates for transport properties | [](https://colab.research.google.com/github/BASE-Laboratory/OpenImpala/blob/master/tutorials/05_surrogate_modelling.ipynb) |
+| 06 | [Topology Optimisation](https://github.com/BASE-Laboratory/OpenImpala/blob/master/tutorials/06_topology_optimisation.ipynb) โ Microstructure design | [](https://colab.research.google.com/github/BASE-Laboratory/OpenImpala/blob/master/tutorials/06_topology_optimisation.ipynb) |
+| 07 | [HPC Scaling](https://github.com/BASE-Laboratory/OpenImpala/blob/master/tutorials/07_hpc_scaling.ipynb) โ Performance analysis and scaling behaviour | [](https://colab.research.google.com/github/BASE-Laboratory/OpenImpala/blob/master/tutorials/07_hpc_scaling.ipynb) |
+
+## Running locally
+
+All tutorials can also run locally if you have OpenImpala installed:
+
+```bash
+pip install openimpala jupyter matplotlib
+jupyter notebook tutorials/01_hello_openimpala.ipynb
+```
diff --git a/docs/user-guide/concepts.md b/docs/user-guide/concepts.md
new file mode 100644
index 00000000..9ad16478
--- /dev/null
+++ b/docs/user-guide/concepts.md
@@ -0,0 +1,77 @@
+# Concepts
+
+## What OpenImpala computes
+
+OpenImpala takes a **segmented 3D voxel image** (where each voxel is labelled
+with a phase ID) and computes **effective transport properties** by solving
+partial differential equations directly on the voxel grid.
+
+### Phase data
+
+Images are segmented into integer phase IDs stored in an AMReX `iMultiFab`.
+Typically:
+
+- **Phase 0** = pore / void
+- **Phase 1** = solid matrix
+
+This is configurable via the `phase` parameter. Multi-phase transport is
+supported: each phase can be assigned a different transport coefficient.
+
+### Volume fraction
+
+The simplest metric: the fraction of voxels belonging to a given phase.
+
+$$\varepsilon = \frac{N_{\text{phase}}}{N_{\text{total}}}$$
+
+### Percolation
+
+Before solving transport equations, OpenImpala checks whether the target phase
+forms a **connected path** from inlet to outlet using a GPU-accelerated
+flood-fill algorithm. If the phase does not percolate, transport is zero.
+
+### Tortuosity
+
+Tortuosity quantifies how much a winding pore structure impedes transport
+compared to a straight channel. OpenImpala solves the steady-state diffusion
+equation:
+
+$$\nabla \cdot (D \nabla \phi) = 0$$
+
+with Dirichlet boundary conditions at inlet ($\phi = 0$) and outlet
+($\phi = 1$), and zero-flux Neumann conditions on lateral faces.
+
+The effective diffusivity is computed from the resulting flux:
+
+$$D_{\text{eff}} = \frac{|\text{average flux}|}{\text{cross-section area} \times |\nabla\phi_{\text{imposed}}|}$$
+
+Tortuosity is then:
+
+$$\tau = \frac{\varepsilon_{\text{active}}}{D_{\text{eff}}}$$
+
+where $\varepsilon_{\text{active}}$ is the volume fraction of the percolating
+(connected) phase.
+
+For a uniform medium on an $N$-cell grid, the discrete solution gives
+$D_{\text{eff}} = N/(N-1)$, so $\tau = (N-1)/N$.
+
+### Effective diffusivity tensor
+
+For anisotropic microstructures, the full effective diffusivity tensor
+$\mathbf{D}_{\text{eff}}$ is computed by solving the **cell problem** from
+homogenisation theory:
+
+$$\nabla_\xi \cdot \left( D \nabla_\xi \chi_k \right) = -\nabla_\xi \cdot \left( D \hat{e}_k \right)$$
+
+for corrector functions $\chi_k$ in each direction $k \in \{x, y, z\}$, with
+periodic boundary conditions. The tensor components are:
+
+$$D_{\text{eff},ij} = \frac{1}{|Y|} \int_Y D(\mathbf{x}) \left( \delta_{ij} + \frac{\partial \chi_j}{\partial x_i} \right) \, d\mathbf{x}$$
+
+### Face coefficients
+
+Inter-cell diffusivities use the **harmonic mean** of adjacent cell values:
+
+$$D_{\text{face}} = \frac{2 D_L D_R}{D_L + D_R}$$
+
+This is physically correct for resistances in series and ensures that a solid
+cell ($D = 0$) adjacent to a pore cell correctly blocks transport.
diff --git a/docs/user-guide/gpu.md b/docs/user-guide/gpu.md
new file mode 100644
index 00000000..2935757f
--- /dev/null
+++ b/docs/user-guide/gpu.md
@@ -0,0 +1,51 @@
+# GPU Acceleration
+
+OpenImpala supports NVIDIA GPU acceleration via CUDA. All compute kernels,
+flood fills, and solver loops are GPU-compatible.
+
+## Installation
+
+```bash
+pip install openimpala-cuda
+```
+
+The GPU wheel requires:
+- NVIDIA GPU with compute capability 7.0+ (Volta or newer)
+- CUDA runtime libraries (typically provided by the NVIDIA driver)
+
+The `openimpala-cuda` package is a drop-in replacement for `openimpala` โ the
+Python API is identical.
+
+## Usage
+
+No code changes are needed. The same Python scripts work on both CPU and GPU:
+
+```python
+import openimpala as oi
+import numpy as np
+
+data = np.random.choice([0, 1], size=(256, 256, 256), dtype=np.int32)
+
+with oi.Session():
+ result = oi.tortuosity(data, phase=1, direction="z")
+```
+
+When a GPU is available, AMReX automatically offloads `ParallelFor` kernels
+and HYPRE solver operations to the device.
+
+## What runs on GPU
+
+- Phase data lookup and coefficient field construction
+- Flood-fill percolation checks (atomic scatter-add)
+- HYPRE matrix assembly and linear solves
+- Solution extraction and flux integration
+- Through-thickness profile computation
+- Connected components labelling
+
+## Performance considerations
+
+- GPU acceleration provides the most benefit for large domains (>128^3)
+- For small problems, CPU may be faster due to kernel launch overhead
+- The MLMG solver currently runs on CPU only; use HYPRE solvers for GPU
+- Data transfer between host and device is minimised by keeping AMReX
+ data structures on the device throughout the solve
diff --git a/docs/user-guide/hpc.md b/docs/user-guide/hpc.md
new file mode 100644
index 00000000..6b355d73
--- /dev/null
+++ b/docs/user-guide/hpc.md
@@ -0,0 +1,77 @@
+# HPC Usage
+
+OpenImpala is designed for distributed-memory parallelism via MPI, making it
+suitable for large-scale simulations on HPC clusters.
+
+## Running with MPI
+
+### Python
+
+```bash
+# Install mpi4py
+pip install openimpala mpi4py
+
+# Run on 4 MPI ranks
+mpirun -np 4 python my_script.py
+```
+
+### C++ executable
+
+```bash
+mpirun -np 16 ./Diffusion3d inputs
+```
+
+### Apptainer on a cluster
+
+```bash
+mpirun -np 16 apptainer exec openimpala-v4.0.0.sif /opt/OpenImpala/build/Diffusion3d inputs
+```
+
+## SLURM batch script
+
+```bash
+#!/bin/bash
+#SBATCH --job-name=openimpala
+#SBATCH --nodes=2
+#SBATCH --ntasks-per-node=32
+#SBATCH --time=02:00:00
+#SBATCH --partition=compute
+
+module load mpi
+
+srun apptainer exec openimpala-v4.0.0.sif \
+ /opt/OpenImpala/build/Diffusion3d inputs
+```
+
+## Domain decomposition
+
+AMReX decomposes the 3D domain into boxes distributed across MPI ranks. The
+`max_grid_size` parameter controls the maximum box size:
+
+```ini
+amr.max_grid_size = 64
+```
+
+- **Smaller values** create more boxes, improving load balance across many ranks
+- **Larger values** reduce inter-rank communication but may cause load imbalance
+- Choose a power of 2 that evenly divides your domain dimensions
+
+## Scaling guidelines
+
+| Domain size | Recommended ranks | max_grid_size |
+|-------------|-------------------|---------------|
+| 128^3 | 1-4 | 64 |
+| 256^3 | 4-16 | 64 |
+| 512^3 | 16-64 | 64 |
+| 1024^3 | 64-256 | 128 |
+
+## Memory estimates
+
+Approximate memory per rank for a tortuosity solve:
+
+- Phase data: ~4 bytes/voxel (int32)
+- Solution field: ~8 bytes/voxel (float64)
+- HYPRE matrix: ~56 bytes/voxel (7-point stencil)
+- **Total: ~70 bytes/voxel**
+
+For a 512^3 domain on 64 ranks: ~140 MB per rank.
diff --git a/docs/user-guide/input-files.md b/docs/user-guide/input-files.md
new file mode 100644
index 00000000..08465d56
--- /dev/null
+++ b/docs/user-guide/input-files.md
@@ -0,0 +1,79 @@
+# Input Files
+
+When running OpenImpala from the command line (C++ executable), configuration
+is specified via AMReX `ParmParse` text files. The file is passed as the first
+argument:
+
+```bash
+./Diffusion3d inputs
+```
+
+## Example input file
+
+```ini
+# --- Image Input ---
+image.filename = microstructure.tiff
+image.threshold = 128
+
+# --- Solver Configuration ---
+tortuosity.direction = 2 # 0=X, 1=Y, 2=Z
+tortuosity.phase_id = 0 # Phase to solve for
+tortuosity.solver_type = PCG # PCG, FlexGMRES, GMRES, BiCGSTAB, SMG, PFMG
+
+# --- HYPRE Solver Parameters ---
+hypre.eps = 1.0e-9 # Convergence tolerance
+hypre.maxiter = 200 # Maximum iterations
+
+# --- AMReX Grid Configuration ---
+amr.max_grid_size = 64 # Box decomposition size
+
+# --- Output ---
+results.path = ./results # Output directory
+tortuosity.write_plotfile = false # Write AMReX plotfile of solution
+tortuosity.verbose = 1 # 0=silent, 1=basic, 2+=detailed
+```
+
+## Key parameters
+
+### Image input
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `image.filename` | string | Path to 3D image (TIFF, HDF5, RAW, DAT) |
+| `image.threshold` | float | Binarisation threshold value |
+| `image.hdf5_dataset` | string | HDF5 dataset path (default: `/data`) |
+
+### Solver
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `tortuosity.direction` | int | 0 | Flow direction: 0=X, 1=Y, 2=Z |
+| `tortuosity.phase_id` | int | 0 | Phase ID of the conducting phase |
+| `tortuosity.solver_type` | string | PCG | HYPRE solver algorithm |
+| `tortuosity.vlo` | float | 0.0 | Dirichlet BC at inlet |
+| `tortuosity.vhi` | float | 1.0 | Dirichlet BC at outlet |
+
+### Multi-phase transport
+
+```ini
+tortuosity.active_phases = 0 2 # Phase IDs with non-zero D
+tortuosity.phase_diffusivities = 1.0 0.5 # Corresponding D values
+```
+
+### Grid decomposition
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `amr.max_grid_size` | int | 32 | Maximum box size for domain decomposition |
+
+Smaller values create more boxes (better MPI load balance); larger values
+reduce communication overhead. Powers of 2 that divide the domain dimensions
+evenly are recommended.
+
+## Output files
+
+| File | Description |
+|------|-------------|
+| `results.json` | Structured JSON with all computed properties |
+| `results.txt` | Human-readable summary |
+| `plt_*/` | AMReX plotfile (if `write_plotfile = true`) |
diff --git a/docs/user-guide/solvers.md b/docs/user-guide/solvers.md
new file mode 100644
index 00000000..b8f7825a
--- /dev/null
+++ b/docs/user-guide/solvers.md
@@ -0,0 +1,71 @@
+# Solvers
+
+OpenImpala provides two solver backends for computing tortuosity, plus a
+legacy solver retained for comparison.
+
+## HYPRE solvers (default)
+
+The primary backend uses [HYPRE](https://computing.llnl.gov/projects/hypre-scalable-linear-solvers-multigrid-methods)
+structured-grid solvers. Available algorithms:
+
+| Solver | Type | Best for | Python name |
+|--------|------|----------|-------------|
+| **PCG** | Krylov (CG) | Single-phase diffusion (SPD systems) | `"pcg"` or `"auto"` |
+| FlexGMRES | Krylov | Multi-phase, non-symmetric problems | `"flexgmres"` |
+| GMRES | Krylov | General sparse systems | `"gmres"` |
+| BiCGSTAB | Krylov | Non-symmetric, when GMRES stalls | `"bicgstab"` |
+| SMG | Multigrid | Small grids, direct-like convergence | `"smg"` |
+| PFMG | Multigrid | Large grids, low memory | `"pfmg"` |
+
+**Default:** `"auto"` selects PCG, which is optimal for the single-phase
+steady-state diffusion problem (the Laplacian with harmonic-mean face
+coefficients is symmetric positive-definite).
+
+```python
+# Use the default (PCG)
+result = oi.tortuosity(data, phase=1, direction="z")
+
+# Explicitly choose a solver
+result = oi.tortuosity(data, phase=1, direction="z", solver="flexgmres")
+```
+
+## AMReX MLMG solver
+
+The matrix-free geometric multigrid solver uses AMReX's native
+`MLABecLaplacian` operator. Advantages:
+
+- **No matrix assembly** โ the operator is applied matrix-free
+- **Lower memory** โ approximately 3x less than HYPRE's `StructMatrix`
+- **Faster setup** โ no algebraic multigrid (AMG) construction
+
+Best for small-to-medium grids on shared-memory systems.
+
+```python
+result = oi.tortuosity(data, phase=1, direction="z", solver="mlmg")
+```
+
+## When to use which
+
+| Scenario | Recommended solver |
+|----------|--------------------|
+| Quick desktop analysis (<256^3) | `"mlmg"` |
+| Single-phase, any size | `"auto"` (PCG) |
+| Multi-phase with varying D | `"flexgmres"` |
+| Large distributed MPI runs | `"pcg"` or `"pfmg"` |
+| Debugging / comparison | `"smg"` (most robust) |
+
+## Effective diffusivity tensor
+
+The `EffectiveDiffusivityHypre` solver uses the same HYPRE backends but solves
+the cell problem with periodic boundary conditions. This is accessed via the
+C++ API or the command-line interface, not yet exposed in the high-level
+Python facade.
+
+## Solver parameters
+
+When using the C++ interface or input files, solver behaviour is controlled via:
+
+```
+hypre.eps = 1.0e-9 # Convergence tolerance
+hypre.maxiter = 200 # Maximum iterations
+```
diff --git a/pyproject.toml b/pyproject.toml
index 12c734ca..20441548 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,6 +25,13 @@ test = [
"pytest>=7",
"numpy",
]
+docs = [
+ "sphinx>=7.0",
+ "furo",
+ "breathe",
+ "myst-parser",
+ "sphinx-autodoc-typehints",
+]
all = [
"mpi4py",
"tqdm",