diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 000000000..0bfb055ac --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,37 @@ +name: openswmm.engine CodeQL config + +# security-extended adds higher-precision security queries beyond the +# default suite without pulling in the style/quality noise from +# security-and-quality. Adjust here if signal-to-noise needs tuning. +queries: + - uses: security-extended + +paths-ignore: + # ── EPA SWMM 5.x legacy tree ────────────────────────────────────────── + # The legacy C solver, output reader, and CLI must remain byte-for-byte + # identical to the upstream EPA reference to preserve the regression + # baseline (see src/legacy/engine/CMakeLists.txt). CodeQL findings here + # cannot be acted on, so we exclude the whole tree to keep the alert + # list focused on the new C++20 engine and Python bindings. + - src/legacy/** + + # ── Generated / build outputs ───────────────────────────────────────── + - build*/** + - cmake-build*/** + - python/build/** + - "**/_export.h" + - "**/legacy_version.h" + - "**/version.h" + + # ── Test data and golden files (not code) ───────────────────────────── + - tests/regression/data/** + - tests/regression_testing/results/** + - tests/unit/**/data/** + + # ── Vendored third-party (if ever checked in) ───────────────────────── + - vcpkg/** + - vcpkg_installed/** + + # ── Non-code ────────────────────────────────────────────────────────── + - docs/** + - paper/** diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml deleted file mode 100644 index 0fcb8e000..000000000 --- a/.github/workflows/build-and-test.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: Build and Test - -on: - push: - branches: [ main, develop, release ] - pull_request: - branches: [ main, develop, release ] - -env: - OMP_NUM_THREADS: 1 - BUILD_HOME: build - TEST_HOME: nrtests - PACKAGE_NAME: vcpkg-export-20240724-151754.1.0.0 - PKG_NAME: vcpkg-export-20240724-151754 - -jobs: - unit_test: - name: Build and unit test - runs-on: windows-latest - environment: testing - defaults: - run: - shell: cmd - - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Install boost-test - env: - REMOTE_STORE: "https://nuget.pkg.github.com/USEPA/index.json" - USERNAME: michaeltryby - run: | - nuget sources add -Name github -Source ${{ env.REMOTE_STORE }} -Username ${{ env.USERNAME }} -Password ${{ secrets.ACCESS_TOKEN }} - nuget install ${{env.PKG_NAME}} -Source github - - - name: Build - env: - TOOL_CHAIN_PATH: \scripts\buildsystems\vcpkg.cmake - run: | - cmake -B.\build -DBUILD_TESTS=ON -DCMAKE_TOOLCHAIN_FILE=.\${{env.PACKAGE_NAME}}${{env.TOOL_CHAIN_PATH}} . - cmake --build .\build --config DEBUG - - - name: Unit Test - run: ctest --test-dir .\build -C Debug --output-on-failure - - - reg_test: - name: Build and reg test - runs-on: windows-2019 - defaults: - run: - shell: cmd - working-directory: ci-tools/windows - - steps: - - name: Checkout swmm repo - uses: actions/checkout@v4 - - - name: Checkout ci-tools repo - uses: actions/checkout@v4 - with: - repository: USEPA/swmm-ci-tools - ref: master - path: ci-tools - - - name: Setup python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install requirements - run: | - python -m pip install --upgrade pip - python -m pip install -r requirements-swmm.txt - - - name: Build - run: make.cmd /g "Visual Studio 16 2019" - - - name: Before reg test - env: - NRTESTS_URL: https://github.com/USEPA/swmm-nrtestsuite - BENCHMARK_TAG: v2.5.0-dev - run: before-nrtest.cmd ${{ env.BENCHMARK_TAG }} - - - name: Run reg test - run: run-nrtests.cmd %GITHUB_RUN_ID%_%GITHUB_RUN_NUMBER% - - - name: Upload artifacts - if: ${{ always() }} - uses: actions/upload-artifact@v4 - with: - name: build-test-artifacts - path: upload/*.* - diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..f5d0b95da --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,108 @@ +name: CodeQL + +on: + push: + branches: [main] # drop develop — covered by PR + paths: ['src/**', 'include/**', 'python/**', 'CMakeLists.txt', 'vcpkg.json'] + pull_request: + branches: [main] + paths: ['src/**', 'include/**', 'python/**', 'CMakeLists.txt', 'vcpkg.json'] + schedule: [{ cron: "0 6 * * 1" }] + workflow_dispatch: + +env: + VCPKG_ROOT: ${{ github.workspace }}/vcpkg + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + include: + # C/C++ requires a real compile so CodeQL can hook every translation unit. + - language: c-cpp + build-mode: manual + # Python is scanned from source — no build needed. + - language: python + build-mode: none + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + config-file: ./.github/codeql/codeql-config.yml + + # ── C/C++ build pipeline (mirrors unit_testing.yml) ──────────────── + - name: Checkout vcpkg + if: matrix.language == 'c-cpp' + uses: actions/checkout@v5 + with: + repository: microsoft/vcpkg + ref: 2025.02.14 + path: vcpkg + + - name: Install Ninja + if: matrix.language == 'c-cpp' + run: sudo apt-get update && sudo apt-get install -y ninja-build + + - name: Bootstrap vcpkg + if: matrix.language == 'c-cpp' + working-directory: ${{ env.VCPKG_ROOT }} + run: | + ./bootstrap-vcpkg.sh + chmod +x vcpkg + + - name: Export GitHub Actions cache variables + if: matrix.language == 'c-cpp' + uses: actions/github-script@v8 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Cache vcpkg downloads (source tarballs) + if: matrix.language == 'c-cpp' + uses: actions/cache@v4 + with: + path: ${{ env.VCPKG_ROOT }}/downloads + key: vcpkg-downloads-codeql-${{ hashFiles('vcpkg.json') }} + restore-keys: | + vcpkg-downloads-codeql- + vcpkg-downloads-Linux-x64-linux- + + # Tests are off by default — CodeQL needs the engine libs compiled, + # not the test exes, so we skip them to keep the analysis window tight. + - name: Configure + if: matrix.language == 'c-cpp' + run: > + cmake + --preset=Linux-debug + -B build-codeql + -DOPENSWMM_BUILD_TESTS=OFF + -DOPENSWMM_BUILD_UNIT_TESTS=OFF + -DOPENSWMM_BUILD_REGRESSION_TESTS=OFF + -DOPENSWMM_BUILD_BENCHMARKS=OFF + -DOPENSWMM_BUILD_PYTHON=OFF + + - name: Build + if: matrix.language == 'c-cpp' + run: cmake --build build-codeql --config Debug + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml new file mode 100644 index 000000000..a09233290 --- /dev/null +++ b/.github/workflows/deployment.yml @@ -0,0 +1,455 @@ +name: Deployment + +on: + push: + branches: [main] + tags: ["v*.*.*"] + pull_request: + branches: [main] + workflow_dispatch: + +env: + VCPKG_ROOT: ${{ github.workspace }}/vcpkg + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + OMP_NUM_THREADS: 1 + # Single source of truth for the vcpkg ref (used by every checkout + the + # in-container clone in pyproject's linux before-all via environment-pass). + VCPKG_REF: "2025.02.14" + # macOS minimum target. Set globally so the engine build job AND vcpkg's + # dependency builds (SUNDIALS/HDF5/sqlite3) compile at this floor; ignored on + # Linux/Windows. The Python wheels set the same value via cibuildwheel + # (pyproject [tool.cibuildwheel.macos].environment). Must be ≤ every bundled + # dylib's minimum or delocate/CPack will ship libs that won't load on 11.x. + MACOSX_DEPLOYMENT_TARGET: "11.0" + +jobs: + # ────────────────────────────────────────────────────────────────────── + # Build release binaries + # ────────────────────────────────────────────────────────────────────── + build: + name: "Build (${{ matrix.alias }})" + permissions: + contents: read + actions: write # x-gha vcpkg binary cache + actions/cache + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + alias: Linux-x64 + cmake_preset: Linux + shell_ext: .sh + vcpkg_triplet: x64-linux + cmake_osx_arch: "" + + - os: macos-latest + alias: macOS-arm64 + cmake_preset: Darwin + shell_ext: .sh + vcpkg_triplet: arm64-osx + cmake_osx_arch: arm64 + + - os: macos-15-intel + alias: macOS-x64 + cmake_preset: Darwin + shell_ext: .sh + vcpkg_triplet: x64-osx + cmake_osx_arch: x86_64 + + - os: windows-latest + alias: Windows-x64 + cmake_preset: Windows + shell_ext: .bat + vcpkg_triplet: x64-windows + cmake_osx_arch: "" + + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Checkout vcpkg + uses: actions/checkout@v5 + with: + repository: microsoft/vcpkg + ref: ${{ env.VCPKG_REF }} + path: vcpkg + + - name: Build libomp for the macOS target (11.0) + if: runner.os == 'macOS' + # Build libomp from source at MACOSX_DEPLOYMENT_TARGET (11.0, from env) + # instead of `brew install libomp` (whose bottle is min-target 15.0), so + # the engine zip's bundled libomp loads on macOS 11+. ninja + cmake are + # preinstalled on the macOS runners. + run: bash scripts/build_macos_libomp.sh + + - name: Install Ninja (Linux) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y ninja-build + + - name: Bootstrap vcpkg (Windows) + if: runner.os == 'Windows' + working-directory: ${{ env.VCPKG_ROOT }} + run: | + .\bootstrap-vcpkg${{ matrix.shell_ext }} + .\vcpkg.exe integrate install + + - name: Bootstrap vcpkg (Unix) + if: runner.os != 'Windows' + working-directory: ${{ env.VCPKG_ROOT }} + run: | + ./bootstrap-vcpkg${{ matrix.shell_ext }} + chmod +x vcpkg + + - name: Export GitHub Actions cache variables + uses: actions/github-script@v8 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Cache vcpkg downloads (source tarballs) + uses: actions/cache@v4 + with: + path: ${{ env.VCPKG_ROOT }}/downloads + key: vcpkg-downloads-${{ runner.os }}-${{ matrix.vcpkg_triplet }}-${{ hashFiles('vcpkg.json') }} + restore-keys: | + vcpkg-downloads-${{ runner.os }}-${{ matrix.vcpkg_triplet }}- + vcpkg-downloads-${{ runner.os }}- + + - name: Configure (Release) + # Release packaging skips the test/benchmark targets so vcpkg pulls only + # the runtime deps. Kokkos + hypre/AMG are vcpkg/CMake defaults, but + # requested EXPLICITLY here so the release zip always ships the + # Kokkos-OpenMP plugin and a BoomerAMG-linked engine even if a default + # ever flips: VCPKG_MANIFEST_FEATURES "2d;gpu;hypre" pulls Kokkos + + # sundials[kokkos] + hypre; OPENSWMM_BUILD_GPU_PLUGIN=ON (omp backend) + # builds the plugin (find_package(Kokkos REQUIRED) fails configure if + # Kokkos is absent); OPENSWMM_WITH_HYPRE=ON wires BoomerAMG + # (find_package(HYPRE REQUIRED) fails configure if hypre is absent). + run: > + cmake + --preset=${{ matrix.cmake_preset }} + -B build-${{ matrix.vcpkg_triplet }} + -DVCPKG_MANIFEST_FEATURES="2d;gpu;hypre" + -DOPENSWMM_BUILD_GPU_PLUGIN=ON + -DOPENSWMM_GPU_BACKEND=omp + -DOPENSWMM_WITH_HYPRE=ON + -DOPENSWMM_BUILD_TESTS=OFF + -DOPENSWMM_BUILD_UNIT_TESTS=OFF + -DOPENSWMM_BUILD_REGRESSION_TESTS=OFF + -DOPENSWMM_BUILD_BENCHMARKS=OFF + -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_osx_arch }} + + - name: Build and package + run: cmake --build build-${{ matrix.vcpkg_triplet }} --target package --config Release + + # Gate: fail if the Kokkos GPU plugin did not build (silent Kokkos-default + # regression). The AMG link is already gated by OPENSWMM_WITH_HYPRE=ON -> + # find_package(HYPRE REQUIRED) at configure; this catches a plugin that + # configured but failed to produce a binary. + - name: Verify Kokkos plugin built (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + PLUGIN=$(find build-${{ matrix.vcpkg_triplet }} \ + \( -name 'libopenswmm_gpu_omp.dylib' -o -name 'libopenswmm_gpu_omp.so' \) \ + | head -1 || true) + if [ -z "$PLUGIN" ]; then + echo "::error::Kokkos GPU plugin (libopenswmm_gpu_omp) was not built — Kokkos default regressed" + exit 1 + fi + echo "Kokkos plugin built: $PLUGIN" + + - name: Verify Kokkos plugin built (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $plugin = Get-ChildItem -Recurse -Path "build-${{ matrix.vcpkg_triplet }}" ` + -Filter "openswmm_gpu_omp.dll" -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $plugin) { + Write-Host "::error::Kokkos GPU plugin (openswmm_gpu_omp.dll) was not built — Kokkos default regressed" + exit 1 + } + Write-Host "Kokkos plugin built: $($plugin.FullName)" + + - name: Upload release artifacts + uses: actions/upload-artifact@v5 + with: + name: release-${{ matrix.vcpkg_triplet }} + path: | + build-${{ matrix.vcpkg_triplet }}/*.tar.gz + build-${{ matrix.vcpkg_triplet }}/*.zip + + # ────────────────────────────────────────────────────────────────────── + # Python wheels — cibuildwheel matrix (one runner per OS/arch). + # + # Each runner produces wheels for every supported CPython version + # (3.10–3.13) in a single job. Linux builds run inside manylinux_2_28 + # containers; vcpkg is bootstrapped INSIDE the container by the + # `before-all` hook in pyproject.toml's [tool.cibuildwheel.linux] + # block. macOS / Windows run on the host runner and use the vcpkg + # checkout below. + # + # See docs/CIBUILDWHEEL_REVERT_PLAN.md for full rationale. + # ────────────────────────────────────────────────────────────────────── + wheels: + name: "Wheels (${{ matrix.os }})" + # Independent of the engine `build` job: wheels rebuild the engine from + # source via scikit-build-core, so a single-platform engine-zip failure must + # not block all wheels. (Previously `needs: build` coupled them.) + permissions: + contents: read + actions: write + strategy: + fail-fast: false + matrix: + os: + - ubuntu-24.04 # Linux x86_64 + - ubuntu-24.04-arm # Linux aarch64 (native, no QEMU) + - windows-2025 # Windows x86_64 + - macos-15 # macOS arm64 + - macos-15-intel # macOS x86_64 + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + # Export GHA cache creds so the in-container vcpkg (Linux) and the + # host vcpkg (macOS/Windows) can both reuse the x-gha binary cache. + - name: Export GitHub Actions cache variables + uses: actions/github-script@v8 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + # On macOS and Windows there is no container; vcpkg lives on the host + # and is referenced by python/CMakePresets.json via $env{VCPKG_ROOT}. + - name: Checkout vcpkg (host-side, macOS/Windows) + if: runner.os != 'Linux' + uses: actions/checkout@v5 + with: + repository: microsoft/vcpkg + ref: ${{ env.VCPKG_REF }} + path: vcpkg + + - name: Bootstrap vcpkg (macOS) + if: runner.os == 'macOS' + run: ./vcpkg/bootstrap-vcpkg.sh -disableMetrics + + - name: Bootstrap vcpkg (Windows) + if: runner.os == 'Windows' + run: .\vcpkg\bootstrap-vcpkg.bat -disableMetrics + + - name: Build wheels + uses: pypa/cibuildwheel@v3.1.4 + with: + package-dir: ./python + output-dir: ./wheelhouse + env: + # Host-side VCPKG_ROOT consumed by macOS/Windows builds. + # Linux ignores this (VCPKG_ROOT is set to /host/vcpkg inside + # the container by pyproject.toml's before-all hook). + VCPKG_ROOT: ${{ github.workspace }}/vcpkg + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + # Forwarded into the manylinux/musllinux container by pyproject's + # environment-pass so the in-container vcpkg clone uses the same ref. + VCPKG_REF: ${{ env.VCPKG_REF }} + # Force the macOS deployment target via cibuildwheel's env channel. + # Setting it ONLY in pyproject.toml [tool.cibuildwheel.macos].environment + # was not propagating to the x86_64 build's wheel-tag computation + # (wheel ended up tagged macosx_10_9 despite the toml override). + # CIBW_ENVIRONMENT_MACOS is read early and applied uniformly. + CIBW_ENVIRONMENT_MACOS: MACOSX_DEPLOYMENT_TARGET=15.0 VCPKG_ROOT=${{ github.workspace }}/vcpkg + + - name: Upload Python wheels + if: always() + uses: actions/upload-artifact@v5 + with: + name: python-wheels-${{ matrix.os }} + path: ./wheelhouse/*.whl + + # ────────────────────────────────────────────────────────────────────── + # GPU/OpenMP companion wheels — `openswmm-gpu-omp`. + # + # Separate distribution that ships ONLY the Kokkos-OpenMP plugin + # (libopenswmm_gpu_omp). Built from packages/gpu-omp/ — its CMakeLists reuses + # the engine's real plugin CMake (src/engine/2d/gpu, omp backend) and pulls + # only the vcpkg `gpu` feature (Kokkos + sundials[kokkos]). No GPU driver is + # needed (OpenMP), so the same OS/arch matrix as the base wheels applies. + # wheel.py-api=py3 ⇒ one py3-none wheel per platform. Independent of `wheels`. + # ────────────────────────────────────────────────────────────────────── + gpu-omp-wheels: + name: "GPU-omp wheels (${{ matrix.os }})" + permissions: + contents: read + actions: write + strategy: + fail-fast: false + matrix: + os: + - ubuntu-24.04 # Linux x86_64 + - ubuntu-24.04-arm # Linux aarch64 (native, no QEMU) + - windows-2025 # Windows x86_64 + - macos-15 # macOS arm64 + - macos-15-intel # macOS x86_64 + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Export GitHub Actions cache variables + uses: actions/github-script@v8 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Checkout vcpkg (host-side, macOS/Windows) + if: runner.os != 'Linux' + uses: actions/checkout@v5 + with: + repository: microsoft/vcpkg + ref: ${{ env.VCPKG_REF }} + path: vcpkg + + - name: Bootstrap vcpkg (macOS) + if: runner.os == 'macOS' + run: ./vcpkg/bootstrap-vcpkg.sh -disableMetrics + + - name: Bootstrap vcpkg (Windows) + if: runner.os == 'Windows' + run: .\vcpkg\bootstrap-vcpkg.bat -disableMetrics + + - name: Build companion wheels + uses: pypa/cibuildwheel@v3.1.4 + with: + package-dir: ./packages/gpu-omp + output-dir: ./wheelhouse + env: + VCPKG_ROOT: ${{ github.workspace }}/vcpkg + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + VCPKG_REF: ${{ env.VCPKG_REF }} + CIBW_ENVIRONMENT_MACOS: MACOSX_DEPLOYMENT_TARGET=15.0 VCPKG_ROOT=${{ github.workspace }}/vcpkg + + - name: Upload companion wheels + if: always() + uses: actions/upload-artifact@v5 + with: + name: python-gpu-omp-wheels-${{ matrix.os }} + path: ./wheelhouse/*.whl + + # ────────────────────────────────────────────────────────────────────── + # Python source distribution (sdist) — platform-independent, built once. + # ────────────────────────────────────────────────────────────────────── + sdist: + name: "Python sdist" + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Check version sync (CMake / vcpkg / pyproject) + run: python scripts/check_version_sync.py + + - name: Install build frontend + run: | + python -m pip install --upgrade pip + python -m pip install build + + - name: Build sdist + working-directory: python + run: python -m build --sdist + + - name: Upload sdist + uses: actions/upload-artifact@v5 + with: + name: python-sdist + path: python/dist/*.tar.gz + + # ────────────────────────────────────────────────────────────────────── + # C++ unit-test gate for releases. + # + # The test workflows (unit_testing*.yml, regression_testing.yml) only run on + # push/PR to main/develop — NOT on tag pushes — so without this a tag could + # release binaries that fail the suite. This job runs the Linux C++ unit + # tests and is a `needs:` of `release`. (The Python binding tests already run + # per-wheel via cibuildwheel's test-command, so the gap was the C++ side.) + # vcpkg deps come from the x-gha binary cache, so reruns are fast. + # ────────────────────────────────────────────────────────────────────── + test: + name: "C++ unit tests (release gate)" + permissions: + contents: read + actions: write + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Checkout vcpkg + uses: actions/checkout@v5 + with: + repository: microsoft/vcpkg + ref: ${{ env.VCPKG_REF }} + path: vcpkg + + - name: Install Ninja + run: sudo apt-get update && sudo apt-get install -y ninja-build + + - name: Bootstrap vcpkg + working-directory: ${{ env.VCPKG_ROOT }} + run: | + ./bootstrap-vcpkg.sh + chmod +x vcpkg + + - name: Export GitHub Actions cache variables + uses: actions/github-script@v8 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Configure (Debug, unit tests) + run: > + cmake + --preset=Linux-debug + -B build-test + -DVCPKG_MANIFEST_FEATURES="tests;geopackage" + -DOPENSWMM_BUILD_UNIT_TESTS=ON + -DOPENSWMM_WITH_GEOPACKAGE=ON + + - name: Build + run: cmake --build build-test --config Debug + + - name: Unit tests + run: ctest --test-dir build-test -C Debug -L unit --output-on-failure + + # ────────────────────────────────────────────────────────────────────── + # Create GitHub Release + # ────────────────────────────────────────────────────────────────────── + release: + name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/v') + needs: [build, test, wheels, gpu-omp-wheels, sdist] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download all artifacts + uses: actions/download-artifact@v5 + with: + path: dist + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + files: dist/**/* diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 000000000..7bc9a7113 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,160 @@ +name: Documentation + +on: + push: +# branches: [main, develop] + branches: [main, develop, swmm6_rel] + tags: ["v*.*.*"] + paths: ['docs/**', 'python/docs/**', 'python/openswmm/**', 'include/**', '**/*.md'] + pull_request: + branches: [main, develop] + # branches: [main] + paths: ['docs/**', 'python/docs/**', 'python/openswmm/**', 'include/**', '**/*.md'] + workflow_dispatch: + +# Allow only one concurrent deployment. Do NOT cancel in-progress runs so +# that production deployments always complete. +concurrency: + group: pages + cancel-in-progress: false + +# ───────────────────────────────────────────────────────────────────────────── +# Job 1 – Build Python (Sphinx) documentation +# ───────────────────────────────────────────────────────────────────────────── +jobs: + build_python_docs: + name: Build Python Bindings Docs (Sphinx) + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Sphinx and theme dependencies + # Install from requirements.txt (single source of truth — keeps + # CI in lockstep with the doc-author's local environment, including + # sphinx-design for the grid/tab-set directives in the User Guide + # and Migration pages). + run: | + pip install -r python/docs/requirements.txt + pip install numpy + + - name: Build Sphinx HTML docs + # conf.py adds python/ to sys.path and installs the Cython stub-file + # finder, so no compiled extensions or package installation is needed. + # -W promotes warnings to errors so a regression in the docs blocks + # the build; --keep-going still surfaces every issue in one pass. + run: | + sphinx-build -b html -W --keep-going python/docs python/docs/_build/html + + - name: Upload Python docs artifact + uses: actions/upload-artifact@v5 + with: + name: python-docs + path: python/docs/_build/html/ + retention-days: 1 + +# ───────────────────────────────────────────────────────────────────────────── +# Job 2 – Build C/C++ documentation (Doxygen) +# Runs after the Python docs build per the documented deployment order. +# ───────────────────────────────────────────────────────────────────────────── + build_doxygen_docs: + name: Build C/C++ Engine Docs (Doxygen) + needs: build_python_docs + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Install Graphviz and download tools + run: | + sudo apt-get update + sudo apt-get install -y graphviz wget tar + + - name: Install Doxygen 1.13.2 + run: | + DOXYGEN_VERSION=1.13.2 + # Doxygen release TAGS use underscores (Release_1_13_2) while the + # asset filename uses dots (doxygen-1.13.2.linux.bin.tar.gz). Building + # the tag with dots 404s, so derive the underscore form explicitly. + DOXYGEN_TAG="Release_${DOXYGEN_VERSION//./_}" + wget -q "https://github.com/doxygen/doxygen/releases/download/${DOXYGEN_TAG}/doxygen-${DOXYGEN_VERSION}.linux.bin.tar.gz" + tar -xzf doxygen-${DOXYGEN_VERSION}.linux.bin.tar.gz + sudo mv doxygen-${DOXYGEN_VERSION}/bin/doxygen /usr/local/bin/doxygen + doxygen --version + + - name: Build Doxygen HTML docs + working-directory: docs + run: doxygen Doxyfile + + - name: Upload Doxygen docs artifact + uses: actions/upload-artifact@v5 + with: + name: doxygen-docs + path: docs/html/ + retention-days: 1 + +# ───────────────────────────────────────────────────────────────────────────── +# Job 3 – Merge both doc sets and deploy to GitHub Pages +# Final site layout: +# / → Doxygen C/C++ engine docs (main entry point) +# /python/ → Sphinx openswmm Python bindings docs +# ───────────────────────────────────────────────────────────────────────────── + deploy: + name: Deploy to GitHub Pages + # Deploys fire on direct pushes to a whitelisted branch and on manual + # workflow_dispatch runs targeting one of those same branches. PR events + # are intentionally excluded — PR builds verify the docs compile but must + # not overwrite the live Pages site. To preview a PR, push the source + # branch (swmm6_rel) or dispatch the workflow manually against it from + # the Actions tab. + if: | + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + && (github.ref == 'refs/heads/main' + || github.ref == 'refs/heads/develop' + || github.ref == 'refs/heads/swmm6_rel') + needs: [build_python_docs, build_doxygen_docs] + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + pages: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Download Doxygen docs + uses: actions/download-artifact@v5 + with: + name: doxygen-docs + path: site/ + + - name: Download Python docs + uses: actions/download-artifact@v5 + with: + name: python-docs + path: site/python/ + + - name: Configure GitHub Pages + uses: actions/configure-pages@v5 + + - name: Upload combined site artifact + uses: actions/upload-pages-artifact@v4 + with: + path: site/ + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 + diff --git a/.github/workflows/regression_testing.yml b/.github/workflows/regression_testing.yml new file mode 100644 index 000000000..6a6323c8f --- /dev/null +++ b/.github/workflows/regression_testing.yml @@ -0,0 +1,257 @@ +name: Regression Testing + +on: + push: + branches: [main] + pull_request: + branches: [main] + + workflow_dispatch: + +env: + VCPKG_ROOT: ${{ github.workspace }}/vcpkg + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + OMP_NUM_THREADS: 1 + +jobs: + regression: + name: "Regression (${{ matrix.alias }})" + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + alias: Linux-x64 + cmake_preset: Linux + shell_ext: .sh + vcpkg_triplet: x64-linux + cmake_osx_arch: "" + + - os: windows-latest + alias: Windows-x64 + cmake_preset: Windows + shell_ext: .bat + vcpkg_triplet: x64-windows + cmake_osx_arch: "" + + - os: macos-latest + alias: macOS-arm64 + cmake_preset: Darwin + shell_ext: .sh + vcpkg_triplet: arm64-osx + cmake_osx_arch: arm64 + + - os: macos-15-intel + alias: macOS-x64 + cmake_preset: Darwin + shell_ext: .sh + vcpkg_triplet: x64-osx + cmake_osx_arch: x86_64 + + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Checkout vcpkg + uses: actions/checkout@v5 + with: + repository: microsoft/vcpkg + ref: 2025.02.14 + path: vcpkg + + - name: Install OpenMP (macOS) + if: runner.os == 'macOS' + run: brew install libomp + + - name: Install Ninja (Linux) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y ninja-build + + - name: Bootstrap vcpkg (Windows) + if: runner.os == 'Windows' + working-directory: ${{ env.VCPKG_ROOT }} + run: | + .\bootstrap-vcpkg${{ matrix.shell_ext }} + .\vcpkg.exe integrate install + + - name: Bootstrap vcpkg (Unix) + if: runner.os != 'Windows' + working-directory: ${{ env.VCPKG_ROOT }} + run: | + ./bootstrap-vcpkg${{ matrix.shell_ext }} + chmod +x vcpkg + + - name: Export GitHub Actions cache variables + uses: actions/github-script@v8 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Cache vcpkg downloads (source tarballs) + uses: actions/cache@v4 + with: + path: ${{ env.VCPKG_ROOT }}/downloads + key: vcpkg-downloads-${{ runner.os }}-${{ matrix.vcpkg_triplet }}-${{ hashFiles('vcpkg.json') }} + restore-keys: | + vcpkg-downloads-${{ runner.os }}-${{ matrix.vcpkg_triplet }}- + vcpkg-downloads-${{ runner.os }}- + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install Python test requirements + run: | + python -m pip install --upgrade pip + python -m pip install pytest numpy + + - name: Configure (Release with tests) + # The *-tests-release preset sets VCPKG_MANIFEST_FEATURES=tests;geopackage + # and enables OPENSWMM_BUILD_UNIT_TESTS + OPENSWMM_BUILD_REGRESSION_TESTS + # + OPENSWMM_WITH_GEOPACKAGE on top of the Release-flavoured base preset. + # gpu;hypre are vcpkg defaults, but added EXPLICITLY (overriding the + # preset's feature list) + the Kokkos/AMG CMake options pinned ON so the + # 2D-parity regression suite always exercises the Kokkos-OpenMP plugin + # and BoomerAMG path; find_package(Kokkos/HYPRE REQUIRED) fails configure + # if either default ever regresses. + run: > + cmake + --preset=${{ matrix.cmake_preset }}-tests-release + -B build-${{ matrix.vcpkg_triplet }} + -DVCPKG_MANIFEST_FEATURES="tests;geopackage;gpu;hypre" + -DOPENSWMM_BUILD_GPU_PLUGIN=ON + -DOPENSWMM_GPU_BACKEND=omp + -DOPENSWMM_WITH_HYPRE=ON + -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_osx_arch }} + + - name: Build (Release) + run: cmake --build build-${{ matrix.vcpkg_triplet }} --config Release + + - name: Unit tests (sanity check) + run: ctest --test-dir build-${{ matrix.vcpkg_triplet }} -C Release -L unit --output-on-failure + + # - name: Diagnose Linux unit-test segfaults (gdb) + # if: failure() && runner.os == 'Linux' + # working-directory: tests/unit/legacy/engine/data + # run: | + # sudo apt-get install -y gdb + # for t in test_solver_api test_solver_errors test_solver_hotstart test_solver_shapes test_solver_expanded_api; do + # bin=$(find ${{ github.workspace }}/build-${{ matrix.vcpkg_triplet }} -name "$t" -type f -executable | head -1) + # if [ -z "$bin" ]; then echo "Binary $t not found"; continue; fi + # echo "===================================================" + # echo "==== gdb backtrace: $t" + # echo "===================================================" + # gdb -batch \ + # -ex 'set pagination off' \ + # -ex 'set confirm off' \ + # -ex 'handle SIGSEGV stop print nopass' \ + # -ex run \ + # -ex 'thread apply all bt full' \ + # -ex quit \ + # "$bin" 2>&1 | head -400 || true + # done + + - name: C++ regression tests + run: ctest --test-dir build-${{ matrix.vcpkg_triplet }} -C Release -L regression --output-on-failure + + - name: Python regression tests + working-directory: tests/regression_testing + env: + OPENSWMM_BUILD_DIR: ${{ github.workspace }}/build-${{ matrix.vcpkg_triplet }} + run: python -m pytest -v --tb=short + + - name: Install Release tree + run: > + cmake --install build-${{ matrix.vcpkg_triplet }} + --config Release + --prefix "${{ github.workspace }}/regression-release-${{ matrix.vcpkg_triplet }}" + + - name: Verify bundled runtime libs (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + INST="${{ github.workspace }}/regression-release-${{ matrix.vcpkg_triplet }}" + cd "$INST/bin" + echo "--- bin/ contents ---" + ls -la + echo + for exe in openswmm openswmm-legacy; do + [ -x "$exe" ] || continue + echo "--- $exe linkage ---" + if [[ "${{ runner.os }}" == "macOS" ]]; then + otool -L "$exe" + else + ldd "$exe" + fi + done + # Gate: the Kokkos-OpenMP plugin must be installed (lib/ on Unix). + PLUGIN=$(find "$INST/lib" \( -name 'libopenswmm_gpu_omp.dylib' -o -name 'libopenswmm_gpu_omp.so' \) | head -1 || true) + if [ -z "$PLUGIN" ]; then + echo "::error::Kokkos GPU plugin (libopenswmm_gpu_omp) missing from $INST/lib — Kokkos default regressed" + exit 1 + fi + echo "Kokkos plugin installed: $PLUGIN" + + - name: Verify bundled runtime libs (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $inst = "${{ github.workspace }}/regression-release-${{ matrix.vcpkg_triplet }}" + Set-Location "$inst/bin" + Write-Host "--- bin/ contents ---" + Get-ChildItem | Format-Table Mode,Length,Name + # Gate: the Kokkos-OpenMP plugin must be installed (bin/ on Windows). + $plugin = Get-ChildItem -Recurse -Path $inst -Filter "openswmm_gpu_omp.dll" -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $plugin) { + Write-Host "::error::Kokkos GPU plugin (openswmm_gpu_omp.dll) missing from $inst — Kokkos default regressed" + exit 1 + } + Write-Host "Kokkos plugin installed: $($plugin.FullName)" + + - name: Upload Release install artifact + uses: actions/upload-artifact@v5 + with: + name: regression-release-${{ matrix.vcpkg_triplet }} + path: regression-release-${{ matrix.vcpkg_triplet }}/ + + - name: Upload regression results + if: always() + uses: actions/upload-artifact@v5 + with: + name: regression-${{ matrix.vcpkg_triplet }} + path: tests/regression_testing/results/ + if-no-files-found: ignore + + # ────────────────────────────────────────────────────────────────────── + # Python source distribution (sdist) — platform-independent, built once. + # ────────────────────────────────────────────────────────────────────── + sdist: + name: "Python sdist" + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install build frontend + run: | + python -m pip install --upgrade pip + python -m pip install build + + - name: Build sdist + working-directory: python + run: python -m build --sdist + + - name: Upload sdist + uses: actions/upload-artifact@v5 + with: + name: python-sdist + path: python/dist/*.tar.gz diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 000000000..b829f1d1d --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,59 @@ +name: Scorecard supply-chain security + +on: + # Some checks (branch protection, code review enforcement) only run when + # the workflow is triggered by a branch-protection-rule change. + branch_protection_rule: + # Weekly scan so new scorecard checks land here as they ship. + schedule: + - cron: "20 7 * * 1" + push: + branches: ["main"] + workflow_dispatch: + +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the SARIF results to Security > Code scanning and to publish the score to the OpenSSF Scorecard API. + security-events: write + # Needed to publish the score to the OpenSSF Scorecard API and to + # mint short-lived OIDC tokens that prove the run came from this repo. + id-token: write + # Read access only — no write to repo contents. + contents: read + actions: read + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Run analysis + uses: ossf/scorecard-action@v2.4.1 + with: + results_file: results.sarif + results_format: sarif + # publish_results uploads the score to securityscorecards.dev so + # the README badge and the public dashboard stay current. Toggle + # to false if you'd rather keep results internal to this repo. + publish_results: true + + # Persist the SARIF for 5 days so it can be downloaded and inspected + # outside the GitHub Security tab. + - name: Upload artifact + uses: actions/upload-artifact@v5 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Surface findings in Security > Code scanning alongside CodeQL. + - name: Upload to code scanning + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/typing.yml b/.github/workflows/typing.yml new file mode 100644 index 000000000..80ad77769 --- /dev/null +++ b/.github/workflows/typing.yml @@ -0,0 +1,56 @@ +name: Typing (mypy) + +on: + push: + branches: [main, develop] + paths: + - 'python/openswmm/**/*.py' + - 'python/openswmm/**/*.pyi' + - 'python/tests/typing/**' + - 'python/pyproject.toml' + - '.github/workflows/typing.yml' + pull_request: + branches: [main, develop] + paths: + - 'python/openswmm/**/*.py' + - 'python/openswmm/**/*.pyi' + - 'python/tests/typing/**' + - 'python/pyproject.toml' + - '.github/workflows/typing.yml' + workflow_dispatch: + +# This workflow does NOT build the compiled engine — mypy walks the +# .pyi stubs and pure-Python modules directly. Cheap to run (~30s). +jobs: + mypy: + name: mypy ${{ matrix.config.label }} + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + config: + - label: "openswmm.engine (default mode)" + args: "openswmm/engine" + - label: "tests/typing/ (strict mode)" + args: "--strict tests/typing/test_surface.py" + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install mypy + numpy + typing dependencies + # mypy needs the .pyi files plus numpy stubs. The package itself + # isn't installed — we run against the source tree. + run: | + pip install mypy numpy + + - name: Run mypy + working-directory: python + run: mypy ${{ matrix.config.args }} diff --git a/.github/workflows/unit_testing.yml b/.github/workflows/unit_testing.yml new file mode 100644 index 000000000..e1e1788e4 --- /dev/null +++ b/.github/workflows/unit_testing.yml @@ -0,0 +1,313 @@ +name: Unit Testing + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +env: + VCPKG_ROOT: ${{ github.workspace }}/vcpkg + VCPKG_EXE: ${{ github.workspace }}/vcpkg/vcpkg + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + # Note: the vcpkg `tests` feature is enabled via -DVCPKG_MANIFEST_FEATURES on + # the Configure step below; the env-var form isn't honored by the vcpkg + # toolchain at CMake configure time. + OMP_NUM_THREADS: 1 + +jobs: + # ────────────────────────────────────────────────────────────────────── + # C / C++ Engine — build, unit test, and package + # ────────────────────────────────────────────────────────────────────── + engine: + name: "C++ Engine (${{ matrix.alias }})" + permissions: + contents: read + actions: write + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + alias: Linux-x64 + cmake_preset: Linux + shell_ext: .sh + vcpkg_triplet: x64-linux + cmake_osx_arch: "" + + - os: macos-latest + alias: macOS-arm64 + cmake_preset: Darwin + shell_ext: .sh + vcpkg_triplet: arm64-osx + cmake_osx_arch: arm64 + + - os: macos-15-intel + alias: macOS-x64 + cmake_preset: Darwin + shell_ext: .sh + vcpkg_triplet: x64-osx + cmake_osx_arch: x86_64 + + - os: windows-latest + alias: Windows-x64 + cmake_preset: Windows + shell_ext: .bat + vcpkg_triplet: x64-windows + cmake_osx_arch: "" + + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Checkout vcpkg + uses: actions/checkout@v5 + with: + repository: microsoft/vcpkg + ref: 2025.02.14 + path: vcpkg + + - name: Install OpenMP (macOS) + if: runner.os == 'macOS' + run: brew install libomp + + - name: Install Ninja (Linux) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y ninja-build + + - name: Bootstrap vcpkg (Windows) + if: runner.os == 'Windows' + working-directory: ${{ env.VCPKG_ROOT }} + run: | + .\bootstrap-vcpkg${{ matrix.shell_ext }} + .\vcpkg.exe integrate install + + - name: Bootstrap vcpkg (Unix) + if: runner.os != 'Windows' + working-directory: ${{ env.VCPKG_ROOT }} + run: | + ./bootstrap-vcpkg${{ matrix.shell_ext }} + chmod +x vcpkg + + - name: Export GitHub Actions cache variables + uses: actions/github-script@v8 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Cache vcpkg downloads (source tarballs) + uses: actions/cache@v4 + with: + path: ${{ env.VCPKG_ROOT }}/downloads + key: vcpkg-downloads-${{ runner.os }}-${{ matrix.vcpkg_triplet }}-${{ hashFiles('vcpkg.json') }} + restore-keys: | + vcpkg-downloads-${{ runner.os }}-${{ matrix.vcpkg_triplet }}- + vcpkg-downloads-${{ runner.os }}- + + # The Windows presets use the Ninja Multi-Config generator (version- + # agnostic, unlike a pinned "Visual Studio 17 2022" that breaks when the + # runner's VS major changes). Ninja needs cl.exe/link.exe on PATH, which + # the VS generator used to supply implicitly — set up the MSVC dev + # environment so subsequent steps can find the compiler. + - name: Set up MSVC environment (Windows) + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + # gpu;hypre are vcpkg default-features and the plugin/hypre CMake options + # default ON, but request them EXPLICITLY so the Kokkos `tests/gpu` parity + # tests and the BoomerAMG path are always built+exercised here even if an + # upstream default flips (find_package(Kokkos/HYPRE REQUIRED) → configure + # fails loudly rather than silently dropping Kokkos/AMG). + - name: Configure + run: > + cmake + --preset=${{ matrix.cmake_preset }}-debug + -B build-${{ matrix.vcpkg_triplet }} + -DVCPKG_MANIFEST_FEATURES="tests;geopackage;gpu;hypre" + -DOPENSWMM_BUILD_TESTS=OFF + -DOPENSWMM_BUILD_UNIT_TESTS=ON + -DOPENSWMM_BUILD_GPU_PLUGIN=ON + -DOPENSWMM_GPU_BACKEND=omp + -DOPENSWMM_WITH_HYPRE=ON + -DOPENSWMM_WITH_GEOPACKAGE=ON + -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_osx_arch }} + + - name: Build + run: cmake --build build-${{ matrix.vcpkg_triplet }} --config Debug + + - name: Unit tests + run: ctest --test-dir build-${{ matrix.vcpkg_triplet }} -C Debug -L unit --output-on-failure + + - name: Diagnose Linux unit-test segfaults (gdb) + if: failure() && runner.os == 'Linux' + run: | + sudo apt-get install -y gdb + BUILD="${{ github.workspace }}/build-${{ matrix.vcpkg_triplet }}" + # CTest records the tests that failed in this run here, one per line + # as ":". Derive the binaries to backtrace from that + # instead of a hard-coded list, so this always targets whatever + # actually failed. + FAILED_LOG=$(find "$BUILD/Testing/Temporary" -name 'LastTestsFailed*.log' 2>/dev/null | head -1) + if [ -z "$FAILED_LOG" ]; then + echo "No LastTestsFailed log found under $BUILD/Testing/Temporary"; exit 0 + fi + echo "Failed tests this run:"; cat "$FAILED_LOG" + cut -d: -f2 "$FAILED_LOG" | while read -r t; do + [ -z "$t" ] && continue + bin=$(find "$BUILD" -name "$t" -type f -executable | head -1) + if [ -z "$bin" ]; then echo "Binary for test '$t' not found"; continue; fi + echo "===================================================" + echo "==== gdb backtrace: $t ($bin)" + echo "===================================================" + gdb -batch \ + -ex 'set pagination off' \ + -ex 'set confirm off' \ + -ex 'handle SIGSEGV stop print nopass' \ + -ex run \ + -ex 'thread apply all bt full' \ + -ex quit \ + "$bin" 2>&1 | head -400 || true + done + + - name: Diagnose macOS unit-test segfaults (lldb) + if: failure() && runner.os == 'macOS' + run: | + BUILD="${{ github.workspace }}/build-${{ matrix.vcpkg_triplet }}" + # Same approach as the Linux gdb step, using lldb (ships with Xcode). + FAILED_LOG=$(find "$BUILD/Testing/Temporary" -name 'LastTestsFailed*.log' 2>/dev/null | head -1) + if [ -z "$FAILED_LOG" ]; then + echo "No LastTestsFailed log found under $BUILD/Testing/Temporary"; exit 0 + fi + echo "Failed tests this run:"; cat "$FAILED_LOG" + cut -d: -f2 "$FAILED_LOG" | while read -r t; do + [ -z "$t" ] && continue + bin=$(find "$BUILD" -name "$t" -type f -perm -u+x | head -1) + if [ -z "$bin" ]; then echo "Binary for test '$t' not found"; continue; fi + echo "===================================================" + echo "==== lldb backtrace: $t ($bin)" + echo "===================================================" + lldb --batch \ + -o 'run' \ + -o 'thread backtrace all' \ + -o 'quit' \ + "$bin" 2>&1 | head -400 || true + done + + - name: Dump failed vcpkg build logs (Windows) + if: failure() && runner.os == 'Windows' + shell: bash + run: | + # The Windows failure is during `cmake configure`, when vcpkg builds a + # dependency (e.g. Kokkos). vcpkg prints "See logs for more + # information: " but CI never echoes those files, so the real + # compiler error is invisible. Dump every per-package out/err log so + # the actual error shows up in the job output. + BT="${{ github.workspace }}/vcpkg/buildtrees" + if [ ! -d "$BT" ]; then echo "No vcpkg buildtrees at $BT"; exit 0; fi + # Dump the per-package build logs for the deps most likely to fail + # here (Kokkos is the current culprit; sundials/hdf5 pull it in). + # Falls back to every *.log if none of those dirs exist. + dirs="" + for d in kokkos sundials hdf5; do + [ -d "$BT/$d" ] && dirs="$dirs $BT/$d" + done + [ -z "$dirs" ] && dirs="$BT" + find $dirs -type f -name '*.log' 2>/dev/null | while read -r f; do + echo "===================================================" + echo "==== vcpkg log: $f" + echo "===================================================" + tail -200 "$f" || true + done + + - name: Package + run: cmake --build build-${{ matrix.vcpkg_triplet }} --target package + + - name: Upload build artifacts + if: always() + uses: actions/upload-artifact@v5 + with: + name: engine-${{ matrix.vcpkg_triplet }} + path: | + build-${{ matrix.vcpkg_triplet }}/*.tar.gz + build-${{ matrix.vcpkg_triplet }}/*.zip + + # ── Release build for Python wheel consumption ──────────────────────── + # vcpkg packages are already in the binary cache from the Debug build + # above, so this configure is fast (no SUNDIALS recompilation). + - name: Configure (Release, for Python wheel) + run: > + cmake + --preset=${{ matrix.cmake_preset }} + -B build-${{ matrix.vcpkg_triplet }}-release + -DVCPKG_MANIFEST_FEATURES="geopackage;gpu;hypre" + -DOPENSWMM_BUILD_TESTS=OFF + -DOPENSWMM_BUILD_UNIT_TESTS=OFF + -DOPENSWMM_BUILD_REGRESSION_TESTS=OFF + -DOPENSWMM_BUILD_BENCHMARKS=OFF + -DOPENSWMM_BUILD_GPU_PLUGIN=ON + -DOPENSWMM_GPU_BACKEND=omp + -DOPENSWMM_WITH_HYPRE=ON + -DOPENSWMM_WITH_GEOPACKAGE=ON + -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_osx_arch }} + + - name: Build (Release, for Python wheel) + run: cmake --build build-${{ matrix.vcpkg_triplet }}-release --config Release + + - name: Install (Release, for Python wheel) + run: > + cmake --install build-${{ matrix.vcpkg_triplet }}-release + --config Release + --prefix "${{ github.workspace }}/engine-install-${{ matrix.vcpkg_triplet }}" + + - name: Verify bundled runtime libs (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + INST="${{ github.workspace }}/engine-install-${{ matrix.vcpkg_triplet }}" + cd "$INST/bin" + echo "--- bin/ contents ---" + ls -la + echo + for exe in openswmm openswmm-legacy; do + [ -x "$exe" ] || continue + echo "--- $exe linkage ---" + if [[ "${{ runner.os }}" == "macOS" ]]; then + otool -L "$exe" + else + ldd "$exe" + fi + done + # Gate: the Kokkos-OpenMP plugin must be installed (lib/ on Unix). + PLUGIN=$(find "$INST/lib" \( -name 'libopenswmm_gpu_omp.dylib' -o -name 'libopenswmm_gpu_omp.so' \) | head -1 || true) + if [ -z "$PLUGIN" ]; then + echo "::error::Kokkos GPU plugin (libopenswmm_gpu_omp) missing from $INST/lib — Kokkos default regressed" + exit 1 + fi + echo "Kokkos plugin installed: $PLUGIN" + + - name: Verify bundled runtime libs (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $inst = "${{ github.workspace }}/engine-install-${{ matrix.vcpkg_triplet }}" + Set-Location "$inst/bin" + Write-Host "--- bin/ contents ---" + Get-ChildItem | Format-Table Mode,Length,Name + # Gate: the Kokkos-OpenMP plugin must be installed (bin/ on Windows). + $plugin = Get-ChildItem -Recurse -Path $inst -Filter "openswmm_gpu_omp.dll" -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $plugin) { + Write-Host "::error::Kokkos GPU plugin (openswmm_gpu_omp.dll) missing from $inst — Kokkos default regressed" + exit 1 + } + Write-Host "Kokkos plugin installed: $($plugin.FullName)" + + - name: Upload engine install artifact + uses: actions/upload-artifact@v5 + with: + name: engine-install-${{ matrix.vcpkg_triplet }} + path: engine-install-${{ matrix.vcpkg_triplet }}/ diff --git a/.github/workflows/unit_testing_python.yml b/.github/workflows/unit_testing_python.yml new file mode 100644 index 000000000..af97e94bc --- /dev/null +++ b/.github/workflows/unit_testing_python.yml @@ -0,0 +1,125 @@ +name: Unit Testing Python + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +env: + VCPKG_ROOT: ${{ github.workspace }}/vcpkg + VCPKG_EXE: ${{ github.workspace }}/vcpkg/vcpkg + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + VCPKG_MANIFEST_FEATURES: "tests" + OMP_NUM_THREADS: 1 + +jobs: + # ────────────────────────────────────────────────────────────────────── + # Python bindings — cibuildwheel matrix (one runner per OS/arch). + # + # cibuildwheel's test-command in pyproject.toml runs pytest inside + # each built wheel, so the previous "Run Python tests" and + # "Run smoke test" steps are no longer needed here. + # + # PR runs build only one CPython version (cp312) per OS for speed. + # Pushes to main/develop and manual triggers build the full matrix. + # See docs/CIBUILDWHEEL_REVERT_PLAN.md. + # ────────────────────────────────────────────────────────────────────── + python: + name: "Python (${{ matrix.os }})" + permissions: + contents: read + actions: write + strategy: + fail-fast: false + matrix: + os: + - ubuntu-24.04 # Linux x86_64 + - ubuntu-24.04-arm # Linux aarch64 (native, no QEMU) + - windows-2025 # Windows x86_64 + - macos-15 # macOS arm64 + - macos-15-intel # macOS x86_64 + runs-on: ${{ matrix.os }} + env: + # Full matrix on push/dispatch; single CPython on PR for fast feedback. + CIBW_BUILD: ${{ github.event_name == 'pull_request' && 'cp312-*' || 'cp310-* cp311-* cp312-* cp313-*' }} + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Export GitHub Actions cache variables + uses: actions/github-script@v8 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Checkout vcpkg (host-side, macOS/Windows) + if: runner.os != 'Linux' + uses: actions/checkout@v5 + with: + repository: microsoft/vcpkg + ref: 2025.02.14 + path: vcpkg + + - name: Bootstrap vcpkg (macOS) + if: runner.os == 'macOS' + run: ./vcpkg/bootstrap-vcpkg.sh -disableMetrics + + - name: Bootstrap vcpkg (Windows) + if: runner.os == 'Windows' + run: .\vcpkg\bootstrap-vcpkg.bat -disableMetrics + + - name: Build wheels + uses: pypa/cibuildwheel@v3.1.4 + with: + package-dir: ./python + output-dir: ./wheelhouse + env: + # Host-side VCPKG_ROOT consumed by macOS/Windows builds. + # Linux uses /host/vcpkg inside the container (see pyproject.toml). + VCPKG_ROOT: ${{ github.workspace }}/vcpkg + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + # See deployment.yml for rationale — pyproject.toml's + # [tool.cibuildwheel.macos].environment doesn't reach the + # x86_64 wheel-tag computation reliably; CIBW_ENVIRONMENT_MACOS does. + CIBW_ENVIRONMENT_MACOS: MACOSX_DEPLOYMENT_TARGET=15.0 VCPKG_ROOT=${{ github.workspace }}/vcpkg + + - name: Upload Python wheels + if: always() + uses: actions/upload-artifact@v5 + with: + name: python-wheels-${{ matrix.os }} + path: ./wheelhouse/*.whl + + # ────────────────────────────────────────────────────────────────────── + # Python source distribution (sdist) — platform-independent, built once. + # ────────────────────────────────────────────────────────────────────── + sdist: + name: "Python sdist" + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install build frontend + run: | + python -m pip install --upgrade pip + python -m pip install build + + - name: Build sdist + working-directory: python + run: python -m build --sdist + + - name: Upload sdist + uses: actions/upload-artifact@v5 + with: + name: python-sdist + path: python/dist/*.tar.gz diff --git a/.gitignore b/.gitignore index 7e8e71376..3546252e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,34 @@ .DS_Store +# Working notes not intended for version control +doc/FlashX_SWMM_crosswalk.md +doc/Verification_Implementation_Plan.md + # Eclipse Stuff .metadata/ .settings/ build/ +build-*/ nrtests/ upload/ *_export.h +docs/html/ + +# add python ignore files and file types +python/build/ +python/dist/ +python/OpenSWMMCore.egg-info/ +python/_skbuild/ +__pycache__/ +*.pyc +*.pyo +*.pyd +*.so +*.egg +*.egg-info +*.manifest +*.spec + diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 000000000..91bb18270 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,15 @@ +# Authors + +OpenSWMM is built on the EPA SWMM 5.x foundation and extended by the contributors listed below. + +## Project Lead + +- **Caleb Buahin** @cbuahin -- Project lead, architect, and primary developer of the OpenSWMM 6.x engine rewrite (data-oriented SoA architecture, plugin system, GeoPackage I/O, C/C++ API, dynamic wave solver alignment, Python bindings). + +## Contributors + +## AI-Assisted Development + +Portions of the OpenSWMM 6.x codebase were developed with the assistance of: + +- **Claude** (Anthropic) -- Code generation, architecture design, GeoPackage strategy and implementation, test development. diff --git a/Build.md b/Build.md deleted file mode 100644 index b9352e45d..000000000 --- a/Build.md +++ /dev/null @@ -1,32 +0,0 @@ - - - -## Building SWMM Locally on Windows - - -### Dependencies - -Before the project can be built the required dependencies must be installed. - -**Summary of Build Dependencies: Windows** - - - Build - - Build Tools for Visual Studio 2017 - - CMake 3.13 - - -### Build - -SWMM can be built with one simple command. -``` -\> cd swmm -\swmm>tools\make.cmd -``` diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..a2b702075 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,634 @@ +# Changelog + +All notable changes to the OpenSWMM Engine are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] — Runtime forcing Phase 4 + §3 legacy quality sources + +See `docs/RUNTIME_FORCING_PHASE4_HANDOFF.md`, +`docs/RUNTIME_FORCING_PHASE4_AUDIT.md` (per-group outcomes), and +`docs/RUNTIME_FORCING_API_GAP_PLAN.md` §7/§12. + +### Added + +- **Python GeoPackage model export** — `Solver.write_with_plugin(path, + output_plugin_id)` and the convenience `Solver.write_geopackage(path, + crs=...)` so a loaded model can be exported to a `.gpkg` from Python (the C + API already had `swmm_model_write_with_plugin`; only `ModelBuilder` wrapped + it before). `write_geopackage(crs="EPSG:2284")` applies the CRS via + `solver.spatial.crs` first, so every feature layer is tagged with that SRS — + without a CRS the geometries get an undefined SRS (`srs_id 0`) and GIS tools + cannot place them. Added the `GEOPACKAGE_PLUGIN_ID` constant (the real id is + `org.hydrocouple.openswmm.plugins.geopackage`; corrected the stale example in + `openswmm_model.h` that omitted `.plugins.`). Tests: + `python/tests/engine/test_geopackage_export.py`. + +- **GUI-editor round-trip APIs** — getters/setters so the property and + category editors can *load* an existing definition, not just write one + (closes the gaps in `openswmm.gui/docs/HANDOFF_compile_verify_agent.md` + §5.2–§5.2f): + - Pollutant: `swmm_pollutant_set_units` (inverse of the existing + `_get_units`; pre-start-only). + - Aquifer: `swmm_aquifer_get_evap_pattern` / `_set_evap_pattern` — the one + string column (`[ETupat]`) the param-code API didn't cover. + - Snowpack (previously add/list-only): `swmm_snowpack_set/get_plowable`, + `_impervious`, `_pervious` (the seven `[SNOWPACKS]` surface values), + `_set/get_removal` (six values) and `_set/get_removal_subcatch`. + - Inlet: `swmm_inlet_get_params` (inverse of `_set_params`) and + `swmm_inlet_get_type`. + - LID: `swmm_lid_get_surface` / `_soil` / `_storage` / `_drain` (inverse of + the four layer setters), `swmm_lid_get_type`, and full set/get for the + remaining two layers — `swmm_lid_set/get_pavement` (6 values: thick, + void-ratio, frac-imperv, ksat, clog-factor, regen-days) and + `swmm_lid_set/get_drainmat` (3 values: thick, void-frac, roughness) — so + PERM_PAVEMENT and GREEN_ROOF controls now round-trip every layer. + - Python bindings for all of the above (`Pollutant.units` setter, + `Aquifers.get/set_evap_pattern`, `Snowpacks.set/get_*`, + `Inlets.get_params/get_type`, + `LIDs.get_surface/soil/storage/drain/type` + `set/get_pavement` + + `set/get_drainmat`) with `.pyi` stubs. Tests: + `tests/unit/engine/test_editor_roundtrip_api.cpp` (bit-exact C round-trips) + and `python/tests/engine/test_editor_roundtrip.py`. All six LID layers are + now covered end-to-end. + +- **Phase 4 wave B1 — runtime time-pattern factors (P6) & street sweeping + (P4).** Audited the mid-run mutation semantics of both groups and added the + legacy parity setters so both engines share the contract: + - Legacy `swmm_TIME_PATTERN` object type + `swmm_PatternProperty` + (`FACTOR` with the factor index in `subIndex`, read-only `COUNT`/`TYPE`) + and `swmm_LANDUSE` + `swmm_LanduseProperty` + (`SWEEP_INTERVAL`/`SWEEP_REMOVAL`) via new `set/getPatternValue` / + `set/getLanduseValue` in `swmm5.c`; settable pre-start and while running + (both are per-step lookups). Python `SWMMPatternProperties` / + `SWMMLandUseProperties` enums (+ `.pyi`), enum coverage, and parity tests + `python/tests/legacy/test_param_runtime.py`. + - Refactored audit tests `python/tests/engine/test_param_runtime.py`. +- **Phase 4 wave B2 — runtime buildup/washoff function coefficients (P2).** + Both groups SOUND mid-run; the accumulated buildup pool is preserved (an + edit only changes how buildup evolves going forward). Legacy parity: + `swmm_LanduseProperty` extended with `BUILDUP_FUNC`/`COEFF1..3`/`NORMALIZER` + and `WASHOFF_FUNC`/`COEFF`/`EXPON`/`SWEEP_EFFIC`/`BMP_EFFIC` (pollutant index + via `subIndex`); `set/getLanduseValue` gained `subIndex`; buildup edits + recompute `maxDays` per `landuse_readBuildup`. Tests in + `test_param_runtime.py` (engine + legacy). +- **Phase 4 wave B3 — infiltration parameters (P1): documented pre-start-only.** + The audit found the refactored infiltration setters + (`swmm_subcatch_set_infil_horton`/`_green_ampt`/`_curve_number`) are guarded + to the editable states by `CHECK_GEOMETRY` and raise `LifecycleError` while + running (correcting the gap-plan's "no running guard" note); the + per-subcatchment infiltration state is built once at `start()`. P1 is + therefore a pre-start edit in both engines (no legacy parity setter). Tests + in `test_param_runtime.py::TestInfiltrationParams`. +- **Phase 4 wave B4 — pollutant kinetics (P5).** `kdecay` / co-pollutant / + snow-only are read live each step (sound mid-run, no cache); legacy parity + added via `swmm_PollutProperty` `KDECAY`/`CO_POLLUTANT`/`CO_FRACTION`/ + `SNOW_ONLY` (kdecay accepted in 1/day, stored as the legacy 1/sec). The + initial network concentration (`INIT_CONCEN`) has no per-step consumer, so + both engines now reject it mid-run (refactored `CHECK_GEOMETRY` + → `LifecycleError`; legacy `ERR_API_IS_RUNNING`). `SWMMPollutantProperties` + 4→9 (+ `.pyi`, enum coverage). Tests in `test_param_runtime.py`. +- **Phase 4 wave B5 — external-inflow / DWF baselines & scale (P7/P8).** The + inflow solver caches ext/DWF definitions at start (same cache class as P6). + New direct setters `swmm_ext_inflow_set_scale` / `_set_baseline` and + `swmm_dwf_set_baseline` (plus the add/remove paths) now refresh that cache so + a mid-run edit takes effect on the next step; bindings + `Inflows.set_external_scale` / `_baseline` / `set_dwf_baseline`. Legacy + parity for node-keyed `[INFLOWS]`/`[DWF]` baseline editing is deferred (the + legacy per-node linked-list inflow model has no flat-index API; runtime + inflow control remains available via `swmm_NODE_LATFLOW`) — see the audit + doc. Tests: `TestInflowBaselineRuntime`. +- **Phase 4 wave B6 — treatment expressions (P3).** SOUND mid-run with a + cache-refresh fix (same class as P6/P2/P7): the step loop evaluates the + compiled-expression cache built at start, so `swmm_treatment_set`/`_clear` + now recompile the edited (node, pollutant) cell via + `SWMMEngine::refreshTreatment` — an edit/replace/clear takes effect on the + next step, and a failed parse is rejected (`BadParamError`) with the + previous expression restored. Legacy parity via dedicated functions (an + expression cannot ride `setNodeValue`): `swmm_setTreatment` / + `swmm_clearTreatment` re-use the `[TREATMENT]` input parser, freeing any + prior `MathExpr` so runtime replaces don't leak; Python + `Solver.set_treatment`/`clear_treatment` (+ `.pyi`). Tests: + `TestTreatmentRuntime` (engine), `TestLegacyTreatment` (legacy). +- **Phase 4 wave B7 — LID layer parameters (P11).** The four refactored LID + setters were silent no-op stubs; they now write `ctx.lid_controls.*` for + real. Split contract: surface/soil/storage are **pre-start-only** (they seed + per-unit LID state at start; `LifecycleError` mid-run, physical bounds + enforced) while the **drain** coefficients are runtime-editable + (`SWMMEngine::refreshLIDDrainParams` re-copies the per-unit drain columns + the step loop reads — the cistern/rain-barrel RTC knob). Legacy parity for + the sound group: `lid_setDrainParams` (`lid.c`, input-file units matching + `readDrainData`) + exported `swmm_setLidDrain` + `Solver.set_lid_drain` + binding. Tests: `TestLidParamsRuntime` (paired deterministic runs diverge + only after the mid-run drain edit), `TestLegacyLidDrain`. Flagged + follow-up: the refactored LID module lacks unit conversion of layer params + (consumes raw input values; legacy converts via UCF) — see the audit doc. +- **Phase 4 wave B8 — aquifer parameters (P10).** New setter in both engines + (none existed before). Flux coefficients (conductivity, conductivity slope, + tension slope, upper-evap fraction, lower-evap depth, lower-loss coefficient) + are runtime-editable — refactored `SWMMEngine::refreshAquiferParams` re-derives + the groundwater solver's per-subcatchment flux columns on each edit; legacy + reads `Aquifer[]` live. Structural / initial-condition parameters (porosity, + wilting point, field capacity, bottom/water-table elevation, upper moisture) + seed GW state and are pre-start-only (`LifecycleError` / `ERR_API_IS_RUNNING` + while running). Refactored `swmm_aquifer_get_param`/`_set_param` + + `SWMM_AquiferParam` enum, binding `Aquifers.get_param`/`set_param` + + `AquiferParam`; legacy `swmm_AquiferProperty` (800 block) via the existing + `swmm_AQUIFER` object case, binding `SWMMAquiferProperties`. Enum coverage + +12 (aquifer). Tests: `TestAquiferParamsRuntime`, `TestLegacyAquiferParams`. +- **Phase 4 wave B9 — adjustment arrays (P9): no setter, decision recorded.** + The audit closes the gap matrix without new code: the monthly climate + adjustment arrays are covered at runtime by the more direct Phase-1 forcing + setters, and the per-subcatchment N-PERV/DSTORE/INFIL adjustment patterns are + retunable mid-run via the P6 pattern-factor setter. See the audit doc for the + rationale. This completes the Phase 4 parameter-surface audit — every + §12.1 group now has a recorded disposition. + +- **§3 legacy water-quality source setters — functional tests.** The legacy + `setPollutValue` source concentrations (rain/wet-deposition `pptConcen`, + groundwater `gwConcen`, RDII `rdiiConcen`, dry-weather `dwfConcen`) and the + ponded-surface quality injection are now covered by real-solver tests: + `python/tests/legacy/test_quality_sources.py` (Q1 rain → washoff, Q2 GW, + Q3 RDII, Q5 DWF, Q6 ponded round-trip + washoff). Each source feeds an + existing inflow term already counted in the quality mass balance. Fixtures + derive a self-contained inline-storm copy of `legacy_small.inp` (quality + routing enabled, orphan external stage timeseries dropped, two-day horizon) + with one TSS pollutant whose source columns are set one at a time so each + node signal is attributable to a single source; artifacts land in + `python/tests/legacy/output/`. + +### Fixed + +- **Refactored DWF/external-inflow patterns ignored mid-run edits.** + `InflowSolver::init` copies pattern factors into a per-step lookup cache, so + `swmm_pattern_set_factors` (which mutates `ctx.patterns`) had no effect on + DWF/external inflow mid-run (groundwater-evap patterns read the live context + and were unaffected). Added `InflowSolver::refreshPatterns` + + `SWMMEngine::inflowSolver()`; the setter now refreshes the cache so an edit + takes effect on the next step. +- **Legacy subcatchment pollutant bindings** passed the pollutant index in + the `sub_index` slot, but the C `getSubcatchValue`/`setSubcatchValue` + pollutant cases read it from `pollutantIndex`. `LegacySubcatchment` + `get_pollutant_buildup`, `set_external_pollutant_buildup`, and + `set_ponded_concentration` now pass `pollutant_index=` and work at runtime + (previously raised an object-index API error). + +## [Unreleased] — User-flag schema bindings + 2D/MCP gap closure + +See `docs/API_GAP_CLOSURE_PLAN_2026-06-10.md`. + +### Added + +- **Python bindings:** the user-flag schema C API (`swmm_userflag_define` + / `undefine` / `def_count` / `def_get` / `value_get` / `value_set` / + `value_clear`) is now bound — the last unbound block of the 702-function + engine surface. `ModelBuilder` gains `define_userflag`, + `undefine_userflag`, `userflag_def_count`, `get_userflag_def`, and + `get/set/clear_userflag_value`; the `solver.userflags` view gains + `define()`, `undefine()`, `definitions()` (returning `UserFlagDef` + records), `get_value()` / `set_value()` / `clear_value()`, real + `len()` / iteration over definitions, and STRING-flag support in the + mapping interface. New `UserFlagType` enum (BOOLEAN / INTEGER / REAL / + STRING). Tests: `python/tests/engine/test_userflags_schema.py`. +- **Python bindings:** new lazy `Solver.surface2d` property returning the + cached `Surface2D` view, so 2D access no longer requires constructing + `Surface2D(solver.handle)` by hand. + +### Changed + +- **Python bindings:** `del solver.userflags[name]` now removes the + flag's schema definition and per-object values via + `swmm_userflag_undefine` (previously raised `TypeError`); assigning a + `str` value auto-defines a STRING flag, mirroring the scalar setters. +- `python/tests/test_api_coverage.py`: removed 54 stale `KNOWN_UNBOUND` + allowlist entries for symbols that had since been bound; the allowlist + is now empty and the coverage test enforces the full surface. + +## [Unreleased] — Runtime forcing verification pass (handoff build/test/fix) + +Builds, runs and verifies the runtime-forcing batch per +`docs/RUNTIME_FORCING_TESTING_HANDOFF.md`. Full Python suite green +(833 passed) and the C++ unit suite green (78/78, 2D enabled). Thin bindings +(§2) and functional tests (§3: M4, M5, S1, S2, T1, Q4, Q5 + GW quality) added. + +### Fixed + +- **`[POLLUTANTS]` concentrations silently zeroed:** `PostParseResolver` + unconditionally re-ran `resize_pollutants()` (which zero-fills) *after* + `handle_pollutants` had parsed the values, so every INP rain/GW/RDII/DWF/ + init concentration loaded as 0 (the DWF/GW/rain quality features did + nothing for INP-driven models). Guarded the resize like the adjacent + node/link resizes (`if count != n`). +- **Refactored snow never accumulated:** the `[SUBCATCHMENTS]` snow-pack + column was never read (no deferred name resolution) and the snow solver's + per-subarea `fArea` was never initialised, so `plowSnow`/melt treated + every surface as zero-area. Added `snowpack_name` deferred resolution and + `fArea` init (legacy `snow_initSnowpack`). +- **Climate temperature/wind forcing stuck after clear:** a one-shot or + cleared prescription never reverted because the forcing overwrote the same + `ClimateState` field it read as the broadcast base. Added + `temperature_src`/`wind_speed_src` source bases resolved fresh each step. +- **`ForcingData::effective_rainfall`/`_snowfall` out-of-bounds:** lacked the + size guard `effective_evap_rate` has, segfaulting direct-solver unit tests + with unsized forcing arrays. +- **Legacy `get_value` misread valid negatives:** `swmm_getValueExpanded`'s + return was validated by sign, so a sub-freezing air temperature or the + −999 API-unset sentinel raised a spurious error. Now keys off the system + ERROR_CODE and the API-error sentinel range. +- **Groundwater inflow quality (audit A5):** GW inflow pollutant mass + (`q_gw × c_gw`) was never applied. Added `QualitySolver::addGwLoads()` and + a `qual_routing_gw_in` bucket; the report's Groundwater Inflow quality row + (previously hardcoded 0) and the quality continuity total now include it + (and DWF). +- **2D mass-balance evaporation (§4.1):** moved the cumulative evap loss from + the 2D state mirror into `MassBalance2D::evap_out`, folded into `error()`, + and surfaced in the report's 2D continuity block. +- **`[POLLUTANTS]` writer round-trip:** `InpWriter` now writes the `Cdwf` + and `Cinit` columns (§4.4). +- **numpy 1.x build:** `PatchNumpyPxd.cmake` only rewrites the Cython pxd on + numpy ≥ 2.0 (it would break the numpy 1.x build it was meant to support). +- Deleted dead `SnowSolver::batchATIUpdate`/`batchAccumulate` (§4.3); added + scalar `execute`/`plowSnow` convenience overloads (per-subcatchment array + signatures broke `test_snow.cpp`). + +## [Unreleased] — Runtime forcing API phases 1–3 complete (gap plan rows 5–12) + +See `docs/RUNTIME_FORCING_API_GAP_PLAN.md` and +`docs/RUNTIME_FORCING_TESTING_HANDOFF.md` (functional verification pending). + +### Added + +- **Global evaporation prescription (M4):** legacy `swmm_API_EVAP` system + property (replaces the post-adjustment `Evap.rate` for all consumers + incl. conduits/storage; per-subcatchment PET still wins); refactored + `swmm_forcing_climate_evap()` channel. Python: + `LegacySystem.set/get/clear_api_evap_rate`, `Forcing.climate_evap`. +- **DRY_ONLY runtime toggle (M5):** legacy `swmm_EVAP_DRY_ONLY`; + refactored `swmm_climate_set/get_dry_only()`. Python: + `LegacySystem.set/get_evap_dry_only`, `Forcing.climate_dry_only`. +- **Legacy pollutant source setters (Q1–Q3, Q5):** new `swmm_POLLUTANT` + dispatch with `swmm_POLLUT_RAIN/GW/RDII/DWF_CONCEN` (500 block), + runtime-settable; `SWMMPollutantProperties` Python enum. +- **Refactored link quality forcing channel (Q4):** + `swmm_forcing_link_quality()` (REPLACE = concentration, ADD = mass rate, + mass-balanced); `Forcing.link_quality`; `ForcingType.LINK_QUALITY`. +- **Legacy ponded-quality injection (Q6):** + `swmm_SUBCATCH_POLLUTANT_PONDED_CONCENTRATION` now settable while running. +- **Groundwater state injection (S1):** legacy `swmm_SUBCATCH_GW_MOISTURE` + / `_GW_LOWER_DEPTH` (set/get via `gwater_get/setState`); refactored + `swmm_subcatch_set/get_gw_state()` with porosity/thickness clamping. +- **Snowpack state injection (S2):** legacy `swmm_SUBCATCH_SNOW_SWE/_FW/ + _ATI/_COLDC` (per snow subarea via sub_index); refactored + `swmm_subcatch_set/get_snow_state()`. +- **2D mesh evaporation (T1):** depth-limited evaporation sink inside the + CVODE RHS; `swmm_2d_force_evap()` / `swmm_2d_force_evap_uniform()` + (m/s, OVERRIDE/ADD, RESET/PERSIST); `swmm_2d_get_mass_balance()` gained + an `evap_out` total; Python `Surface2D.force_evap/_uniform`. + +### Fixed + +- **Refactored DWF quality (audit A1):** the `[POLLUTANTS]` `Cdwf` column + was parsed and discarded and dry weather quality inflow did not exist. + Added `PollutantData.c_dwf`, `QualitySolver::addDwfLoads()` (mirroring + RDII loads), a `qual_routing_dw_in` mass-balance bucket included in the + continuity error, the report's Dry Weather Inflow row (previously + hardcoded 0), and `swmm_pollutant_set/get_dwf_conc()`. + +### Audits + +- A3: refactored ponded-quality and buildup setters are runtime-callable + (no running guards) — functional verification handed off. +- A4: the 2D solver has **no infiltration sink** (T2 remains out of scope). +- Phase 4 parameter-surface audits are execution-gated — protocol in + `docs/RUNTIME_FORCING_TESTING_HANDOFF.md` §6. + +## [Unreleased] — Snowfall forcing + snow-path repair (gap plan row 4) + +See `docs/RUNTIME_FORCING_API_GAP_PLAN.md` (item M3). + +### Added + +- **Per-subcatchment snowfall forcing (M3):** refactored + `swmm_forcing_subcatch_snowfall()` (in/hr US, mm/hr SI as SWE; + OVERRIDE/ADD, RESET/PERSIST) with `Forcing.subcatchment_snowfall()` and + `ForcingType.SUBCATCH_SNOWFALL`; resolves on the temperature-split gage + snowfall before accumulation, plowing, and melt. Legacy already had + `swmm_SUBCATCH_API_SNOWFALL`. +- New checked-in snow fixture `python/tests/data/solver/site_drainage_snow.inp` + (snow pack on S1, constant 25 °F temperature series). + +### Fixed + +- **Refactored snow path:** snowfall never accumulated — `plowSnow()` was + never called from the step pipeline, so packs could melt but never grow. + Accumulation + plowing now runs each runoff step before melt (matching + legacy `runoff.c` order), and the snow solver takes per-subcatchment + rain/snow inputs (previously a single area-weighted broadcast), with + per-subcatchment rain-on-snow vs. degree-day melt selection (matching + legacy `snow_getSnowMelt`/`meltSnowpack`). +- **Legacy `apiSnowfall` continuity hole:** prescribed snowfall influenced + melt computations but never accumulated in the pack (only gage snow did, + via `snow_plowSnow`), while still counting as rainfall inflow in the + runoff mass balance. `snow_plowSnow()` now includes `apiSnowfall`. +- **Refactored `swmm_subcatch_get_snow_depth()`** was a stub returning 0; + it now returns the area-weighted pack SWE in user depth units via the + new `SWMMEngine::subcatchSnowDepth()`. + +## [Unreleased] — Runtime climate forcing (gap plan rows 1–3) + +See `docs/RUNTIME_FORCING_API_GAP_PLAN.md` (items A2, A5, M1, M2). + +### Added + +- **Air temperature forcing (M1):** legacy `swmm_API_TEMPERATURE` system + property (set while running; `<= -999` clears) with read-only + `swmm_TEMPERATURE`; refactored `swmm_forcing_climate_temperature()` + channel (OVERRIDE/ADD, RESET/PERSIST) with `swmm_climate_get_temperature()`. + Applied before derived climate quantities (saturation vapor pressure, + psychrometric constant, Hargreaves moving average) so snowmelt and + temperature-evap consumers stay consistent. User units (°F US, °C SI). +- **Wind speed forcing (M2):** legacy `swmm_API_WINDSPEED` (negative + clears) with read-only `swmm_WINDSPEED`; refactored + `swmm_forcing_climate_wind()` with `swmm_climate_get_wind_speed()`. + User units (mph US, km/hr SI). +- Python: `LegacySystem.set/get/clear_api_temperature` and + `…_api_wind_speed`; refactored `Forcing.climate_temperature/_wind` + setters, `get_climate_temperature/_wind_speed` getters, and + `ForcingTarget.CLIMATE` for `Forcing.clear`. + +### Fixed + +- **Subcatchment rainfall forcing (A2):** `swmm_forcing_subcatch_rainfall()` + previously had no effect — `applyForcings()` pre-wrote + `subcatches.rainfall`, which the runoff solver then overwrote from the + gage. The forcing now resolves inside the runoff solver's rainfall + assembly (same pattern as the PET forcing fix). +- **`Forcing.clear()` channel mapping (A5):** the Python binding passed + `ForcingTarget` object-kind codes where C `SWMM_ForcingType` channel + codes were expected, so clearing a SUBCATCH actually cleared node + quality. It now clears every channel belonging to the requested object. + +## [Unreleased] — Subcatchment PET prescription + +See `docs/SUBCATCHMENT_PET_PRESCRIPTION_PLAN.md`. + +### Added + +- **Legacy engine:** new `swmm_SUBCATCH_API_PET` subcatchment property — + prescribe a potential evapotranspiration rate (in/day or mm/day) per + subcatchment at runtime. The prescribed rate replaces the climate-derived + `Evap.rate` for surface, LID, and groundwater upper-zone evaporation + (bypassing `DRY_ONLY` and monthly adjustments); a negative value clears + it. New read-only `swmm_EVAPRATE` system property returns the current + climate-derived rate. Python: `LegacySubcatchment.set_api_pet` / + `get_api_pet` / `clear_api_pet`, `LegacySystem.get_evap_rate`. +- **Refactored engine:** new `swmm_climate_get_evap_rate()` C API getter + and `Forcing.climate_evap_rate()` Python method for caller-side + adjustment composition. + +### Fixed + +- **Refactored engine:** `swmm_forcing_subcatch_evap()` previously had no + effect — it overwrote `evap_loss` before the runoff solver recomputed it. + It now prescribes a PET *rate* (user units: in/day US, mm/day SI — + previously documented as ft/sec) consumed by the runoff, LID, and + groundwater solvers, so capping to available water and mass-balance + accounting happen along the normal computation paths. + +## [Unreleased] — Pythonic Python bindings (v1) + +### Changed — **breaking** (Python bindings only; C API unchanged) + +A full property-style rewrite of the `openswmm.engine` Python surface. +See `docs/PYTHONIC_BINDINGS_PLAN.md` and the +`docs/PYTHONIC_BINDINGS_DONE.md` wrap-up. The C API is untouched. + +#### Solver lifecycle + +- Lifecycle methods (`open`, `initialize`, `start`, `step`, `stride`, + `end`, `report`, `close`) **raise on failure** instead of returning + integer codes. `step()` and `stride()` return a + `datetime.timedelta`; `timedelta(0)` is the end-of-simulation sentinel. +- `Solver.state` now returns the `EngineState` enum. +- `Solver.elapsed` and `Solver.routing_step` are `datetime.timedelta`. +- New `Solver.start_datetime`, `end_datetime`, `current_datetime`, + `report_start_datetime` return `datetime.datetime`. +- New `Solver.steps()` iterator and `Solver.until(target)` (accepts + `datetime` or `timedelta`). +- Every file argument accepts `pathlib.Path` / `os.PathLike`. + +#### Views on the Solver + +- `solver.options` — `MutableMapping` over `[OPTIONS]` plus typed + shortcuts (`start_datetime`, `routing_step`, …). +- `solver.userflags` — `MutableMapping` with auto-typed bool/int/float. +- `solver.events` — `MutableSequence[Event]` (each entry carries + `datetime` `start` / `end`). +- `solver.save_schedule` — `MutableSequence[SaveScheduleEntry]` for the + `[SAVE HOTSTART]` block. + +#### Domain collections + wrappers + +Each `solver.` returns a collection that is **indexable by +`int | str`**, iterable, and `len`-able. Items are typed wrapper +objects with property-style access: + +- `solver.nodes["J1"].depth = 1.2` +- `solver.links["C1"].xsect = (XSectShape.CIRCULAR, 1.0, 0, 0, 0)` +- `solver.subcatchments["S1"].infiltration.set_horton(...)` +- `solver.gages["RG1"].rainfall = 25.4` +- `solver.pollutants["TSS"].kdecay = 0.05` + +Per-type sub-views raise `AttributeError` on wrong-type nodes/links: +`node.outfall` only on OUTFALL, `node.storage` only on STORAGE, +`link.pump` only on PUMP, etc. + +Bulk numpy access is now a property pair: +`solver.nodes.depths` / `solver.links.flows` / `solver.subcatchments.runoffs`. + +#### OutputReader + +- Path-agnostic constructor (`str` / `Path`). +- Typed metadata: `start_datetime`, `report_step` (`timedelta`), + `flow_units` (`FlowUnits` enum), `period_times` + (`np.ndarray[datetime64[s]]`), `node_ids` / `link_ids` / + `subcatchment_ids` lists. +- Variable-selector arguments require an enum (`OutNodeVar` etc.); + object selectors accept `int | str`. +- `node_attributes(key, period)` returns `Dict[OutNodeVar, float]`. +- `node_stats(key)` returns a typed view with `max_depth`, + `max_overflow`, `vol_flooded`, `time_flooded`. + +#### MassBalance, Statistics, HotStart, Tables + +- `solver.mass_balance.routing_diagnostics` returns the + `RoutingDiagnostics` dataclass. +- `solver.statistics._` are all bulk numpy properties. +- `HotStart.open(path)` classmethod / `HotStart.save_from(solver, path)` + static; `sim_datetime` (`datetime`), `warnings` (`list[str]`), + `apply(solver)` on the hot-start. +- `solver.tables` exposes `TimeSeries.points` as a structured numpy + array `(time: datetime64[s], value: float64)`. `solver.patterns` is a + separate indexable collection. + +#### Exceptions + +New `EngineError` hierarchy in `openswmm.engine._exceptions`. Every +subclass **also** inherits from a standard-library exception: + +- `BadIndexError(EngineError, IndexError)` +- `BadParamError(EngineError, ValueError)` +- `LifecycleError(EngineError, RuntimeError)` +- `HotStartError(EngineError, RuntimeError)` +- `FileError(EngineError, IOError)` +- `ParseError(EngineError, ValueError)` +- `NumericalError(EngineError, RuntimeError)` +- `CRSError(EngineError, ValueError)` +- `DependencyError(EngineError, RuntimeError)` +- `PluginError(EngineError, RuntimeError)` +- `BadHandleError(EngineError, RuntimeError)` +- `StaleObjectError(LifecycleError)` — raised when a wrapper's + generation counter no longer matches the solver's after a + rename/delete. + +Every `EngineError` carries `.code` (raw int), `.code_enum` +(`ErrorCode` member), `.message` (filled by the C API). + +#### New enums + +- `OrificeType`, `WeirType`, `OutletRatingType` (in `openswmm_links.h`). +- `ErrorCode.DEPENDENCY = 15` (was missing from the Python side). + +#### DateTime conversion C API + +- New `include/openswmm/engine/openswmm_datetime.h` exposes + encode/decode/`add_seconds`/`time_diff` primitives matching the + legacy `datetime.c` bit-for-bit. +- Reached from Python through `openswmm.engine.datetime_api` (the + Cython binding) plus the high-level `oadate_to_datetime` / + `datetime_to_oadate` helpers. +- All "Julian date" wording removed from the C API header + documentation; the convention is documented as the OLE Automation / + Delphi TDateTime epoch (1899-12-30) — **not** astronomical Julian. + +### Documentation + +- Sphinx CI gate (`sphinx-build -W --keep-going`) was already in place + and is kept; every guide page renders warning-free against the new + `.pyi` stubs. +- New `guide/datetime.rst`, `guide/plotting.rst` pages. +- `guide/concepts.rst`, `guide/error_handling.rst`, every domain + guide and the migration page all rewritten for the v1 surface. +- v0 → v1 cheat sheet appended to `migration/swmm5_to_swmm6.rst`. + +### Test-suite migration (now landed) + +The legacy per-domain `test_*.py` files have been processed: + +- **Duplicated coverage neutralised** — `test_nodes.py`, + `test_links.py`, `test_subcatchments.py`, `test_gages.py`, + `test_massbalance.py`, `test_output_reader.py`, `test_hotstart.py`, + `test_spatial.py`, `test_infrastructure.py`, + `test_quality_pollutants.py`, `test_tables.py`, `test_new_api.py`, + `test_new_modules.py`, and all `*_expanded.py` files are now + module-level `pytest.skip()` stubs (each names its replacement + `*_pythonic.py` file in the docstring). They can be `git rm`-ed in + a subsequent sweep without changing CI behaviour. +- **Unique-scenario tests migrated to v1** — `test_integration.py`, + `test_callbacks_and_xsect.py`, `test_workflow.py`, + `test_opened_state_editing.py`, `test_concurrent_simulation.py`, + `test_controls_advancement.py`, `test_controls_inflows.py`, + `test_rdii_advancement.py`, `test_solver.py`. Each uses the v1 + surface (`solver.nodes["J1"].depth`, `for elapsed in solver.steps()`, + `solver.links[0].xsect = (...)`, etc.). + +### mypy gate + +- `python/pyproject.toml` ships a `[tool.mypy]` block: default-mode + check across the whole `openswmm.engine` package plus strict-mode on + the pure-Python modules (`_enums`, `_exceptions`, `_dates`). +- `python/tests/typing/test_surface.py` exercises every public symbol + with explicit type annotations — runs under strict mode. +- New CI workflow `.github/workflows/typing.yml` runs both passes on + every PR touching the bindings or the typing test. + +## [6.0.0-alpha.1] — 2026-03-25 + +### Added + +#### New Engine Architecture +- **Data-oriented engine** — Refactored core data structures to Structure of Arrays (SoA) layout for cache efficiency and SIMD-friendly computation. +- **Reentrant design** — All simulation state encapsulated in an opaque `SWMM_Engine` handle, eliminating global state and enabling multiple independent simulations per process. +- **Plugin-based I/O** — Output and report writing abstracted through a plugin interface with a dedicated I/O thread and double-buffered snapshots. +- **Engine lifecycle state machine** — Explicit states: CREATED → OPENED → INITIALIZED → STARTED → RUNNING → ENDED → CLOSED. + +#### Comprehensive C API (19 headers) +- `openswmm_engine.h` — Engine lifecycle, error codes, state machine. +- `openswmm_model.h` — Model building, validation, serialization, options. +- `openswmm_nodes.h` — Junctions, outfalls, storage nodes, dividers. +- `openswmm_links.h` — Conduits, pumps, orifices, weirs, outlets with 20 cross-section shapes. +- `openswmm_subcatchments.h` — Subcatchments, infiltration (Horton/Green-Ampt/Curve Number), landuse coverage. +- `openswmm_gages.h` — Rain gages with timeseries and file data sources. +- `openswmm_pollutants.h` — Pollutant definitions and runtime quality injection. +- `openswmm_tables.h` — Time series, curves, patterns, and cursor-optimized lookups. +- `openswmm_inflows.h` — External inflows, dry weather flow, RDII. +- `openswmm_controls.h` — Control rule expressions and direct link setting/status actions. +- `openswmm_infrastructure.h` — Transects, streets, inlets, LID controls and LID usage. +- `openswmm_spatial.h` — CRS, coordinates, polylines, polygons for all object types. +- `openswmm_quality.h` — Landuse, buildup/washoff functions, treatment expressions. +- `openswmm_massbalance.h` — Continuity errors and cumulative flux totals. +- `openswmm_callbacks.h` — Progress, warning, step-begin/end, plugin state, and hot-start-missing callbacks. +- `openswmm_hotstart.h` — Hot start file save/load/modify/query with workflow examples. +- `openswmm_statistics.h` — Node, link, and subcatchment simulation statistics. +- `openswmm_engine_export.h` — Auto-generated shared library export macros. + +#### Features +- **Hot start API** — Save, open, modify, query, and close hot start files through a transparent C ABI. +- **CRS support** — Coordinate reference system specification via OPTIONS section. +- **User flags** — Custom USER_FLAGS section for user-defined metadata on objects. +- **Plugin SDK** — Header-only development kit for building output/report plugins. +- **HEC-22 inlet analysis** — Street inlet capture with grate, curb, slotted, and custom inlet types (from SWMM 5.2). +- **Variable speed pumps** — Type5 pump curves with speed scaling. +- **New storage shapes** — Conical and pyramidal shapes with elliptical/rectangular bases. +- **Python bindings** — Cython-based bindings with solver context manager, iterative stepping, and output reading. + +#### Testing & CI +- **Google Test migration** — All unit tests converted from Boost.Test to Google Test 1.15.2. +- **Comprehensive test suite** — 73+ legacy engine tests, 41 legacy output tests, and new engine unit tests. +- **Reorganized test structure** — `tests/unit/legacy/{engine,output}` and `tests/unit/{engine,output}`. +- **Multi-platform CI** — GitHub Actions for Windows x64, Linux x64, macOS x64, and macOS ARM64. +- **Performance benchmarks** — Google Benchmark integration for critical-path profiling. + +#### Documentation +- **Doxygen API documentation** — All 19 public C API headers thoroughly documented with `@brief`, `@details`, `@param`, `@returns`, `@see`, and `@note` tags. +- **Technical reference manuals** — Hydrology, Hydraulics, and Water Quality reference manuals updated for OpenSWMM. +- **User manual** — Comprehensive user manual with modeling capabilities, typical applications, and input/output descriptions. +- **Author/license metadata** — All new engine source files annotated with `@author`, `@copyright`, and `@license` Doxygen tags. + +### Changed + +- **Project renamed** from `OpenSWMMCore` to `openswmm` with `openswmm.engine` as the primary library output name. +- **CMake minimum version** raised to 3.21 (from 3.15). +- **C++ standard** set to C++20 (from C++11/14). +- **C standard** set to C17. +- **CMake options** namespaced to `OPENSWMM_*` prefix (legacy `OPENSWMMCORE_*` aliases preserved). +- **Version scheme** updated to SemVer 2.0.0 with pre-release tags. +- **vcpkg** adopted as the dependency manager (replacing NuGet-based Boost distribution). +- **CI/CD pipelines** cleaned up: updated to `actions/checkout@v4`, `actions/setup-python@v5`, `actions/upload-artifact@v4`; removed stale branch triggers; fixed CMake flag from `-DBUILD_TESTS=ON` to `-DOPENSWMM_BUILD_TESTS=ON`. + +### Removed + +- **Boost.Test dependency** — Replaced entirely by Google Test. +- **NuGet package dependency** — Regression testing no longer requires external NuGet-hosted Boost packages. +- **Global state** — Eliminated from the new engine (legacy solver globals preserved in `src/legacy/`). + +### Fixed + +- **CI CMake flag** — Unit testing workflow was passing `-DBUILD_TESTS=ON` which did not match the actual `OPENSWMM_BUILD_TESTS` option, preventing tests from being built in CI. +- **Documentation workflow** — Removed stale `bug_fixes` branch trigger; updated to `actions/checkout@v4`. +- **Export header** — Fixed misplaced `@author`/`@copyright` block that was injected inside a `#define` preprocessor directive in `openswmm_engine_export.h`. + +## [5.2.0] — Legacy + +Last EPA-maintained release. See [docs/SWMM_5.2.0.md](docs/SWMM_5.2.0.md) for details on HEC-22 inlet analysis, new storage shapes, variable speed pumps, and control rule enhancements. diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..84591ce21 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,11 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +authors: +- family-names: "Buahin" + given-names: "Caleb" + orcid: "https://orcid.org/0000-0002-9859-2264" +title: "Open-Source SWMM (SWMM2D): Reimagining the Stormwater Management Model for the Digital Water Transformation" +version: 6.0.0 +doi: https://doi.org/10.1016/j.envsoft.2018.07.015 +date-released: 2025-01-06 +url: "https://github.com/hydrocouple/openswmm.engine" \ No newline at end of file diff --git a/CLA.md b/CLA.md new file mode 100644 index 000000000..387f1ed97 --- /dev/null +++ b/CLA.md @@ -0,0 +1,126 @@ +# Contributor License Agreement — openswmm.engine + +**Version 1.0 — May 2026** + +Thank you for your interest in contributing to **openswmm.engine**, maintained by the Technical Manager of the project (currently [@cbuahin](https://github.com/cbuahin)). This Contributor License Agreement ("CLA") clarifies the intellectual property rights granted with contributions to the project. By signing this CLA you confirm that you have the legal authority to grant these rights, and that the Technical Manager may rely on them. + +This CLA does **not** transfer your copyright to the Technical Manager. You retain full ownership of your contributions. The CLA grants a license that enables the project to be distributed, maintained, and relicensed in the future without requiring additional consent from every contributor. + +The Technical Manager role may transfer over time per the succession process described in [CONTRIBUTING.md §3](./CONTRIBUTING.md#3-succession--delegation). Rights granted under this CLA persist through any such transition and vest in whoever holds the Technical Manager role at the time they are exercised. + +--- + +## 1. Definitions + +- **"You"** — the individual, or the legal entity on whose behalf an individual is signing, submitting a contribution. +- **"Contribution"** — any original work of authorship, including source code, documentation, tests, configuration, data, UI assets, or any other material, intentionally submitted to the project by You via a pull request, patch, issue attachment, or any other means. +- **"Project"** — the openswmm.engine software repository hosted at https://github.com/HydroCouple/openswmm.engine. +- **"Technical Manager"** — the current holder of the Technical Manager role for the Project, as defined in [CONTRIBUTING.md §2](./CONTRIBUTING.md#2-repository--technical-management). The current Technical Manager is [@cbuahin](https://github.com/cbuahin). + +--- + +## 2. Grant of Copyright License + +Subject to the terms of this CLA, You hereby grant to the Technical Manager a **perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license** to: + +- reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works; +- **relicense** Your Contributions under any license, including but not limited to the GPLv3, LGPL, AGPL, MIT, or a commercial license, at the Technical Manager's sole discretion and without further notice to You. + +The relicensing right is granted specifically to preserve the Technical Manager's ability to adapt the project's licensing terms over time (for example, to offer a dual open-source/commercial licensing model) without being required to seek individual consent from each contributor. + +--- + +## 3. Grant of Patent License + +Subject to the terms of this CLA, You hereby grant to the Technical Manager and to recipients of software distributed by the Technical Manager a **perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable patent license** to make, have made, use, offer to sell, sell, import, and otherwise transfer Your Contributions, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution alone or by combination of Your Contribution with the Project. + +If any entity institutes patent litigation against You or any other party alleging that Your Contribution constitutes patent infringement, any patent licenses granted to that entity under this CLA for that Contribution shall terminate as of the date such litigation is filed. + +--- + +## 4. Copyright Retention + +You retain full copyright ownership of Your Contributions. This CLA grants a license as described above; it does not transfer or assign your copyright. + +--- + +## 5. Representations and Warranties + +By submitting a Contribution, You represent and warrant that: + +1. **You are the author.** You are the sole author of the Contribution, or you have the legal right to submit it on behalf of all co-authors. +2. **You have authority to grant this license.** The license grant in Sections 2 and 3 does not violate any agreement you have with a third party, and no third party has any claim over your Contribution that would restrict the grant. +3. **Third-party content is disclosed.** If Your Contribution includes any third-party code, data, or assets, you have identified them clearly in the pull request description, confirmed their licenses are compatible with the Project's current license, and included all required attribution. +4. **Employer authorization.** If you are employed and your Contribution relates to your employer's business or was created using employer resources, your employer has either (a) authorized you to make this Contribution and granted the necessary rights, or (b) signed a Corporate CLA (see Section 6) covering this Contribution. + +--- + +## 6. Corporate Contributors + +If you are submitting a Contribution on behalf of a company, organization, or other legal entity ("Organization"), the Organization must also sign a **Corporate CLA (CCLA)**. The CCLA covers all individuals authorized by the Organization to submit Contributions on its behalf. + +To submit a CCLA, open a GitHub Discussion in the **[openswmm.engine Discussions](https://github.com/HydroCouple/openswmm.engine/discussions)** tab with the title `[CCLA] ` and include: +- The legal name of the Organization. +- The name and title of the authorized signatory. +- A list of GitHub usernames authorized to submit Contributions under the CCLA. + +Tag [@cbuahin](https://github.com/cbuahin) in the discussion. The CCLA takes effect when acknowledged in writing by the Technical Manager. + +--- + +## 7. Moral Rights + +To the fullest extent permitted by applicable law, You waive and agree not to assert any moral rights you may have in Your Contributions against the Technical Manager or recipients of the software, including rights of integrity and rights of attribution beyond those provided in [AUTHORS.md](./AUTHORS.md). + +--- + +## 8. No Warranty + +Your Contributions are provided on an **"AS IS" basis**, without warranties or conditions of any kind, either express or implied, including, without limitation, any warranties or conditions of title, non-infringement, merchantability, or fitness for a particular purpose. + +--- + +## 9. How to Sign + +### Individual Contributors + +First-time contributors must sign this CLA before their pull request can be merged. Signing is done entirely through GitHub — no PDF or email is required. + +**Using CLA Assistant (recommended):** + +The project uses [CLA Assistant](https://cla-assistant.io) to automate CLA signing. When you open your first pull request, a bot will post a comment asking you to sign. Click the link in that comment and authenticate with your GitHub account to record your agreement. + +Alternatively, you may sign manually by posting the following comment on your pull request: + +> I have read the CLA Document and I hereby sign the CLA. + +Your GitHub username and the date of the comment serve as your electronic signature and are recorded permanently in the pull request history. + +### Returning Contributors + +Once you have signed the CLA, it covers all future Contributions to this project. You do not need to sign again for subsequent pull requests. + +--- + +## 10. Applicability to Prior Contributions + +If you have previously submitted Contributions to this project before this CLA was established, those Contributions are deemed covered by this CLA as of the date you sign it, provided you have the authority to grant the licenses described herein for those prior Contributions. + +--- + +## 11. Governing Law + +This CLA shall be governed by and construed in accordance with the laws of the United States, without regard to its conflict of law provisions. + +--- + +## 12. Contact + +Questions about this CLA should be directed to the Technical Manager via GitHub: + +- **GitHub:** [@cbuahin](https://github.com/cbuahin) +- **Discussions:** [github.com/HydroCouple/openswmm.engine/discussions](https://github.com/HydroCouple/openswmm.engine/discussions) + +--- + +*This CLA is maintained by the Technical Manager of openswmm.engine (currently [@cbuahin](https://github.com/cbuahin)). Last updated: May 2026.* diff --git a/CMakeLists.txt b/CMakeLists.txt index 99af0d1cd..8cde44e03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,69 +1,428 @@ # -# CMakeLists.txt - CMake configuration file for swmm-solver +# CMakeLists.txt - CMake configuration file for OpenSWMM Engine # -# Created: July 11, 2019 -# Modified: Aug 16, 2022 +# Project: openswmm.engine +# Description: Open Storm Water Management Model Engine +# Version: 6.0.0-alpha.1 (refactored engine) / 6.0.0-beta.1 (legacy engine wrapper) # -# Author: Michael E. Tryby -# US EPA ORD/CESER +# This is the root CMake file for the openswmm.engine project which contains: +# - openswmm_legacy_engine : Original EPA SWMM 5.x C solver (preserved unmodified) +# - openswmm_legacy_output : Original EPA SWMM 5.x binary output reader +# - openswmm_engine : New refactored C++20 engine with plugin I/O +# - openswmm_plugin_sdk : Header-only plugin development kit +# +# Build system: CMake 3.21+ with vcpkg for dependency management +# Test framework: Google Test + Google Benchmark +# +# See docs/MASTER_IMPLEMENTATION_PLAN.md for the full refactoring roadmap. # +cmake_minimum_required(VERSION 3.21...4.2) -cmake_minimum_required (VERSION 3.13) - -if("${CMAKE_BINARY_DIR}" STREQUAL "${CMAKE_SOURCE_DIR}") - message(FATAL_ERROR "In-source builds are disabled.") +# Prevent in-source builds +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) + message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there.") endif() - -project(swmm-solver - VERSION 5.2.4 +# Modern project declaration with metadata +project( + openswmm + VERSION 6.0.0 + DESCRIPTION "Open Storm Water Management Model Engine" + HOMEPAGE_URL "https://hydrocouple.org/projects/openswmm" LANGUAGES C CXX ) -# Append local dir to module search path -list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) +# Pre-release label for the refactored engine (e.g. "alpha.1", "beta.1", "rc.1"). Leave empty for a final release. +set(OPENSWMM_PRERELEASE "alpha.2" CACHE STRING "Pre-release tag for the refactored engine (e.g. alpha.1, beta.1, rc.1). Empty for a final release.") + +# Pre-release label for the legacy engine wrapper. Advances to beta ahead of the refactored engine. +set(OPENSWMM_LEGACY_PRERELEASE "beta.1" CACHE STRING "Pre-release tag for the legacy engine wrapper (e.g. beta.1, rc.1). Empty for a final release.") + +if(OPENSWMM_PRERELEASE) + set(OPENSWMM_FULL_VERSION "${PROJECT_VERSION}-${OPENSWMM_PRERELEASE}") +else() + set(OPENSWMM_FULL_VERSION "${PROJECT_VERSION}") +endif() + +if(OPENSWMM_LEGACY_PRERELEASE) + set(OPENSWMM_LEGACY_FULL_VERSION "${PROJECT_VERSION}-${OPENSWMM_LEGACY_PRERELEASE}") +else() + set(OPENSWMM_LEGACY_FULL_VERSION "${PROJECT_VERSION}") +endif() + +message(STATUS "OpenSWMM Engine version (refactored): ${OPENSWMM_FULL_VERSION}") +message(STATUS "OpenSWMM Engine version (legacy): ${OPENSWMM_LEGACY_FULL_VERSION}") + + +# Modern build type handling +# Only default to Release when no build type was supplied (via preset, -D, or +# parent project). Using FORCE here unconditionally would clobber an explicit +# CMAKE_BUILD_TYPE=Debug from a preset and silently mix Debug CMAKE_C_FLAGS +# with the default Release -O3 -DNDEBUG flag set. +get_property(isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(NOT isMultiConfig) + if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type" FORCE) + endif() + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Release" "MinSizeRel" "RelWithDebInfo") +endif() -# Sets the position independent code property for all targets +# Essential global settings +set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_POSITION_INDEPENDENT_CODE ON) -# Sets default install prefix when cmakecache is initialized for first time -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/install CACHE PATH "..." FORCE) +# Modern feature options with namespacing +option(OPENSWMM_BUILD_TESTS "Build all tests (unit + regression). Convenience flag." OFF) +option(OPENSWMM_BUILD_UNIT_TESTS "Build unit tests only" OFF) +option(OPENSWMM_BUILD_REGRESSION_TESTS "Build regression tests only" OFF) +option(OPENSWMM_BUILD_BENCHMARKS "Build Google Benchmark performance tests" OFF) +option(OPENSWMM_INSTALL "Install openswmm libraries" ON) +option(OPENSWMM_BUILD_PYTHON "Build Python bindings" OFF) +option(OPENSWMM_WITH_GEOPACKAGE "Build GeoPackage I/O library (requires sqlite3)" ON) +option(OPENSWMM_BUILD_2D "Build optional 2D surface routing module (requires SUNDIALS)" ON) +option(OPENSWMM_WITH_HYPRE "Wire hypre BoomerAMG as a 2D-solver preconditioner (PRECONDITIONER=AMG). ON by default: the `hypre` vcpkg feature is in default-features, so a from-source build gets BoomerAMG (the 2D solver's default/most-robust preconditioner). hypre pulls BLAS/LAPACK (and, depending on the port, an MPI stack, called sequentially via MPI_COMM_SELF). To produce a hypre-free build, set this OFF *and* exclude the `hypre` vcpkg feature together (e.g. VCPKG_MANIFEST_NO_DEFAULT_FEATURES) — they must stay in sync or find_package(HYPRE REQUIRED) fails. Defines OPENSWMM_HAVE_HYPRE and links HYPRE::HYPRE." ON) +option(OPENSWMM_BUILD_GPU_PLUGIN "Build the GPU/Kokkos surface-solver plugin (standalone, dlopen()ed at runtime — never linked into the core). ON by default: a from-source build gets the Kokkos-OpenMP plugin and runtime `auto` uses it (the `gpu` vcpkg feature is in default-features, supplying Kokkos + sundials[kokkos]). The portable base build/wheel turns this OFF explicitly and excludes the `gpu` feature (VCPKG_MANIFEST_NO_DEFAULT_FEATURES) to stay Kokkos-free; pip users get OpenMP via the separate `openswmm-gpu-omp` companion wheel. Backend selected via OPENSWMM_GPU_BACKEND (default omp)." ON) + +# OPENSWMM_BUILD_TESTS is a convenience flag that enables both unit and regression +if(OPENSWMM_BUILD_TESTS) + set(OPENSWMM_BUILD_UNIT_TESTS ON CACHE BOOL "" FORCE) + set(OPENSWMM_BUILD_REGRESSION_TESTS ON CACHE BOOL "" FORCE) endif() +# Include essential modules +include(CMakePackageConfigHelpers) +include(GNUInstallDirs) +include(GenerateExportHeader) + +# ---- Runtime-dependency bundling ---------------------------------------- +# Regexes shared by every install(RUNTIME_DEPENDENCY_SET …) call to keep the +# OS-provided libraries out of the package (bundling libc / libGL.so.1 / +# opengl32.dll would break dispatch on the target machine and is forbidden +# by Apple's bundle rules). Anything NOT matched here — SUNDIALS, HDF5, +# libomp, sqlite3, and any vcpkg-provided OpenGL-stack wrappers (GLEW, +# GLFW, ANGLE, glbinding) — will be copied next to the executable. +set(OPENSWMM_RUNTIME_DEP_PRE_EXCLUDES + # Windows OS contract DLLs (MSVC redists not in this list — we bundle them) + "api-ms-.*" "ext-ms-.*" + # Windows system DLLs by basename. These get pulled in transitively + # by Win32 API surface (shell, ACL UI, networking, theming, etc.) and + # must NOT be bundled — Windows always provides them. Critically, + # excluding them via POST is too late: one bundle pass copies them + # into the staging bin/ dir, the next pass then finds them in TWO + # places (system32 + staging/bin) and file(GET_RUNTIME_DEPENDENCIES) + # errors with "Multiple conflicting paths" before POST filters run. + # Add to this list when a new system DLL surfaces in CI warnings. + # NB1: must be ONE quoted string — CMake parses each "" as a separate + # list element, so splitting across lines breaks the alternation. + # NB2: char-class form is intentional. CMake regex is case-SENSITIVE, + # and PE import tables typically store system DLL names UPPERCASE + # (e.g. ACLUI.dll, KERNEL32.dll). A plain lowercase alternation + # silently fails to match — verified by cmake -P testing. + "^([Aa][Cc][Ll][Uu][Ii]|[Aa][Dd][Vv][Aa][Pp][Ii]32|[Bb][Cc][Rr][Yy][Pp][Tt]|[Bb][Cc][Rr][Yy][Pp][Tt][Pp][Rr][Ii][Mm][Ii][Tt][Ii][Vv][Ee][Ss]|[Cc][Oo][Mm][Bb][Aa][Ss][Ee]|[Cc][Oo][Mm][Cc][Tt][Ll]32|[Cc][Oo][Mm][Dd][Ll][Gg]32|[Cc][Rr][Yy][Pp][Tt]32|[Cc][Rr][Yy][Pp][Tt][Bb][Aa][Ss][Ee]|[Cc][Rr][Yy][Pp][Tt][Ss][Pp]|[Dd][Bb][Gg][Cc][Oo][Rr][Ee]|[Dd][Bb][Gg][Hh][Ee][Ll][Pp]|[Dd][Nn][Ss][Aa][Pp][Ii]|[Dd][Ww][Mm][Aa][Pp][Ii]|[Dd][Ww][Rr][Ii][Tt][Ee]|[Ff][Ww][Pp][Uu][Cc][Ll][Nn][Tt]|[Gg][Dd][Ii]32|[Gg][Dd][Ii]32[Ff][Uu][Ll][Ll]|[Ii][Mm][Aa][Gg][Ee][Hh][Ll][Pp]|[Ii][Mm][Mm]32|[Ii][Pp][Hh][Ll][Pp][Aa][Pp][Ii]|[Kk][Ee][Rr][Nn][Ee][Ll]32|[Kk][Ee][Rr][Nn][Ee][Ll][Bb][Aa][Ss][Ee]|[Mm][Pp][Rr]|[Mm][Ss][Aa][Ss][Nn]1|[Mm][Ss][Cc][Oo][Rr][Ee][Ee]|[Mm][Ss][Ii]|[Mm][Ss][Vv][Cc][Pp]_[Ww][Ii][Nn]|[Mm][Ss][Vv][Cc][Rr][Tt]|[Mm][Ss][Ww][Ss][Oo][Cc][Kk]|[Nn][Ee][Tt][Aa][Pp][Ii]32|[Nn][Ee][Tt][Uu][Tt][Ii][Ll][Ss]|[Nn][Oo][Rr][Mm][Aa][Ll][Ii][Zz]|[Nn][Tt][Dd][Ll][Ll]|[Nn][Tt][Mm][Aa][Rr][Tt][Aa]|[Oo][Ll][Ee]32|[Oo][Ll][Ee][Aa][Cc][Cc]|[Oo][Ll][Ee][Aa][Uu][Tt]32|[Pp][Oo][Ww][Rr][Pp][Rr][Oo][Ff]|[Pp][Rr][Oo][Ff][Aa][Pp][Ii]|[Pp][Rr][Oo][Pp][Ss][Yy][Ss]|[Pp][Ss][Aa][Pp][Ii]|[Rr][Pp][Cc][Rr][Tt]4|[Ss][Aa][Mm][Cc][Ll][Ii]|[Ss][Ee][Cc][Hh][Oo][Ss][Tt]|[Ss][Ee][Cc][Uu][Rr]32|[Ss][Ee][Tt][Uu][Pp][Aa][Pp][Ii]|[Ss][Ff][Cc]|[Ss][Ff][Cc]_[Oo][Ss]|[Ss][Hh][Cc][Oo][Rr][Ee]|[Ss][Hh][Ee][Ll][Ll]32|[Ss][Hh][Ll][Ww][Aa][Pp][Ii]|[Ss][Rr][Vv][Cc][Ll][Ii]|[Uu][Ss][Ee][Rr]32|[Uu][Ss][Ee][Rr][Ee][Nn][Vv]|[Uu][Ss][Pp]10|[Uu][Xx][Tt][Hh][Ee][Mm][Ee]|[Vv][Ee][Rr][Ss][Ii][Oo][Nn]|[Ww][Ee][Rr]|[Ww][Ii][Nn]32[Uu]|[Ww][Ii][Nn][Ii][Nn][Ee][Tt]|[Ww][Ii][Nn][Mm][Mm]|[Ww][Ii][Nn][Nn][Ss][Ii]|[Ww][Ii][Nn][Tt][Rr][Uu][Ss][Tt]|[Ww][Kk][Ss][Cc][Ll][Ii]|[Ww][Ll][Dd][Aa][Pp]32|[Ww][Ss]2_32|[Ww][Ss][Oo][Cc][Kk]32|[Ww][Tt][Ss][Aa][Pp][Ii]32|[Zz][Ll][Ii][Bb][Ww][Aa][Pp][Ii])\\.[Dd][Ll][Ll]" + # macOS / Linux system search paths + "^/usr/lib/.*" "^/lib/.*" "^/lib64/.*" + "^/System/Library/.*" "^/usr/lib/system/.*" + # Unix system libs by basename + "^libc\\..*" "^libm\\..*" "^libdl\\..*" + "^libpthread\\..*" "^librt\\..*" "^libutil\\..*" + "^libresolv\\..*" "^libnsl\\..*" + "^libstdc\\+\\+\\..*" "^libgcc_s\\..*" + # OpenGL loader / driver dispatch — must come from the OS, never bundled. + # vcpkg's GLEW/GLFW/ANGLE wrappers do NOT match these (different names). + # `$` end-anchor is dropped on purpose: CMake's variable-reference parser + # (CMP0010) treats stray `$` characters as bad references when these + # regex strings get interpolated into install(CODE "...") bodies. + "^libGL\\..*" "^libGLX\\..*" "^libEGL\\..*" "^libGLdispatch\\..*" + "^opengl32\\.dll" "^glu32\\.dll" +) + +set(OPENSWMM_RUNTIME_DEP_POST_EXCLUDES + ".*[/\\\\][Ss]ystem32[/\\\\].*\\.dll" + ".*[/\\\\][Ss]ys[Ww][Oo][Ww]64[/\\\\].*\\.dll" +) + +# Install runtime files for a list of imported targets into the given +# destination. Skips targets that are not defined in the current +# configuration, which lets callers list optional deps unconditionally. +# +# `IMPORTED_RUNTIME_ARTIFACTS` requires SHARED_LIBRARY / MODULE_LIBRARY / +# EXECUTABLE targets. We allowlist those explicitly and skip everything +# else — covers STATIC_LIBRARY (vcpkg static triplets), INTERFACE_LIBRARY +# (header-only deps), OBJECT_LIBRARY, and notably UNKNOWN_LIBRARY (the +# type reported by `FindSQLite3.cmake` and other classic Find modules +# that don't classify the artifact). Without this filter, configure +# aborts with "given target X which is not an executable, library, or +# module." +function(openswmm_install_runtime_deps DESTINATION) + foreach(_target IN LISTS ARGN) + if(NOT TARGET ${_target}) + continue() + endif() + get_target_property(_type ${_target} TYPE) + if(NOT (_type STREQUAL "SHARED_LIBRARY" OR + _type STREQUAL "MODULE_LIBRARY" OR + _type STREQUAL "EXECUTABLE")) + continue() + endif() + install(IMPORTED_RUNTIME_ARTIFACTS ${_target} + RUNTIME DESTINATION ${DESTINATION} + LIBRARY DESTINATION ${DESTINATION} + ) + endforeach() +endfunction() -# Define install locations (will be prepended by install prefix) -set(TOOL_DIST "bin") -set(INCLUDE_DIST "include") -set(LIBRARY_DIST "lib") -set(CONFIG_DIST "cmake") +# Bundle the full runtime-dependency closure of an installed executable into +# the given subdir of CMAKE_INSTALL_PREFIX. Runs at install time as an +# install(SCRIPT …) — configure_file produces a per-target script from +# cmake/BundleRuntimeDeps.cmake.in. The template approach avoids escape- +# hell with backslashes in the regex strings inside install(CODE "…"). +# +# Why a custom script instead of install(TARGETS … RUNTIME_DEPENDENCY_SET …): +# when openswmm_engine is itself installed by another install(TARGETS …) +# rule, CMake auto-adds the build-tree engine dylib to the set's +# POST_EXCLUDE_FILES_STRICT. That strict exclusion prunes the dep walk +# *through* the engine, so libomp / SUNDIALS / HDF5 — which are deps of +# the engine, not of the CLI — never appear in the resolved set. Probing +# the installed exe via the template sidesteps that auto-exclusion entirely. +function(openswmm_bundle_runtime_deps TARGET_NAME SUBDIR) + # Resolve the executable's installed file name at configure time so the + # template substitution baked into the script is a plain path, not a + # genex (configure_file doesn't evaluate $<…>). + get_target_property(_out_name ${TARGET_NAME} OUTPUT_NAME) + if(NOT _out_name) + set(_out_name "${TARGET_NAME}") + endif() + if(WIN32) + set(_file_name "${_out_name}.exe") + else() + set(_file_name "${_out_name}") + endif() + set(OPENSWMM_BUNDLE_TARGET_NAME "${TARGET_NAME}") + set(OPENSWMM_BUNDLE_TARGET_FILE_NAME "${_file_name}") + set(OPENSWMM_BUNDLE_SUBDIR "${SUBDIR}") + set(OPENSWMM_BUNDLE_PRE_EXCLUDES "[==[${OPENSWMM_RUNTIME_DEP_PRE_EXCLUDES}]==]") + set(OPENSWMM_BUNDLE_POST_EXCLUDES "[==[${OPENSWMM_RUNTIME_DEP_POST_EXCLUDES}]==]") -# Define build options -option(BUILD_TESTS "Builds component tests (requires Boost)" OFF) -option(BUILD_DEF "Builds library with def file interface" OFF) + # Extra DIRECTORIES that file(GET_RUNTIME_DEPENDENCIES) should search + # when resolving transitive dependencies of the executable. Critically + # this is where we point at vcpkg's installed//bin so SUNDIALS, + # HDF5, sqlite3, etc. (linked by openswmm_engine) get found and copied + # into the package. Without this, those DLLs show up in the bundle's + # unresolved-deps warnings on Windows and never get included in the + # cpack zip. Linux/macOS usually resolve them via rpath, but the extra + # dir is harmless on those platforms. + set(OPENSWMM_BUNDLE_EXTRA_DIRS "") + if(DEFINED VCPKG_INSTALLED_DIR AND DEFINED VCPKG_TARGET_TRIPLET) + list(APPEND OPENSWMM_BUNDLE_EXTRA_DIRS + "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin") + elseif(DEFINED ENV{VCPKG_ROOT}) + # Fallback: use the env-var-rooted classic-mode layout. + set(_triplet "${VCPKG_TARGET_TRIPLET}") + if(NOT _triplet AND WIN32) + set(_triplet "x64-windows") + endif() + if(_triplet) + list(APPEND OPENSWMM_BUNDLE_EXTRA_DIRS + "$ENV{VCPKG_ROOT}/installed/${_triplet}/bin") + endif() + endif() + # Use PROJECT_SOURCE_DIR (not CMAKE_SOURCE_DIR): the engine has its own + # project() at line 28, so this resolves to the engine root even when the + # engine is pulled in via add_subdirectory(..) from python/CMakeLists.txt. + # With CMAKE_SOURCE_DIR, the lookup would (incorrectly) start at the + # top-level CMakeLists — which under scikit-build-core is python/. + set(_script_in "${PROJECT_SOURCE_DIR}/cmake/BundleRuntimeDeps.cmake.in") + set(_script_out "${CMAKE_CURRENT_BINARY_DIR}/BundleRuntimeDeps-${TARGET_NAME}.cmake") + configure_file("${_script_in}" "${_script_out}" @ONLY) + install(SCRIPT "${_script_out}") +endfunction() -# Add project subdirectories -add_subdirectory(src/outfile) -add_subdirectory(src/solver) -add_subdirectory(src/run) +# ---- RPATH / runtime-library search-path policy ------------------------- +# Separate build-tree and install rpaths so developers can run binaries +# directly from the build dir without extra environment variables, while +# installed binaries locate their dylibs via relative paths. +# +# CMAKE_INSTALL_RPATH_USE_LINK_PATH appends every linked-library directory +# that is not an implicit system path to each target's install rpath +# automatically — this ensures editable Python installs and standalone +# cmake --install both find the engine dylibs without manual DYLD/LD setup. +# +# Per-target INSTALL_RPATH settings (wheel layout etc.) override the global +# CMAKE_INSTALL_RPATH value but are still AUGMENTED by USE_LINK_PATH. +# +# Windows has no rpath; DLL resolution uses PATH / the executable directory. +if(NOT WIN32) + set(CMAKE_SKIP_BUILD_RPATH FALSE) + set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + if(APPLE) + set(CMAKE_MACOSX_RPATH ON) + # Executables/libs that do NOT set their own INSTALL_RPATH use this. + # @loader_path — same directory as the binary (co-located dylibs) + # @loader_path/../lib — standard prefix layout (bin/ → ../lib/) + set(CMAKE_INSTALL_RPATH "@loader_path;@loader_path/../lib") + else() + set(CMAKE_INSTALL_RPATH "$ORIGIN;$ORIGIN/../lib") + endif() +endif() -if(BUILD_TESTS) +# Build information (improved) +find_package(Git QUIET) +if(Git_FOUND) + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE OPENSWMM_GIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + RESULT_VARIABLE GIT_RESULT + ) + if(NOT GIT_RESULT EQUAL 0) + set(OPENSWMM_GIT_HASH "unknown") + endif() +else() + set(OPENSWMM_GIT_HASH "unknown") +endif() + +# Create interface library for global headers +add_library(openswmm_common INTERFACE) +add_library(openswmm::common ALIAS openswmm_common) + +# Configure global version header +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/include/version.h.in + ${CMAKE_CURRENT_BINARY_DIR}/include/version.h +) + +# Set include directories for the common interface +target_include_directories(openswmm_common + INTERFACE + $ + $ + $ +) + +# Install generated headers +install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/include/version.h + DESTINATION + ${CMAKE_INSTALL_INCLUDEDIR}/openswmm +) + +# Install all public headers +install( + DIRECTORY include/openswmm + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" +) + +# Add testing subdirectories conditionally. +# Note: testing/benchmarks subdirs are added AFTER add_subdirectory(src) below +# so that imported third-party targets (OpenMP::OpenMP_CXX, SUNDIALS::cvode, +# hdf5::hdf5-shared, sqlite3) created during the engine configure are visible +# to the test-tree dependency-staging helper (openswmm_stage_test_runtime_deps). +# CMake permits forward target references in target_link_libraries, so this +# reorder is safe. +if(OPENSWMM_BUILD_UNIT_TESTS OR OPENSWMM_BUILD_REGRESSION_TESTS) enable_testing() - add_subdirectory(tests) + find_package(GTest CONFIG REQUIRED) endif() +# Modern package configuration +if(OPENSWMM_INSTALL OR OPENSWMMCORE_INSTALL) + configure_package_config_file( + cmake/OpenSWMMEngineConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/OpenSWMMEngineConfig.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/OpenSWMMEngine + PATH_VARS CMAKE_INSTALL_INCLUDEDIR CMAKE_INSTALL_LIBDIR + ) -# Create install rules for vcruntime.dll, msvcp.dll, vcomp.dll etc. -set(CMAKE_INSTALL_OPENMP_LIBRARIES TRUE) -include(InstallRequiredSystemLibraries) + write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/OpenSWMMEngineConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMinorVersion # ✅ More appropriate than AnyNewerVersion + ) + install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/OpenSWMMEngineConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/OpenSWMMEngineConfigVersion.cmake + DESTINATION + ${CMAKE_INSTALL_LIBDIR}/cmake/OpenSWMMEngine + ) -# Configure CPack driven installer package -set(CPACK_GENERATOR "ZIP") -set(CPACK_PACKAGE_VENDOR "US_EPA") -set(CPACK_ARCHIVE_FILE_NAME "swmm") + install( + EXPORT OpenSWMMEngineTargets + FILE OpenSWMMEngineTargets.cmake + NAMESPACE OpenSWMMEngine:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/OpenSWMMEngine + ) + install( + TARGETS openswmm_common + EXPORT OpenSWMMEngineTargets + ) +endif() + +# Add subdirectories +add_subdirectory(src) + +# Test / benchmark subdirs are added here (after src/) so the staging helper +# sees the imported runtime targets created during the engine configure. +if(OPENSWMM_BUILD_UNIT_TESTS OR OPENSWMM_BUILD_REGRESSION_TESTS) + add_subdirectory(tests) +endif() + +if(OPENSWMM_BUILD_BENCHMARKS) + find_package(benchmark CONFIG REQUIRED) + add_subdirectory(tests/benchmarks) +endif() + +# Optional Python bindings (Cython extensions compiled via scikit-build). +# Typically invoked indirectly by `pip install ./python`, but can also be +# enabled as part of a top-level CMake build: +# cmake -DOPENSWMM_BUILD_PYTHON=ON .. +if(OPENSWMM_BUILD_PYTHON) + # Discover Python and extension-building dependencies without making them + # hard requirements for a plain C/C++ build. If any of these packages are + # missing, the Python bindings will be skipped with a warning instead of + # causing CMake configuration to fail. + find_package(Python3 QUIET COMPONENTS Interpreter Development.Module) + find_package(PythonExtensions QUIET) + find_package(Cython QUIET) + + if(Python3_FOUND AND PythonExtensions_FOUND AND Cython_FOUND) + add_subdirectory(python/openswmm) + else() + message(WARNING + "OPENSWMM_BUILD_PYTHON is enabled, but required Python build " + "dependencies (Python3, PythonExtensions, Cython) were not found. " + "Skipping build of Python bindings." + ) + endif() +endif() + +# Simplified CPack. +# Generators: Windows ships a .zip; Unix ships BOTH .tar.gz (idiomatic) and .zip +# so the deployment workflow's `*.tar.gz` + `*.zip` upload globs both resolve. +set(CPACK_PACKAGE_NAME "openswmm-engine") +if(WIN32) + set(CPACK_GENERATOR "ZIP") +else() + set(CPACK_GENERATOR "TGZ;ZIP") +endif() +set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") +set(CPACK_PACKAGE_FILE_NAME "openswmm-engine-${OPENSWMM_FULL_VERSION}") +set(CPACK_PACKAGE_VENDOR "https://hydrocouple.org") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_DESCRIPTION}") include(CPack) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..b78448d5a --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,373 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "default-debug", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/debug", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "default", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "cacheVariables": { + } + }, + { + "name": "Windows", + "inherits": "default", + "description": "Windows Release (Ninja Multi-Config, MSVC). FP policy: /fp:precise (IEEE-754, no FMA contraction). C extensions /we4013 promote implicit-decl to error.", + "generator": "Ninja Multi-Config", + "binaryDir": "${sourceDir}/build/windows", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_FLAGS": "/fp:precise /W4 /we4013", + "CMAKE_CXX_FLAGS": "/fp:precise /W4", + "CMAKE_EXE_LINKER_FLAGS": "/LTCG /OPT:REF /OPT:ICF", + "CMAKE_SHARED_LINKER_FLAGS": "/LTCG /OPT:REF /OPT:ICF", + "CMAKE_MODULE_LINKER_FLAGS": "/LTCG /OPT:REF /OPT:ICF", + "CMAKE_EXPORT_COMPILE_COMMANDS": "YES", + "CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS": "OFF", + "CMAKE_C_FLAGS_RELEASE": "/O2 /GL /Gy /Gw", + "CMAKE_CXX_FLAGS_RELEASE": "/O2 /GL /Gy /Gw", + "CMAKE_C_FLAGS_DEBUG": "/Zi /Od /DDEBUG", + "CMAKE_CXX_FLAGS_DEBUG": "/Zi /Od /DDEBUG" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "Windows-debug", + "inherits": "default-debug", + "description": "Windows Debug (Ninja Multi-Config, MSVC). FP policy: /fp:precise (IEEE-754, no FMA contraction). C extensions /we4013 promote implicit-decl to error.", + "generator": "Ninja Multi-Config", + "binaryDir": "${sourceDir}/build/windows-debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_FLAGS": "/fp:precise /W4 /we4013", + "CMAKE_CXX_FLAGS": "/fp:precise /W4", + "CMAKE_EXE_LINKER_FLAGS": "/DEBUG", + "CMAKE_EXPORT_COMPILE_COMMANDS": "YES", + "CMAKE_C_FLAGS_DEBUG": "/Zi /Od /DDEBUG", + "CMAKE_CXX_FLAGS_DEBUG": "/Zi /Od /DDEBUG" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "Linux", + "inherits": "default", + "description": "Linux Release (Ninja). FP policy: -fno-fast-math -ffp-contract=off -fexcess-precision=standard (no FMA fusion, no excess precision). -fno-math-errno is safe perf (libm builtins, bit-identical math).", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/linux", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_FLAGS": "-Wall -Wextra -Werror=implicit-function-declaration -fno-fast-math -ffp-contract=off -fno-math-errno -fexcess-precision=standard -fvisibility=hidden", + "CMAKE_CXX_FLAGS": "-Wall -Wextra -fno-fast-math -ffp-contract=off -fno-math-errno -fexcess-precision=standard -fvisibility=hidden", + "CMAKE_EXE_LINKER_FLAGS": "-Wl,--gc-sections -flto", + "CMAKE_SHARED_LINKER_FLAGS": "-Wl,--gc-sections -flto", + "CMAKE_MODULE_LINKER_FLAGS": "-Wl,--gc-sections -flto", + "CMAKE_EXPORT_COMPILE_COMMANDS": "YES", + "CMAKE_C_FLAGS_RELEASE": "-O2 -flto -fdata-sections -ffunction-sections -fipa-icf", + "CMAKE_CXX_FLAGS_RELEASE": "-O2 -flto -fdata-sections -ffunction-sections -fipa-icf", + "CMAKE_C_FLAGS_DEBUG": "-O0 -g -DDEBUG", + "CMAKE_CXX_FLAGS_DEBUG": "-O0 -g -DDEBUG" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "Linux-debug", + "inherits": "default-debug", + "description": "Linux Debug (Ninja). Same FP policy as Linux preset (-fno-fast-math -ffp-contract=off -fexcess-precision=standard).", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/linux-debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_FLAGS": "-Wall -Wextra -Werror=implicit-function-declaration -fno-fast-math -ffp-contract=off -fno-math-errno -fexcess-precision=standard -fvisibility=hidden", + "CMAKE_CXX_FLAGS": "-Wall -Wextra -fno-fast-math -ffp-contract=off -fno-math-errno -fexcess-precision=standard -fvisibility=hidden", + "CMAKE_EXE_LINKER_FLAGS": "-g", + "CMAKE_EXPORT_COMPILE_COMMANDS": "YES", + "CMAKE_C_FLAGS_DEBUG": "-O0 -g -DDEBUG", + "CMAKE_CXX_FLAGS_DEBUG": "-O0 -g -DDEBUG" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "Darwin", + "inherits": "default", + "description": "macOS Release (Ninja, clang). FP policy: -fno-fast-math -ffp-contract=off (no FMA fusion). -fexcess-precision is omitted: clang ignores it on x86_64/arm64. -fno-math-errno matches Apple clang default. Arch is NOT pinned here: it defaults to the host arch, and CI passes -DCMAKE_OSX_ARCHITECTURES per matrix leg. Pinning universal2 in the preset previously conflicted with the per-arch CI override (vcpkg built universal deps while CMake built single-arch). Pass -DCMAKE_OSX_ARCHITECTURES=\"arm64;x86_64\" and a matching VCPKG_OSX_ARCHITECTURES for a local universal2 build.", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/darwin", + "environment": { + "CC": "clang", + "CXX": "clang++" + }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_FLAGS": "-Wall -Wextra -Werror=implicit-function-declaration -fno-fast-math -ffp-contract=off -fno-math-errno -fvisibility=hidden", + "CMAKE_CXX_FLAGS": "-Wall -Wextra -fno-fast-math -ffp-contract=off -fno-math-errno -fvisibility=hidden", + "CMAKE_EXE_LINKER_FLAGS": "-Wl, -flto", + "CMAKE_EXPORT_COMPILE_COMMANDS": "YES", + "CMAKE_C_FLAGS_RELEASE": "-O3 -flto -fdata-sections -ffunction-sections", + "CMAKE_CXX_FLAGS_RELEASE": "-O3 -flto -fdata-sections -ffunction-sections", + "CMAKE_SHARED_LINKER_FLAGS": "-flto -twolevel_namespace", + "CMAKE_MODULE_LINKER_FLAGS": "-flto", + "CMAKE_C_FLAGS_DEBUG": "-O0 -g -DDEBUG", + "CMAKE_CXX_FLAGS_DEBUG": "-O0 -g -DDEBUG" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "Darwin-debug", + "inherits": "default-debug", + "description": "macOS Debug (Ninja, clang). Same FP policy as Darwin preset (-fno-fast-math -ffp-contract=off -fno-math-errno).", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/darwin-debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_FLAGS": "-Wall -Wextra -Werror=implicit-function-declaration -fno-fast-math -ffp-contract=off -fno-math-errno -fvisibility=hidden", + "CMAKE_CXX_FLAGS": "-Wall -Wextra -fno-fast-math -ffp-contract=off -fno-math-errno -fvisibility=hidden", + "CMAKE_EXE_LINKER_FLAGS": "-g", + "CMAKE_EXPORT_COMPILE_COMMANDS": "YES", + "CMAKE_SHARED_LINKER_FLAGS": "-twolevel_namespace", + "CMAKE_C_FLAGS_DEBUG": "-O0 -g -DDEBUG", + "CMAKE_CXX_FLAGS_DEBUG": "-O0 -g -DDEBUG" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "Windows-cuda", + "inherits": "Windows", + "description": "Windows Release — Kokkos CUDA backend for NVIDIA RTX 2000 Ada (Ada Lovelace, SM 8.9 = ADA89). Requires CUDA Toolkit installed (nvcc on PATH) and MSVC. Disables hypre (needs a CUDA-enabled hypre build). GPU plugin output: openswmm_gpu_cuda.dll.", + "binaryDir": "${sourceDir}/build/windows-cuda", + "environment": { + "OPENSWMM_KOKKOS_CUDA_ARCH": "ADA89" + }, + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": "ON", + "VCPKG_MANIFEST_FEATURES": "2d;gpu-cuda", + "OPENSWMM_GPU_BACKEND": "cuda", + "OPENSWMMENGINE_KOKKOS_CUDA_ARCH": "ADA89", + "CMAKE_CUDA_ARCHITECTURES": "89", + "OPENSWMM_WITH_HYPRE": "OFF", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/install/Windows" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "Windows-cuda-debug", + "inherits": "Windows-debug", + "description": "Windows Debug — Kokkos CUDA backend for NVIDIA RTX 2000 Ada (Ada Lovelace, SM 8.9 = ADA89). Requires CUDA Toolkit installed (nvcc on PATH) and MSVC.", + "binaryDir": "${sourceDir}/build/windows-cuda-debug", + "environment": { + "OPENSWMM_KOKKOS_CUDA_ARCH": "ADA89" + }, + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": "ON", + "VCPKG_MANIFEST_FEATURES": "2d;gpu-cuda", + "OPENSWMM_GPU_BACKEND": "cuda", + "OPENSWMMENGINE_KOKKOS_CUDA_ARCH": "ADA89", + "CMAKE_CUDA_ARCHITECTURES": "89", + "OPENSWMM_WITH_HYPRE": "OFF", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/install/Windows" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "Windows-tests", + "inherits": "Windows-debug", + "description": "Windows debug build with unit + regression tests + geopackage", + "binaryDir": "${sourceDir}/build/windows-tests", + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": "tests;geopackage", + "OPENSWMM_BUILD_UNIT_TESTS": "ON", + "OPENSWMM_BUILD_REGRESSION_TESTS": "ON", + "OPENSWMM_WITH_GEOPACKAGE": "ON" + } + }, + { + "name": "Linux-tests", + "inherits": "Linux-debug", + "description": "Linux debug build with unit + regression tests + geopackage", + "binaryDir": "${sourceDir}/build/linux-tests", + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": "tests;geopackage", + "OPENSWMM_BUILD_UNIT_TESTS": "ON", + "OPENSWMM_BUILD_REGRESSION_TESTS": "ON", + "OPENSWMM_WITH_GEOPACKAGE": "ON" + } + }, + { + "name": "Darwin-tests", + "inherits": "Darwin-debug", + "description": "macOS debug build with unit + regression tests + geopackage", + "binaryDir": "${sourceDir}/build/darwin-tests", + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": "tests;geopackage", + "OPENSWMM_BUILD_UNIT_TESTS": "ON", + "OPENSWMM_BUILD_REGRESSION_TESTS": "ON", + "OPENSWMM_WITH_GEOPACKAGE": "ON" + } + }, + { + "name": "Windows-tests-release", + "inherits": "Windows", + "description": "Windows Release build with unit + regression tests + geopackage", + "binaryDir": "${sourceDir}/build/windows-tests-release", + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": "tests;geopackage", + "OPENSWMM_BUILD_UNIT_TESTS": "ON", + "OPENSWMM_BUILD_REGRESSION_TESTS": "ON", + "OPENSWMM_WITH_GEOPACKAGE": "ON" + } + }, + { + "name": "Linux-tests-release", + "inherits": "Linux", + "description": "Linux Release build with unit + regression tests + geopackage", + "binaryDir": "${sourceDir}/build/linux-tests-release", + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": "tests;geopackage", + "OPENSWMM_BUILD_UNIT_TESTS": "ON", + "OPENSWMM_BUILD_REGRESSION_TESTS": "ON", + "OPENSWMM_WITH_GEOPACKAGE": "ON" + } + }, + { + "name": "Darwin-tests-release", + "inherits": "Darwin", + "description": "macOS Release build with unit + regression tests + geopackage", + "binaryDir": "${sourceDir}/build/darwin-tests-release", + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": "tests;geopackage", + "OPENSWMM_BUILD_UNIT_TESTS": "ON", + "OPENSWMM_BUILD_REGRESSION_TESTS": "ON", + "OPENSWMM_WITH_GEOPACKAGE": "ON" + } + } + ], + "buildPresets": [ + { + "name": "default", + "hidden": true, + "configurePreset": "default" + }, + { + "name": "default-debug", + "hidden": true, + "configurePreset": "default-debug" + }, + { + "name": "Windows", + "configurePreset": "Windows", + "targets": [ + "package" + ] + }, + { + "name": "Windows-debug", + "description": "Build all targets for debugging on Windows", + "configurePreset": "Windows-debug" + }, + { + "name": "Linux", + "configurePreset": "Linux", + "targets": [ + "package" + ] + }, + { + "name": "Linux-debug", + "description": "Build all targets for debugging on Linux", + "configurePreset": "Linux-debug" + }, + { + "name": "Darwin", + "configurePreset": "Darwin", + "targets": [ + "package" + ] + }, + { + "name": "Darwin-debug", + "description": "Build all targets for debugging on macOS", + "configurePreset": "Darwin-debug" + }, + { + "name": "Windows-cuda", + "description": "Build the CUDA GPU plugin and install (Ada Lovelace ADA89)", + "configurePreset": "Windows-cuda", + "targets": ["package"] + }, + { + "name": "Windows-cuda-debug", + "description": "Build the CUDA GPU plugin in Debug mode (Ada Lovelace ADA89)", + "configurePreset": "Windows-cuda-debug" + }, + { + "name": "Windows-tests", + "description": "Build unit and regression tests on Windows", + "configurePreset": "Windows-tests" + }, + { + "name": "Linux-tests", + "description": "Build unit and regression tests on Linux", + "configurePreset": "Linux-tests" + }, + { + "name": "Darwin-tests", + "description": "Build unit and regression tests on macOS", + "configurePreset": "Darwin-tests" + }, + { + "name": "Windows-tests-release", + "description": "Build unit and regression tests in Release on Windows", + "configurePreset": "Windows-tests-release" + }, + { + "name": "Linux-tests-release", + "description": "Build unit and regression tests in Release on Linux", + "configurePreset": "Linux-tests-release" + }, + { + "name": "Darwin-tests-release", + "description": "Build unit and regression tests in Release on macOS", + "configurePreset": "Darwin-tests-release" + } + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..cf175ab8d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,479 @@ +# Contributing to openswmm.engine + +Thank you for your interest in contributing to **openswmm.engine**! This document outlines the governance model, contribution workflow, and standards that keep the project healthy, reproducible, and scientifically rigorous. Please read it carefully before opening an issue, submitting a pull request, or proposing a major change. + +--- + +## Table of Contents + +1. [Community Governance](#1-community-governance) +2. [Repository & Technical Management](#2-repository--technical-management) +3. [Succession & Delegation](#3-succession--delegation) +4. [Licensing, Intellectual Property & CLA](#4-licensing-intellectual-property--cla) +5. [Author Acknowledgment](#5-author-acknowledgment) +6. [How to Cite openswmm.engine](#6-how-to-cite-openswmm.engine) +7. [Versioning Strategy](#7-versioning-strategy) +8. [Deprecation & Backward Compatibility Policy](#8-deprecation--backward-compatibility-policy) +9. [Branching Model](#9-branching-model) +10. [Bug Fixes & Small Improvements](#10-bug-fixes--small-improvements) +11. [Pull Request Process](#11-pull-request-process) +12. [Continuous Integration & Automated Testing](#12-continuous-integration--automated-testing) +13. [Issue Reporting](#13-issue-reporting) +14. [Review Timeline Expectations](#14-review-timeline-expectations) +15. [Major Project Formulation Changes](#15-major-project-formulation-changes) +16. [Conflict of Interest in Reviews](#16-conflict-of-interest-in-reviews) +17. [Regression & Validation Testing](#17-regression--validation-testing) +18. [Documentation Standards](#18-documentation-standards) +19. [Dependency Evaluation Policy](#19-dependency-evaluation-policy) +20. [Security Vulnerability Reporting](#20-security-vulnerability-reporting) +21. [Roadmap & Prioritization](#21-roadmap--prioritization) +22. [Code of Conduct](#22-code-of-conduct) + +--- + +## 1. Community Governance + +openswmm.engine is a community-driven open source project. All contributors are welcome regardless of affiliation, background, or experience level. The community operates on a model of open discussion, merit-based evaluation, and transparent decision-making. Key decisions affecting the project direction are made openly on GitHub, and all community members are encouraged to participate. + +--- + +## 2. Repository & Technical Management + +**Repository and technical management is the responsibility of the Technical Manager (currently [@cbuahin](https://github.com/cbuahin)).** + +The Technical Manager is responsible for: + +- Maintaining the integrity and health of the `main`, `dev`, and `experimental` branches. +- Setting and enforcing coding standards, testing requirements, and documentation guidelines. +- Triaging issues and pull requests in a timely manner. +- Making final decisions on merges, releases, and branch management. +- Coordinating with the community on roadmap items and governance questions. + +Community members who wish to take on elevated responsibilities (e.g., becoming a recurring reviewer or branch maintainer) may express interest by opening a discussion on GitHub. + +--- + +## 3. Succession & Delegation + +The Technical Manager role is a single point of authority, which must be protected against absence or unavailability. + +- In the event the Technical Manager is unavailable for an extended period (e.g., travel, illness, or leave), they may designate a **Temporary Delegate** with equivalent merge and release authority. The delegation will be announced publicly in GitHub Discussions. +- If the Technical Manager steps down permanently, the outgoing manager will nominate a successor from the active contributor community. The nomination is subject to a community feedback period of at least two weeks before taking effect. +- In the absence of a named delegate and in urgent situations (e.g., a critical security fix), any two senior community reviewers may jointly approve and merge a patch to `main`, with a written rationale posted to GitHub Discussions immediately afterward. + +--- + +## 4. Licensing, Intellectual Property & CLA + +openswmm.engine is released under the **MIT License**. + +All contributors must sign the project **Contributor License Agreement (CLA)** before their pull request can be merged. The CLA is detailed in [CLA.md](./CLA.md). Key points: + +- **You retain your copyright.** The CLA grants a license; it does not transfer ownership. +- **Broad license grant.** You grant the Technical Manager a perpetual, irrevocable license to use, distribute, and **relicense** your contribution (e.g., for future dual open-source/commercial licensing). +- **Patent grant.** You grant a royalty-free patent license covering patents necessarily infringed by your contribution. +- **Representation of authority.** You confirm you have the right to submit the contribution and that it contains no unlicensed third-party material. +- **Corporate contributors** must additionally submit a Corporate CLA (CCLA) — see [CLA.md §6](./CLA.md#6-corporate-contributors). + +### Signing the CLA + +The project uses [CLA Assistant](https://cla-assistant.io). When you open your first pull request, a bot will post a comment with a signing link. You may also sign manually by posting the following comment on your PR: + +> I have read the CLA Document and I hereby sign the CLA. + +Once signed, the CLA covers all future contributions. You do not need to sign again. + +If your contribution includes third-party code or data, you are responsible for ensuring that the third-party license is compatible with MIT and that proper attribution is included in your submission. + +The full license text is available in [LICENSE](./LICENSE). + +--- + +## 5. Author Acknowledgment + +All contributors whose work is incorporated into openswmm.engine will be recognized in [AUTHORS.md](./AUTHORS.md). + +- **Name, affiliation (if any), and scope of contribution** will be documented for each contributor. +- The Technical Manager maintains `AUTHORS.md` and will update it at each release. +- If you believe your contribution has been omitted or mis-described, please open an issue or contact the Technical Manager directly. + +Contributors are encouraged to add themselves to `AUTHORS.md` as part of their pull request, subject to review and formatting consistency. + +--- + +## 6. How to Cite openswmm.engine + +If you use openswmm.engine in published research, teaching materials, or engineering reports, please cite the software to give appropriate credit and help others discover the project. + +### Recommended Citation Format + +Until a formal publication is available, please cite the software repository directly: + +``` +Buahin, C. (2026). openswmm.engine [Computer software]. GitHub. https://github.com/cbuahin_github/openswmm.engine +``` + +### DOI & Archival + +Each stable release of openswmm.engine is archived and assigned a **DOI via [Zenodo](https://zenodo.org)**. The DOI for each release is listed in the [GitHub Releases](../../releases) page and in the repository badge. When citing a specific version, use the version-specific DOI so that your reference is reproducible. + +### Publications + +If a peer-reviewed publication describing the software or a specific formulation becomes available, it will be listed here and should be preferred over the repository citation. Authors of accepted publications describing openswmm.engine contributions are encouraged to notify the Technical Manager so the reference can be added. + +--- + +## 7. Versioning Strategy + +openswmm.engine follows **Semantic Versioning (SemVer)** as defined at [semver.org](https://semver.org), with formal pre-release trajectories during testing periods. + +### Version Format + +``` +MAJOR.MINOR.PATCH[-PRERELEASE] +``` + +| Component | When to increment | +|-----------|-------------------| +| `MAJOR` | Incompatible API or formulation changes; fundamental engine restructuring | +| `MINOR` | New features or model components added in a backward-compatible manner | +| `PATCH` | Backward-compatible bug fixes, documentation updates, or performance improvements | + +### Pre-Release Trajectories + +During active release testing periods, the following pre-release labels are used in sequence: + +| Stage | Label example | Purpose | +|--------------------|-------------------|------------------------------------------------------------------------------| +| Alpha | `1.2.0-alpha.1` | Early feature-complete builds, internal or limited testing; may be unstable | +| Beta | `1.2.0-beta.1` | Feature-frozen; broader community testing; known issues may exist | +| Release Candidate | `1.2.0-rc.1` | Final candidate for release; only critical bug fixes accepted | +| Stable | `1.2.0` | Fully validated, production-ready release | + +Pre-release builds are tagged in Git and published to the appropriate distribution channel with clear labels to prevent unintended use in production workflows. + +--- + +## 8. Deprecation & Backward Compatibility Policy + +openswmm.engine is committed to giving users adequate notice before breaking changes are introduced. + +### Deprecation Process + +1. **Announcement** — A feature, API, or behavior to be removed is first marked as deprecated in the release notes and in code (via comments or compiler warnings, as applicable). The deprecation announcement will state the planned removal version. +2. **Minimum notice period** — Deprecated items will not be removed for at least **one full minor version cycle** following the deprecation announcement. For widely-used public APIs, a minimum of **one major version cycle** is preferred. +3. **Removal** — Deprecated items are removed in a `MAJOR` version bump, never in a `MINOR` or `PATCH` release. + +### Backward Compatibility Commitment + +- `PATCH` releases are guaranteed to be fully backward-compatible. +- `MINOR` releases are backward-compatible for all stable public APIs. +- `MAJOR` releases may introduce breaking changes, which will be documented in a migration guide published alongside the release. + +Experimental APIs (clearly marked as such) carry no backward compatibility guarantee and may change or be removed in any release. + +--- + +## 9. Branching Model + +| Branch | Purpose | +|---------------------------|--------------------------------------------------------------------------------------------| +| `main` | Stable, production-ready code. Only receives merges from release candidates. | +| `dev` | Integration branch for ongoing development. All feature and bug-fix branches target here. | +| `experimental/` | Sandboxed branches for exploratory or major formulation changes. See Section 15. | +| `bugfix/-desc` | Short-lived branches forked from `dev` to address specific bug reports. | +| `feature/` | Short-lived branches for minor feature additions, forked from `dev`. | +| `release/` | Release preparation branches (alpha → beta → rc) cut from `dev`. | + +--- + +## 10. Bug Fixes & Small Improvements + +For small, well-scoped bug fixes and minor improvements, follow this workflow: + +1. **Open or reference an issue.** Verify the bug is reproducible and link the relevant issue number in all subsequent commits and pull requests. +2. **Fork the `dev` branch.** Name your branch `bugfix/-short-description` (e.g., `bugfix/42-overflow-in-routing`). +3. **Write a failing unit test first.** The test must demonstrate the defect before the fix is applied. +4. **Implement the minimal fix.** Touch only the code necessary to resolve the issue. Do not refactor adjacent code. +5. **Ensure all regression tests pass.** If a regression test fails as a result of your change, you must either: + - Fix the regression, **or** + - Provide a written, technically justified explanation for why the regression failure is acceptable (included in the pull request description). +6. **Submit a pull request** against `dev` following the process described in Section 11. + +--- + +## 11. Pull Request Process + +All contributions enter the codebase through a pull request (PR). PRs must satisfy the following requirements before they can be merged: + +### Required Approvals + +Every PR requires **all three** of the following approvals: + +| Reviewer | Role | +|------------------------|--------------------------------------------------------------------------------------| +| **Technical Manager** | [@cbuahin](https://github.com/cbuahin) (current Technical Manager) — final authority on code quality, architecture, and correctness | +| **AI Copilot Review** | Automated AI-assisted review for style, logic, and common error patterns | +| **Community Reviewer** | At least one community contributor **other than the PR author** must approve | + +Self-approvals are not permitted. The PR author may not count toward the community reviewer requirement. + +### PR Checklist + +Before requesting review, confirm that your PR: + +- [ ] Targets the correct branch (`dev` for bug fixes and features; see Section 15 for experimental work) +- [ ] CLA signed — first-time contributors must sign the [CLA](./CLA.md) before the PR can be merged +- [ ] Includes a clear description of what was changed and why +- [ ] References any related issues (e.g., `Closes #42`) +- [ ] Includes new or updated unit tests covering the change +- [ ] All existing regression tests pass (or failures are documented and justified) +- [ ] CI pipeline passes (see Section 12) +- [ ] Documentation has been updated where applicable (Doxygen comments, README, etc.) +- [ ] `AUTHORS.md` entry has been added or updated if this is your first contribution + +### Merge Policy + +- Merges into `main` are performed exclusively by the Technical Manager at release time. +- Squash merging is preferred for `bugfix` and `feature` branches to keep the `dev` history clean. +- Merge commits are used when integrating `dev` into a `release` branch to preserve the full history. + +--- + +## 12. Continuous Integration & Automated Testing + +All pull requests are automatically tested via the project's CI pipeline (GitHub Actions) before human review begins. A failing CI pipeline will block merge regardless of approvals. + +The CI pipeline runs on every push to a PR branch and includes: + +| Stage | Description | +|----------------------------|-----------------------------------------------------------------------------------------------| +| **Build** | Compiles the engine and test suite across all supported platforms (Linux, macOS, Windows) | +| **Unit Tests** | Runs the full unit test suite; any failure blocks the PR | +| **Regression Tests** | Runs the analytical benchmark suite; deviations outside tolerance thresholds block the PR | +| **Static Analysis** | Runs a linter and static analyzer (e.g., `clang-tidy`) to catch common errors | +| **Documentation Build** | Verifies that Doxygen documentation builds without errors or warnings | + +Contributors are expected to run the test suite locally before opening a PR to minimize unnecessary CI failures. Instructions for running tests locally are in the [README](./README.md). + +--- + +## 13. Issue Reporting + +### Bug Reports + +Use the **Bug Report** issue template on GitHub. A complete bug report must include: + +- A clear, descriptive title. +- The version of openswmm.engine affected (e.g., `1.2.0-beta.1`). +- The operating system and compiler version. +- A minimal reproducible example — the smallest input file or code snippet that demonstrates the problem. +- Observed vs. expected behavior, including any relevant output or error messages. + +Incomplete bug reports may be closed or deprioritized pending clarification. + +### Feature Requests & Ideas + +Use the **Feature Request** issue template for small, well-scoped additions. For proposals that would change the core physical or mathematical formulation, use the GitHub Discussions **Ideas** section instead (see Section 15). + +### Issue Triage + +The Technical Manager or a designated reviewer will triage new issues within **two weeks** of submission, assigning labels (e.g., `bug`, `enhancement`, `needs-clarification`, `wontfix`) and priority. Contributors are encouraged to comment on existing issues before opening duplicates. + +--- + +## 14. Review Timeline Expectations + +openswmm.engine is maintained by volunteers. The following timelines are targets, not guarantees, but the Technical Manager is committed to keeping them: + +| Action | Target Timeline | +|-------------------------------------------|--------------------------| +| Initial issue triage | Within 2 weeks | +| First review response on a PR | Within 3 weeks | +| Follow-up review after requested changes | Within 2 weeks | +| Major formulation discussion response | Within 4 weeks | + +If you have not received a response within the stated window, you are welcome to post a polite follow-up comment on the issue or PR. PRs that become stale (no activity for 60 days) may be closed with a note that they can be reopened when the contributor is ready to continue. + +--- + +## 15. Major Project Formulation Changes + +Changes that affect the core physical or mathematical formulation of the engine — including new process models, numerical schemes, or significant algorithmic restructuring — follow a more rigorous governance process to protect the scientific integrity of the project. + +### Step 1 — Open a Community Discussion + +Start a new thread in the **Ideas** section of the GitHub Discussions tab. Your proposal should describe: + +- The scientific or engineering motivation for the change. +- The anticipated impact on existing functionality and results. +- Any known limitations, tradeoffs, or open questions. +- References to relevant literature or prior implementations. + +### Step 2 — Community Feedback + +Allow a minimum of **four weeks** for community members to respond. The Technical Manager will facilitate the discussion and summarize the consensus or key points of disagreement. A proposal may be: + +- **Accepted for prototyping** — community finds sufficient merit to proceed. +- **Revised** — further refinement is needed before prototyping. +- **Deferred** — not a current priority; may be revisited in a future release cycle. +- **Declined** — not aligned with the project scope or scientifically unsound. + +### Step 3 — Experimental Branch Implementation + +Accepted proposals are implemented in a dedicated `experimental/` branch. Requirements for this phase: + +- Rigorous **unit tests** covering the new formulation's behavior across the expected parameter space. +- **Regression tests** as described in Section 17. +- Code must compile cleanly and not break the existing test suite on the `dev` branch. +- Implementation notes and algorithm descriptions must be maintained in Doxygen-compatible documentation. + +### Step 4 — Peer Review (Strongly Encouraged) + +Where the improvement represents a meaningful scientific advance, contributors are strongly encouraged to submit a **peer-reviewed publication** describing the formulation, validation methodology, and results. Publication is not a hard requirement, but it significantly strengthens the case for incorporation into the core engine and signals readiness for production use. The Technical Manager can advise on suitable journals or conferences. + +### Step 5 — Regression & Validation Testing + +See Section 17 for full details. At minimum, the experimental implementation must demonstrate: + +- Correctness against problems with known analytical solutions. +- Agreement within acceptable tolerance with the legacy engine and previous versions on benchmark problems. +- No unacceptable degradation in computational performance or numerical stability. + +### Step 6 — Documentation & References + +Before a major formulation change can be merged into `dev`, the following documentation must be complete: + +- **Doxygen comments** on all new and modified public APIs, structs, and functions. +- **Inline citations** linking to peer-reviewed references where the formulation is derived from published work. +- Updated user-facing documentation (e.g., README sections, wiki pages) describing the new capability and how to invoke it. +- A changelog entry summarizing the change for the next release notes. + +--- + +## 16. Conflict of Interest in Reviewscon + +Scientific software reviews can intersect with contributors' professional interests. To protect the integrity of the review process: + +- A reviewer who has a competing or closely related implementation, active publication, or financial interest in the outcome of a review **must disclose this conflict** before participating in the review. +- A conflicted reviewer may participate in discussion but **may not serve as one of the three required approvers** for that PR. +- The Technical Manager makes the final determination on whether a disclosed conflict disqualifies a reviewer on a case-by-case basis. +- Undisclosed conflicts of interest, if discovered after a merge, may trigger a re-review of the affected contribution. + +--- + +## 17. Regression & Validation Testing + +Validation testing is a cornerstone of scientific software development. openswmm.engine uses two complementary regression testing modes: + +### Mode 1 — Analytical Benchmarks + +Test cases are constructed for which a closed-form or exact analytical solution exists. The engine output must agree with the analytical solution within a specified tolerance (defined per test case). These tests verify the mathematical correctness of the implementation independent of any reference implementation. + +Examples include: mass balance checks, steady-state flow solutions, unit hydrograph convolutions, and simplified routing scenarios with exact solutions. + +### Mode 2 — Legacy & Version Comparison Benchmarks + +A suite of reference problems is maintained that have been previously executed with: + +- The **legacy engine** (EPA SWMM or equivalent predecessor), and/or +- A **prior stable release** of openswmm.engine. + +New implementations must reproduce reference outputs within acceptable tolerance bounds. Deviations must be documented and scientifically justified — they may be acceptable if the new formulation is demonstrably more correct, but the deviation must be explicit and reviewed. + +### Benchmarking Criteria + +All benchmarks are evaluated across three dimensions: + +| Criterion | Description | +|--------------------------------|--------------------------------------------------------------------------------------------------| +| **Stability** | The solver must not exhibit divergence, oscillation, or blow-up under standard test conditions | +| **Conservativeness** | Mass, volume, or energy conservation must be maintained within defined tolerances | +| **Computational Performance** | Runtime must not degrade unacceptably relative to the previous version for equivalent problem sizes | + +Performance benchmarks are run on a reference hardware configuration documented in the test suite. Significant regressions in performance require justification and, where possible, mitigation. + +--- + +## 18. Documentation Standards + +openswmm.engine uses **Doxygen** for API documentation. All public-facing code elements (functions, classes, structs, enumerations, and their members) must include Doxygen-compatible documentation comments. + +Minimum documentation per element: + +- `@brief` — one-line summary. +- `@param` — description of each parameter including units where applicable. +- `@return` — description of the return value. +- `@note` / `@warning` — any important behavioral caveats. +- `@ref` or `@cite` — citation of the relevant literature where the implementation follows a published formulation. + +Documentation is reviewed as part of the pull request process. PRs with undocumented public APIs will not be approved. + +--- + +## 19. Dependency Evaluation Policy + +Adding a third-party dependency increases the maintenance burden and risk surface of the project. All proposed new dependencies must be evaluated against the following criteria before inclusion: + +| Criterion | Requirement | +|--------------------------|--------------------------------------------------------------------------------------------------| +| **License compatibility**| The dependency's license must be compatible with MIT (e.g., MIT, BSD, Apache 2.0). Copyleft licenses such as GPL are not permitted. | +| **Maintenance status** | The dependency must be actively maintained with a responsive upstream community. | +| **Stability** | The dependency must have a stable, versioned API. Unpinned or volatile dependencies are not acceptable. | +| **Binary footprint** | Dependencies that substantially increase compiled binary size must be justified by proportional benefit. | +| **Platform support** | The dependency must support all officially targeted platforms (Linux, macOS, Windows). | + +Proposals to add a new dependency should be raised in a GitHub Discussion or issue before implementation. Optional or experimental dependencies that are not required for the core build may be included under a feature flag, with lower scrutiny, provided they meet the license requirement. + +--- + +## 20. Security Vulnerability Reporting + +**Do not report security vulnerabilities through public GitHub issues.** + +If you discover a potential security vulnerability in openswmm.engine, please report it privately so that a fix can be prepared before the issue is publicly disclosed: + +- **GitHub Private Advisory:** Use the [Security Advisories](../../security/advisories) tab to submit a draft advisory directly on GitHub. Tag [@cbuahin](https://github.com/cbuahin) (current Technical Manager) in the advisory. + +Please include in your report: + +- A description of the vulnerability and its potential impact. +- Steps to reproduce, including any relevant input files or code. +- The version(s) of openswmm.engine affected. +- Any known mitigations or workarounds. + +The Technical Manager will acknowledge receipt within **five business days** and work with you on a coordinated disclosure timeline. Credit for responsibly disclosed vulnerabilities will be given in the release notes and security advisory, unless the reporter prefers to remain anonymous. + +--- + +## 21. Roadmap & Prioritization + +The project maintains a public roadmap that communicates the planned direction of openswmm.engine across upcoming releases. The roadmap is updated by the Technical Manager and is available in [ROADMAP.md](./ROADMAP.md). + +The roadmap includes: + +- Features and formulation improvements targeted for upcoming `MINOR` and `MAJOR` releases. +- Known issues and technical debt items scheduled for resolution. +- Research directions under exploration in `experimental` branches. +- Items that have been explicitly deferred or declined, along with the rationale. + +The roadmap is a living document and is updated at each release and after significant community discussions. Contributors are encouraged to align their proposals with roadmap priorities to maximize the likelihood of acceptance. Proposals that fall outside the current roadmap scope are still welcome and will be evaluated on merit, but may be deferred to a future cycle. + +Community members who wish to influence roadmap priorities should participate in the GitHub Discussions **Ideas** section. The Technical Manager reviews discussion threads when updating the roadmap and will reference specific threads in roadmap entries where community input shaped a decision. + +--- + +## 22. Code of Conduct + +openswmm.engine is committed to providing a welcoming, respectful, and inclusive environment for all contributors. All participants are expected to: + +- Engage constructively and professionally in all project spaces (issues, pull requests, discussions). +- Respect differing viewpoints and scientific perspectives. +- Accept constructive criticism of their contributions in good faith. +- Prioritize the long-term health of the project over individual preferences. + +Harassment, personal attacks, or exclusionary behavior of any kind will not be tolerated. Violations may be reported to the Technical Manager at @cbuahin. Reported incidents will be reviewed and addressed promptly and confidentially. + +--- + +*This document is maintained by the Technical Manager of openswmm.engine (currently [@cbuahin](https://github.com/cbuahin)). Last updated: May 2026.* diff --git a/LICENSE b/LICENSE index 16eeb7954..be942bae4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,14 @@ MIT License -Copyright (c) 2025 HydroCouple +Copyright 2026 Caleb Buahin -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This project contains original material has been released as part of various USEPA SWMM software over the years. These reside in the +public domain and cannot be claimed. This project also contains new material prepared by the United States +Government for which domestic copyright protection is not available under 17 +USC § 105. diff --git a/README.md b/README.md index cc5bcc571..95e935b0b 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,217 @@ -ORD Stormwater-Management-Model Solver -================================== +# OpenSWMM Engine -Stormwater Management Model (aka "SWMM") solver only +

+ OpenSWMM +

+**Open Storm Water Management Model — Next-Generation Computational Engine** -## Build Status -[![Build and Test](../../actions/workflows/build-and-test.yml/badge.svg)](../../actions/workflows/build-and-test.yml) +[![Unit Testing](https://github.com/HydroCouple/openswmm.engine/actions/workflows/unit_testing.yml/badge.svg)](https://github.com/HydroCouple/openswmm.engine/actions/workflows/unit_testing.yml) +[![Unit Testing Python](https://github.com/HydroCouple/openswmm.engine/actions/workflows/unit_testing_python.yml/badge.svg)](https://github.com/HydroCouple/openswmm.engine/actions/workflows/unit_testing_python.yml) +[![Documentation](https://github.com/HydroCouple/openswmm.engine/actions/workflows/documentation.yml/badge.svg)](https://github.com/HydroCouple/openswmm.engine/actions/workflows/documentation.yml) +[![CodeQL](https://github.com/HydroCouple/openswmm.engine/actions/workflows/codeql.yml/badge.svg)](https://github.com/HydroCouple/openswmm.engine/actions/workflows/codeql.yml) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/HydroCouple/openswmm.engine/badge)](https://securityscorecards.dev/viewer/?uri=github.com/HydroCouple/openswmm.engine) +[![Issues](https://img.shields.io/github/issues/HydroCouple/openswmm.engine)](https://github.com/HydroCouple/openswmm.engine/issues) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/HydroCouple/openswmm.engine/blob/HEAD/LICENSE) +[![PyPI](https://img.shields.io/pypi/v/openswmm.svg)](https://pypi.org/project/openswmm) +[![Downloads](https://pepy.tech/badge/openswmm)](https://pepy.tech/project/openswmm) +[![Python](https://img.shields.io/pypi/pyversions/openswmm.svg)](https://pypi.org/project/openswmm) +[![Wheel](https://img.shields.io/pypi/wheel/openswmm.svg)](https://pypi.org/project/openswmm) -## Disclaimer -The United States Environmental Protection Agency (EPA) GitHub project code is provided on an "as is" basis and the user assumes responsibility for its use. EPA has relinquished control of the information and no longer has responsibility to protect the integrity, confidentiality, or availability of the information. Any reference to specific commercial products, processes, or services by service mark, trademark, manufacturer, or otherwise, does not constitute or imply their endorsement, recommendation or favoring by EPA. The EPA seal and logo shall not be used in any manner to imply endorsement of any commercial product or activity by EPA or the United States Government. +## Documentation +| | Site | Contents | +|---|---|---| +| **C / C++ Engine** | **[hydrocouple.org/openswmm.engine](https://hydrocouple.org/openswmm.engine)** | Full C API reference, hydrology / hydraulics / water-quality reference manuals, user manual, architecture notes. | +| **Python Bindings** | **[hydrocouple.org/openswmm.engine/python](https://hydrocouple.org/openswmm.engine/python)** | Quickstart, per-domain user guide, Cython API reference, SWMM 5 → v6 migration. | -## Introduction -This is the official SWMM source code repository maintained by US EPA Office of Research and Development, Center For Environmental Solutions & Emergency Response, Water Infrastructure Division located in Cincinnati, Ohio. +Both sites cross-link from their top navigation. -SWMM is a dynamic hydrology-hydraulic water quality simulation model. It is used for single event or long-term (continuous) simulation of runoff quantity and quality from primarily urban areas. SWMM source code is written in the C Programming Language and released in the Public Domain. +--- +## Overview -## Find Out More -The source code distributed here is identical to the code found at the official [SWMM Website](http://www.epa.gov/water-research/storm-water-management-model-swmm). +OpenSWMM Engine is a community-driven, open-source continuation of the EPA Storm Water Management Model — a dynamic hydrology, hydraulic, and water-quality simulator for urban runoff. The project preserves the SWMM legacy under QA/QC and builds the community needed for long-term maintenance, working with ASCE/EWRI and the Water Environment Federation. + +## What's New in v6.0.0 + +### Architecture & Performance + +- **Data-Oriented Design** — Core state refactored to Structure-of-Arrays for cache efficiency and SIMD-friendly batches. +- **Reentrant Engine** — All simulation state lives behind an opaque `SWMM_Engine` handle; multiple independent simulations can run in the same process. +- **Plugin-Based I/O** — Output and report writing dispatch through plugin interfaces on a dedicated I/O thread. +- **C++20 Codebase** — Modern C++20 implementation; the legacy EPA SWMM 5.x solver is preserved unmodified in `src/legacy/`. + +### Process Formulation Enhancements + +#### Implemented + +- **Semi-Implicit Node Continuity** — Single-equation free-surface/surcharge formulation that removes the legacy two-branch discontinuity. Enabled via `NODE_CONTINUITY SEMI_IMPLICIT` (default). [Reference »](https://hydrocouple.org/openswmm.engine) +- **Anderson Acceleration for Picard Iteration** — Depth-2 mixing of residual history cuts iteration counts 25–50% on stiff surcharge transitions with safe fall-back to standard Picard. Enabled via `ANDERSON_ACCEL YES`. +- **Spatially Explicit Overland Flow & Groundwater (2D)** — 2D overland-flow grid coupled to the 1D pipe network. Surcharge re-routes over terrain to downstream nodes, lateral groundwater exchanges are tracked explicitly, and green-infrastructure placement is spatially resolved. +- **Dynamic Preissmann Slot** — Geometry-dependent slot width replaces the fixed-width slot at the free-surface / pressurized transition, improving stability for rapidly filling or draining conduits. +- **Physics-Based Initial Abstraction Recovery** — RDII initial abstraction now evolves as an exponential depletion/recovery process with additive base + thermal recovery rates and frozen-ground suppression. Seasonal RDII variation emerges from temperature dynamics on a single RTK set per sewershed — no monthly parameter tables required. Configured via the new `[RDII_DECAY]` input section. + + $$IA_{avail}(t+\Delta t) = IA_{max} - \bigl(IA_{max} - IA_{avail}(t)\bigr) \cdot e^{-k_{rec}(T)\,\Delta t}, \quad k_{rec}(T) = k_0 + k_T \cdot e^{\,\theta(T - T_{ref})}$$ + +#### In Development + +- **Spatially Explicit Inlets** — Promotes inlets to mode-switching junction nodes that capture street flow when gutter spread exceeds a threshold and revert to passive junctions otherwise. +- **LID as Storage Nodes** — Maps LID layers (surface, media, gravel) onto extended storage nodes using a reduced-physics kinematic Richards ODE for two-way hydraulic feedback. + +### New C API + +A domain-split C API replaces the monolithic legacy interface. Full reference at the [C engine documentation site](https://hydrocouple.org/openswmm.engine). + +| Header | Domain | +|---|---| +| `openswmm_engine.h` | Engine lifecycle, error codes, state machine | +| `openswmm_model.h` | Model building, validation, serialization, options | +| `openswmm_nodes.h` | Junctions, outfalls, storage, dividers | +| `openswmm_links.h` | Conduits, pumps, orifices, weirs, outlets | +| `openswmm_subcatchments.h` | Subcatchments, infiltration, coverage | +| `openswmm_gages.h` | Rain gages | +| `openswmm_pollutants.h` | Pollutant definitions and runtime injection | +| `openswmm_tables.h` | Time series, curves, patterns | +| `openswmm_inflows.h` | External inflows, DWF, RDII (incl. `[RDII_DECAY]`) | +| `openswmm_controls.h` | Control rules and direct link actions | +| `openswmm_infrastructure.h` | Transects, streets, inlets, LID controls | +| `openswmm_spatial.h` | CRS, coordinates, polylines, polygons | +| `openswmm_quality.h` | Landuse, buildup, washoff, treatment | +| `openswmm_massbalance.h` | Continuity errors and cumulative flux totals | +| `openswmm_callbacks.h` | Progress, warning, and step callbacks | +| `openswmm_hotstart.h` | Hot start file save / load / modify | +| `openswmm_statistics.h` | Node, link, and subcatchment statistics | +| `openswmm_geopackage.h` | Optional GeoPackage I/O | + +### Additional Features + +- **Hot Start API** — Save, load, modify, and query hot-start files through a stable C ABI. +- **CRS Support** — Coordinate reference systems specified in `[OPTIONS]`. +- **User Flags** — Typed `[USER_FLAGS]` / `[USER_FLAG_VALUES]` sections attach custom metadata (boolean, integer, real, string) to nodes, links, subcatchments, or gages. +- **Extension Options** — Unrecognized `[OPTIONS]` keys are preserved and exposed to plugins at runtime. +- **Plugin SDK** — Header-only SDK in `include/openswmm/plugin_sdk/` for input, output, and report plugins; the `IPluginComponentInfo` entry point advertises capabilities and supports custom `.inp` section handlers via `SectionRegistry`. +- **GeoPackage I/O** — Optional SQLite + spatial backing store for inputs, results, observed series, and topology in a single `.gpkg` file (`-DOPENSWMM_WITH_GEOPACKAGE=ON`). +- **HEC-22 Inlet Analysis** — Street inlet capture with grate and curb inlets (SWMM 5.2). +- **Variable Speed Pumps** — Type 5 pump curves with speed scaling. +- **New Storage Shapes** — Conical and pyramidal shapes with elliptical and rectangular bases. + +## Quick Start + +```bash +# C / C++ engine +git clone https://github.com/HydroCouple/openswmm.engine.git +cd openswmm.engine +git clone https://github.com/microsoft/vcpkg.git && ./vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$(pwd)/vcpkg + +cmake --preset= -B build -DOPENSWMM_WITH_GEOPACKAGE=ON +cmake --build build --config Release +ctest --test-dir build -C Release --output-on-failure + +# Python bindings (PyPI) +pip install openswmm +``` + +Presets: `Windows`, `Windows-debug`, `Linux`, `Linux-debug`, `Darwin`, `Darwin-debug`. Full build, test, and packaging instructions are in the [C engine docs](https://hydrocouple.org/openswmm.engine). + +```python +from openswmm.engine import Solver, Nodes, Links + +with Solver("model.inp", "model.rpt", "model.out") as s: + nodes, links = Nodes(s), Links(s) + while s.step(): + depth = nodes.get_depth("J1") + flow = links.get_flow("C1") +``` + +Forcing, model building, hot-start, bulk NumPy access, mass balance, and statistics are covered in the [Python docs](https://hydrocouple.org/openswmm.engine/python). + +## Glossary + +Brief definitions of the domain terms used throughout this README. Full treatment lives in the [reference manuals](https://hydrocouple.org/openswmm.engine). + +- **RDII** — Rainfall-Dependent Inflow & Infiltration. Stormwater that enters sanitary or combined sewers through cracks, joints, defective laterals, and roof / foundation drains during and after rainfall. +- **RTK** — The triplet `(R, T, K)` that parameterises a SWMM synthetic unit hydrograph for RDII: `R` is the long-term fraction of rainfall that becomes RDII, `T` is the time to peak (hours), and `K` is the ratio of base time to peak time. +- **IA (Initial Abstraction)** — Rainfall depth absorbed by the catchment before any RDII response begins (interception, surface storage, soil wetting). Recovers between events. +- **DWF** — Dry-Weather Flow. Base sanitary flow plus infiltration unrelated to rainfall, typically specified as an average value with diurnal / day-of-week / monthly patterns. +- **LID** — Low-Impact Development. Distributed green-infrastructure controls (bio-retention cells, permeable pavement, green roofs, infiltration trenches, rain gardens) that intercept, store, and infiltrate runoff at the source. +- **CRS** — Coordinate Reference System. The geodetic / projected coordinate frame (e.g. `EPSG:4326`) the model's spatial data is expressed in. +- **CFS / CMS** — Cubic feet per second / cubic metres per second. The two flow-unit conventions exposed via `FLOW_UNITS`. +- **Dynamic Wave Routing** — Full Saint-Venant momentum solver for link flow, used for backwater, surcharge, and pressurized conditions. +- **Preissmann Slot** — A narrow virtual slot added to a closed conduit's cross-section so that pressurized flow can be solved with the same free-surface equations. The dynamic slot adjusts width with geometry to smooth the surface ↔ pressure transition. +- **Surcharge** — A pipe flowing full and under pressure (HGL above the crown), typically caused by downstream backwater or capacity exceedance. +- **Picard Iteration** — Fixed-point iteration used inside the dynamic-wave timestep to converge implicit node depths. Anderson Acceleration is a residual-history accelerator on top of Picard. +- **Hot Start** — A saved end-of-run state (depths, volumes, IA, snow, GW) that initialises a subsequent simulation, letting long runs be split into checkpoints or warm runs into operational forecasts. +- **HEC-22** — FHWA Hydraulic Engineering Circular No. 22, the design reference whose grate and curb-opening capture equations are used by the inlet-analysis module. +- **GeoPackage** — OGC standard for a SQLite-based, single-file container holding spatial features and tabular data with full CRS metadata. + +## Prerequisites + +| Requirement | Version | +|---|---| +| CMake | 3.21+ | +| C compiler | C17 (GCC 10+, Clang 12+, MSVC 19.29+) | +| C++ compiler | C++20 (GCC 10+, Clang 14+, MSVC 19.29+) | +| vcpkg | 2025.02.14 | +| Python | 3.9 – 3.13 (optional) | +| Ninja | recommended on Linux/macOS | + +## Project Structure + +``` +openswmm.engine/ +├── include/openswmm/ +│ ├── engine/ # New engine public C API headers +│ └── legacy/ # Legacy SWMM 5.x public headers +├── src/ +│ ├── engine/ # New C++20 engine implementation +│ │ ├── input/geopackage/ # Optional GeoPackage I/O +│ │ └── 2d/ # 2D overland-flow & groundwater coupling +│ ├── legacy/ # Original EPA SWMM 5.x solver and output reader +│ ├── plugin_sdk/ # Header-only plugin SDK +│ └── cli/ # Command-line interface +├── tests/ +│ ├── unit/legacy/ # Legacy solver & output tests +│ ├── unit/engine/ # New engine unit tests +│ ├── regression/ # New-vs-legacy regression tests +│ └── benchmarks/ # Performance benchmarks +├── python/ # Cython bindings (scikit-build) +├── docs/ # Doxygen config and technical manuals +└── .github/workflows/ # CI/CD pipelines +``` + +## Libraries Built + +| Target | Description | +|---|---| +| `openswmm_legacy_engine` | Original EPA SWMM 5.x solver (shared) | +| `openswmm_legacy_output` | Original SWMM binary output reader (shared) | +| `openswmm_engine` | New refactored C++20 engine (shared) | +| `openswmm_geopackage` | GeoPackage I/O (static, optional — requires SQLite3) | +| `openswmm_plugin_sdk` | Header-only plugin SDK (INTERFACE) | +| `openswmm_cli` | Command-line executable | + +## Contributing + +Contributions are welcome — bug reports, fixes, new features, docs, tests, and benchmarks. + +1. Read [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and the [Code of Conduct](CODE_OF_CONDUCT.md). +2. Fork the repo and create a feature branch. +3. Ensure C++ (`ctest`) and Python (`pytest`) tests pass. +4. Follow existing style and naming. +5. Open a PR against `develop`. + +### Contributor License Agreement + +First-time contributors must sign the project [CLA](CLA.md) before a pull request can be merged. The CLA grants the project a perpetual, royalty-free copyright and patent license to your contributions and preserves the project's ability to relicense in the future; **you retain full copyright ownership** of your work. + +Signing is automated through [CLA Assistant](https://cla-assistant.io) — when you open your first PR, a bot comments with a one-click sign-in link. The CLA covers all subsequent contributions, so you only sign once. Corporate contributors should additionally submit a CCLA per [CLA §6](CLA.md#6-corporate-contributors). + +## License + +MIT — see [LICENSE](LICENSE). Original EPA SWMM material is in the public domain under 17 USC § 105. + +## Acknowledgements + +OpenSWMM builds on the EPA Storm Water Management Model. See [docs/authors.md](docs/authors.md) for the full contributor list. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 000000000..e41305359 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,135 @@ +# OpenSWMMCore Roadmap + +This document describes the planned development direction for OpenSWMMCore. It is maintained by the Technical Manager, [Caleb Buahin](https://github.com/cbuahin), and updated at each release and after significant community discussions. + +Items are organized by theme rather than strict release targeting, as scientific software timelines depend heavily on validation rigor and community bandwidth. Release assignments will be updated as work matures through experimental branches. + +Community members wishing to influence priorities should participate in the [GitHub Discussions — Ideas](../../discussions) section. Threads that shape roadmap decisions are linked directly from the relevant entries below. + +--- + +## Status Key + +| Symbol | Meaning | +|--------|----------------------------------------------------------| +| 🔬 | Under exploration in an experimental branch | +| 🔧 | Actively in development | +| 📋 | Planned — accepted for future development | +| ⏸ | Deferred — not currently scheduled | +| ✅ | Completed — available in a stable release | + +--- + +## 1. Flow Routing + +### 1.1 Implicit 1D Flow Routing — Full Saint-Venant Equations 📋 + +**Motivation:** The current explicit solver for 1D flow routing imposes Courant-number-dependent time step constraints that can be prohibitively small for steep or rapidly varying flows. An implicit (or semi-implicit) formulation of the full Saint-Venant equations will remove this restriction, enabling stable simulation of subcritical, supercritical, and mixed-regime flows with significantly larger time steps. + +**Planned scope:** +- Implicit or Crank-Nicolson discretization of the continuity and momentum equations for pipe and channel flow. +- Handling of hydraulic jumps and transitions between flow regimes. +- Newton-Raphson or Picard iteration with convergence diagnostics. +- Backward compatibility with existing input formats; solver selection via a configuration flag. +- Regression testing against the explicit solver on standard benchmark cases and against analytical solutions for steady gradually-varied flow profiles. + +**Validation approach:** Comparison to known analytical solutions (e.g., steady uniform flow, backwater curves) and to published benchmark results from the hydraulic literature. + +--- + +## 2. Water Quality Transport + +### 2.1 Advection-Dispersion Model — Pipe Flow 🔬 + +**Motivation:** OpenSWMMCore currently supports simplified first-order water quality routing in pipes. A full advection-dispersion equation (ADE) solver will enable physically accurate simulation of constituent mixing and longitudinal dispersion in pressurized and open-channel conduits. + +**Numerical approach — Lagrangian formulation:** The ADE will be solved using a **Lagrangian (particle-tracking) method**. Parcels of water (and their constituent loads) are advected along the flow field using the velocity provided by the flow routing solver. Dispersion is applied as a superimposed Fickian random-walk step on the parcel positions at each time step. This approach is inherently free of numerical diffusion, eliminates the Courant-number stability constraint on the advection step, and conserves mass exactly at the parcel level — all of which are significant advantages over fixed-grid Eulerian schemes for transport in pipe networks with highly variable velocities and geometries. + +**Planned scope:** +- Lagrangian parcel-tracking advection for 1D pipe and conduit flow, driven by velocity fields from the flow routing solver. +- Fickian random-walk dispersion superimposed on parcel trajectories using user-specified or empirically estimated longitudinal dispersion coefficients. +- Parcel injection, merging, and splitting logic to maintain solution resolution while controlling computational cost. +- Mass-conservative interpolation of parcel concentrations onto the fixed computational grid for output and coupling. +- Coupling to the multispecies reaction module (Section 2.4) for reaction evaluation carried along parcel trajectories. +- Verification of mass conservation and comparison to analytical solutions for simple pipe transport problems. + +### 2.2 Advection-Dispersion Model — Overland Flow 🔬 + +**Motivation:** Surface runoff carries dissolved and particulate constituents across the land surface. A 2D or quasi-2D ADE formulation for overland flow will extend water quality modeling to the catchment scale. + +**Planned scope:** +- 2D depth-averaged ADE for overland flow domains. +- Coupling to the surface runoff and infiltration modules for flow field and source term input. +- Boundary conditions for rainfall-driven constituent loading and inlet/outlet fluxes. + +### 2.3 Advection-Dispersion Model — Groundwater Flow 📋 + +**Motivation:** Subsurface transport of dissolved constituents is relevant to infiltration-based stormwater controls, groundwater recharge, and contaminant fate. An ADE formulation for the groundwater module will complete the full subsurface-surface-pipe transport chain. + +**Planned scope:** +- 1D or 2D ADE for the saturated zone, coupled to the existing groundwater module. +- Dispersion tensor formulation accounting for mechanical dispersion and molecular diffusion. +- Coupling to the surface and pipe water quality modules at shared boundaries. + +### 2.4 Multispecies Reaction Support — All Flow Domains 🔬 + +**Motivation:** Real-world water quality problems involve interacting chemical and biological species (e.g., nitrogen cycling, dissolved oxygen–BOD interactions, pathogen decay). A general multispecies reaction framework will allow users to define arbitrary reaction networks without modifying source code. + +**Planned scope:** +- A general reaction network specification (user-defined stoichiometry, rate laws, and kinetic parameters) applicable to pipe, overland, and groundwater transport. +- Built-in implementations of common reaction sets: nitrification/denitrification, DO-BOD, first-order decay. +- Operator-splitting approach separating transport and reaction steps for numerical stability. +- Validation against published multispecies benchmark problems and analytical solutions for simple reaction networks. +- Applicable across all ADE-enabled flow domains (Sections 2.1–2.3) with a unified species definition interface. + +--- + +## 3. Sediment Transport 📋 + +**Motivation:** Sediment is a primary pollutant and transport vehicle for nutrients, metals, and pathogens in urban stormwater systems. A sediment transport module will enable simulation of erosion, deposition, and sediment routing through catchments, channels, and pipe networks. + +**Planned scope:** +- Overland erosion: USLE/RUSLE-based or physically based detachment models driven by rainfall and surface runoff shear stress. +- Sediment routing in channels and pipes: bedload and suspended load transport using empirical (e.g., Engelund-Hansen, Yang) or process-based formulations. +- Particle size fractionation: multi-fraction sediment transport supporting cohesive and non-cohesive particles. +- Deposition modeling in detention basins, retention ponds, and low-velocity pipe reaches. +- Coupling to the water quality transport module for sediment-associated constituent transport (e.g., particle-bound phosphorus). +- Regression testing against published flume data and field-validated benchmark cases. + +--- + +## 4. Heat Transport 📋 + +**Motivation:** Thermal loading from urban surfaces, impervious cover, and stormwater infrastructure is a significant stressor on receiving water bodies. A heat transport module will enable simulation of stormwater temperature dynamics from catchment to receiving water. + +**Planned scope:** +- Surface energy balance model for catchment-scale water temperature estimation, accounting for solar radiation, long-wave exchange, evaporation, and conduction. +- 1D longitudinal heat transport in pipes and channels, coupled to the ADE solver (Section 2.1). +- Coupling to the groundwater module for subsurface heat exchange and baseflow temperature. +- Thermal stratification in detention basins (simplified layer model). +- Validation against field data from monitored urban catchments and published benchmark cases for stream temperature modeling. + +--- + +## 5. Deferred Items + +The following items have been raised in community discussions but are not currently scheduled. They may be reconsidered in future release cycles as resources permit or as community interest grows. + +| Item | Reason for Deferral | +|------------------------------------------------|----------------------------------------------------------------------------| +| 2D overland flow (full shallow water equations) | High implementation complexity; currently scoped to 1D/quasi-2D approaches | +| Real-time data assimilation & sensor fusion | Requires external telemetry infrastructure not yet in scope | +| GPU-accelerated solvers | Dependency and portability considerations; deferred pending solver maturity | +| Machine learning surrogate models | Research area; may be introduced as an optional experimental module | + +--- + +## 6. Completed Items + +| Item | Version | +|-----------------------------------------------|---------| +| *(To be populated as stable releases ship)* | | + +--- + +*Last updated: April 2026 — Caleb Buahin, Technical Manager* diff --git a/Readme.txt b/Readme.txt deleted file mode 100644 index 7bbb52e66..000000000 --- a/Readme.txt +++ /dev/null @@ -1,47 +0,0 @@ -CONTENTS OF SWMM522_ENGINE.ZIP -============================== - -The 'src' folder of this archive contains the C source code for -version 5.2.2 of the Storm Water Management Model's computational -engine. Consult the included 'Roadmap.txt' file for an overview of -the various code modules. The code can be compiled into both a shared -object library and a command line executable. Under Windows, the -library file (swmm5.dll) is used to power SWMM's graphical user -interface. - -The 'CMakeLists.txt' file is a script used by CMake (https://cmake.org/) -to build the SWMM 5.2 binaries. CMake is a cross-platform build tool -that generates platform native build systems for many compilers. To -check if the required version is installed on your system, enter - - cmake --version - -from a console window and check that the version is 3.5 or higher. - -To build the SWMM 5.2 engine library and its command line executable -using CMake and the Microsoft Visual Studio C compiler on Windows: - -1. Open a console window and navigate to the directory where this - Readme file resides (which should have 'src' as a sub-directory - underneath it). - -2. Issue the following commands: - mkdir build - cd build - -3. Then enter the following CMake commands: - cmake -G .. -A - cmake --build . --config Release - -where is the name of the Visual Studio compiler being used -in double quotes (e.g., "Visual Studio 15 2017" or "Visual Studio 16 2019") -and is Win32 for a 32-bit build or x64 for a 64-bit build. -The resulting engine DLL (swmm5.dll) and command line executable -(runswmm.exe) will appear in the build\Release directory. - -For other platforms, such as Linux or MacOS, Step 3 can be replaced with: - cmake .. - cmake --build . - -The resulting shared object library (libswmm5.so) and command line executable -(runswmm) will appear in the build directory. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..c6d411211 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,91 @@ +# Security Policy + +The OpenSWMM Engine maintainers take security seriously. This document +describes how to report a vulnerability and what to expect once a report +has been received. + +## Supported Versions + +Security fixes target the latest **6.x** release line and the active +`develop` branch. The legacy SWMM 5.x C solver carried under +`src/legacy/` is preserved byte-for-byte as a regression baseline; bugs +discovered in that tree are tracked but fixed upstream at EPA whenever +possible. + +| Version line | Supported | +| ---------------- | ------------------ | +| 6.x (current) | :white_check_mark: | +| 6.x pre-release | :white_check_mark: | +| Legacy SWMM 5.x | :x: (upstream EPA) | + +## Reporting a Vulnerability + +**Please do not file public GitHub issues for security problems.** + +Use one of the private channels below so a fix can be prepared before +public disclosure: + +1. **Preferred — GitHub private security advisory.** + Open a draft advisory in the + [Security tab](https://github.com/HydroCouple/openswmm.engine/security/advisories/new) + of this repository. The maintainers receive a notification immediately + and the advisory remains private until coordinated disclosure. + +Please include, where possible: + +- A clear description of the issue and the affected component + (e.g. `src/engine/...`, Python binding, CLI, plugin SDK). +- Affected version, commit SHA, or release tag. +- Reproduction steps, minimal input model (`.inp`) where applicable, + and the observed vs. expected behaviour. +- Any proof-of-concept exploit, payload, or crash artifact. +- Your assessment of impact (information disclosure, integrity, + availability, RCE) and which deployment scenarios are exposed. + +## What to Expect + +- **Acknowledgement** within **3 business days** of report. +- **Initial assessment** (confirmation, severity, scope) within + **10 business days**. +- **Fix or mitigation plan** communicated within **30 days** of + confirmation for high/critical issues; lower severities are scheduled + into the next maintenance release. +- **Coordinated disclosure**: a public advisory and patched release are + published together. Reporters are credited unless they request + otherwise. + +If a report does not receive a response within the acknowledgement +window, please re-send and CC a maintainer listed in +[`AUTHORS.md`](AUTHORS.md). + +## Scope + +In scope: + +- The refactored C++20 engine (`src/engine/`, `include/openswmm/engine/`) +- Plugin SDK and host-loaded plugin code paths +- Python bindings under `python/openswmm/` +- CLI executables (`src/cli/`, `src/legacy/cli/`) +- Build, packaging, and release tooling under `.github/`, + `CMakePresets.json`, `vcpkg.json` + +Out of scope (will be triaged but not necessarily fixed in this repo): + +- Vulnerabilities in `src/legacy/` that exist in upstream EPA SWMM 5.x + with no OpenSWMM-specific exposure — coordinated upstream. +- Issues in third-party dependencies pulled via vcpkg — please report + to the dependency's own security channel; we will pin to a fixed + version once available. +- Denial-of-service via deliberately malformed input models where the + engine fails safely (no memory corruption, no code execution). + +## Hardening Already in Place + +- Automated **CodeQL** analysis on every push and PR + (see [`.github/workflows/codeql.yml`](.github/workflows/codeql.yml)). +- **OpenSSF Scorecard** weekly supply-chain scan + (see [`.github/workflows/scorecard.yml`](.github/workflows/scorecard.yml)). +- Reproducible builds via pinned `vcpkg` baseline in + [`vcpkg.json`](vcpkg.json). +- All releases built from tagged commits in CI; no manual artifact + uploads. diff --git a/bindings/swmm5.def b/bindings/swmm5.def deleted file mode 100644 index 8bcaf6eb4..000000000 --- a/bindings/swmm5.def +++ /dev/null @@ -1,23 +0,0 @@ -LIBRARY SWMM5.DLL - -EXPORTS - swmm_close = _swmm_close@0 - swmm_decodeDate = _swmm_decodeDate@36 - swmm_end = _swmm_end@0 - swmm_getCount = _swmm_getCount@4 - swmm_getError = _swmm_getError@8 - swmm_getIndex = _swmm_getIndex@8 - swmm_getMassBalErr = _swmm_getMassBalErr@12 - swmm_getName = _swmm_getName@16 - swmm_getSavedValue = _swmm_getSavedValue@12 - swmm_getValue = _swmm_getValue@8 - swmm_getVersion = _swmm_getVersion@0 - swmm_getWarnings = _swmm_getWarnings@0 - swmm_open = _swmm_open@12 - swmm_report = _swmm_report@0 - swmm_run = _swmm_run@12 - swmm_setValue = _swmm_setValue@16 - swmm_start = _swmm_start@4 - swmm_step = _swmm_step@4 - swmm_stride = _swmm_stride@8 - swmm_writeLine = _swmm_writeLine@4 diff --git a/cmake/BundleRuntimeDeps.cmake.in b/cmake/BundleRuntimeDeps.cmake.in new file mode 100644 index 000000000..3d174ca6b --- /dev/null +++ b/cmake/BundleRuntimeDeps.cmake.in @@ -0,0 +1,141 @@ +# +# BundleRuntimeDeps.cmake.in +# +# Configured per-target by openswmm_bundle_runtime_deps() at top-level +# CMakeLists.txt. Runs at install time as part of the per-directory +# cmake_install.cmake script. Probes the already-installed executable +# (so RPATH rewrites from install_name_tool are in effect) and copies +# every shared library it depends on — except OS-provided ones filtered +# by the PRE/POST_EXCLUDE_REGEXES — next to the exe. +# +# @-substituted placeholders: see configure_file() call at the helper. +# + +cmake_policy(PUSH) +cmake_policy(SET CMP0011 NEW) # POLICY scope, regular variable scope rules +# NB: regexes contain '\.' and '\+' which CMake's quoted-string parser would +# otherwise flag under CMP0010 (bad variable reference syntax). Using a +# template file keeps the literal backslashes out of the install(CODE) +# interpolation path. + +set(_exe "${CMAKE_INSTALL_PREFIX}/@OPENSWMM_BUNDLE_SUBDIR@/@OPENSWMM_BUNDLE_TARGET_FILE_NAME@") +if(NOT EXISTS "${_exe}") + message(WARNING "openswmm_bundle_runtime_deps: ${_exe} does not exist yet — install ordering bug?") + cmake_policy(POP) + return() +endif() + +message(STATUS "Bundling runtime deps for @OPENSWMM_BUNDLE_TARGET_NAME@ into @OPENSWMM_BUNDLE_SUBDIR@") + +# Why the set + ${...} dance: +# The @-substituted value is wrapped as [==[a;b;c]==] (bracket-quoted to +# preserve literal backslashes in the regexes through configure_file). +# Passing [==[a;b;c]==] DIRECTLY to file(GET_RUNTIME_DEPENDENCIES ... +# PRE_EXCLUDE_REGEXES ...) passes it as ONE giant single regex that +# matches nothing — verified by direct testing of file(GET_RUNTIME_DEPENDENCIES). +# Capturing into a regular CMake variable first, then dereferencing with +# ${...}, triggers CMake's list-aware expansion that splits on `;` into +# separate arguments. Each regex is then applied individually as intended. +set(_pre_excludes @OPENSWMM_BUNDLE_PRE_EXCLUDES@) +set(_post_excludes @OPENSWMM_BUNDLE_POST_EXCLUDES@) +# Extra search paths captured at configure time — typically vcpkg's +# installed//bin where SUNDIALS, HDF5, etc. live. The list is +# `;`-separated; ${_extra_dirs} dereference splits it into separate args. +set(_extra_dirs "@OPENSWMM_BUNDLE_EXTRA_DIRS@") + +file(GET_RUNTIME_DEPENDENCIES + RESOLVED_DEPENDENCIES_VAR _resolved + UNRESOLVED_DEPENDENCIES_VAR _unresolved + EXECUTABLES "${_exe}" + DIRECTORIES + "${CMAKE_INSTALL_PREFIX}/@CMAKE_INSTALL_BINDIR@" + "${CMAKE_INSTALL_PREFIX}/@CMAKE_INSTALL_LIBDIR@" + ${_extra_dirs} + PRE_EXCLUDE_REGEXES ${_pre_excludes} + POST_EXCLUDE_REGEXES ${_post_excludes} +) + +file(REAL_PATH "${CMAKE_INSTALL_PREFIX}" _prefix_real) +set(_bundled_basename_to_oldpath "") # map of basename → original absolute path +foreach(_dep IN LISTS _resolved) + file(REAL_PATH "${_dep}" _dep_real) + # Skip anything that already lives under the install prefix — those + # files are installed by their own install(TARGETS …) rule and copying + # again would just duplicate them. + string(FIND "${_dep_real}" "${_prefix_real}/" _in_prefix) + if(_in_prefix EQUAL 0) + continue() + endif() + + if(_dep MATCHES "\\.framework/") + # Copy the whole .framework directory, not just the inner binary. + string(REGEX REPLACE "(.*\\.framework)/.*" "\\1" _fwk "${_dep}") + file(INSTALL DESTINATION "${CMAKE_INSTALL_PREFIX}/@OPENSWMM_BUNDLE_SUBDIR@" + TYPE DIRECTORY FILES "${_fwk}" USE_SOURCE_PERMISSIONS) + message(STATUS " bundled framework: ${_fwk}") + else() + file(INSTALL DESTINATION "${CMAKE_INSTALL_PREFIX}/@OPENSWMM_BUNDLE_SUBDIR@" + TYPE SHARED_LIBRARY FILES "${_dep}" FOLLOW_SYMLINK_CHAIN) + message(STATUS " bundled: ${_dep}") + get_filename_component(_bn "${_dep}" NAME) + list(APPEND _bundled_basename_to_oldpath "${_bn}=${_dep}") + endif() +endforeach() + +foreach(_dep IN LISTS _unresolved) + message(WARNING " unresolved runtime dep: ${_dep}") +endforeach() + +# ---- macOS install_name rewrite ----------------------------------------- +# On macOS, bundling a dylib doesn't help if consumers still reference its +# ORIGINAL absolute path (LC_LOAD_DYLIB). Each bundled lib gets its install +# name flipped to @rpath/, and every dylib / executable in the +# install tree gets its references to the original path rewritten the same +# way. The dyld loader then resolves @rpath via each binary's LC_RPATH set, +# which we've configured to include @loader_path / @loader_path/../bin / +# @loader_path/../lib (see project-wide RPATH config in src/engine). +if(APPLE) + find_program(_install_name_tool install_name_tool REQUIRED) + + # 1. Set each bundled lib's own install_name to @rpath/ so any + # binary that links it after rewriting picks the @rpath entry. + foreach(_entry IN LISTS _bundled_basename_to_oldpath) + string(REGEX REPLACE "^([^=]+)=.*$" "\\1" _bn "${_entry}") + set(_installed "${CMAKE_INSTALL_PREFIX}/@OPENSWMM_BUNDLE_SUBDIR@/${_bn}") + if(EXISTS "${_installed}") + execute_process( + COMMAND "${_install_name_tool}" -id "@rpath/${_bn}" "${_installed}" + RESULT_VARIABLE _rc + ) + if(NOT _rc EQUAL 0) + message(WARNING " install_name_tool -id failed on ${_installed}") + endif() + endif() + endforeach() + + # 2. Walk every Mach-O in the install tree and rewrite LC_LOAD_DYLIB + # references that still point at a bundled lib's original abs path. + file(GLOB_RECURSE _machos + "${CMAKE_INSTALL_PREFIX}/@CMAKE_INSTALL_BINDIR@/*" + "${CMAKE_INSTALL_PREFIX}/@CMAKE_INSTALL_LIBDIR@/*" + ) + foreach(_macho IN LISTS _machos) + if(IS_SYMLINK "${_macho}") + continue() + endif() + if(_macho MATCHES "\\.(a|h|hpp|cmake)$") + continue() + endif() + foreach(_entry IN LISTS _bundled_basename_to_oldpath) + string(REGEX REPLACE "^([^=]+)=(.*)$" "\\1" _bn "${_entry}") + string(REGEX REPLACE "^([^=]+)=(.*)$" "\\2" _oldpath "${_entry}") + execute_process( + COMMAND "${_install_name_tool}" -change "${_oldpath}" "@rpath/${_bn}" "${_macho}" + RESULT_VARIABLE _rc + ERROR_QUIET + ) + endforeach() + endforeach() +endif() + +cmake_policy(POP) diff --git a/cmake/FindOpenMP.cmake b/cmake/FindOpenMP.cmake new file mode 100644 index 000000000..606fa9058 --- /dev/null +++ b/cmake/FindOpenMP.cmake @@ -0,0 +1,64 @@ +include_guard(GLOBAL) + +# Apple-specific: try Homebrew locations. +# +# Gate on the imported TARGET, not the cached OpenMP_FOUND bool. CMake caches +# OpenMP_FOUND (we set it CACHE INTERNAL below) but does NOT persist imported +# targets across configure runs — they are recreated each run. So on any +# reconfigure of an existing build tree, OpenMP_FOUND is already TRUE while +# OpenMP::OpenMP_CXX does not yet exist; gating on `NOT OpenMP_FOUND` then +# skips this body and the target is never recreated, so every downstream +# `target_link_libraries(... OpenMP::OpenMP_CXX)` (and Kokkos' exported +# KokkosTargets link interface) fails with "target was not found". Gating on +# `NOT TARGET` recreates it whenever it is missing; the inner per-target +# `NOT TARGET` guards keep this idempotent if a target already exists. +if(APPLE AND NOT TARGET OpenMP::OpenMP_CXX) + message(STATUS "Searching for Homebrew OpenMP...") + + # Common Homebrew paths + set(HOMEBREW_PATHS + /opt/homebrew/opt/libomp # Apple Silicon + /usr/local/opt/libomp # Intel + ) + + foreach(HOMEBREW_PATH ${HOMEBREW_PATHS}) + if(EXISTS "${HOMEBREW_PATH}") + message(STATUS "Found Homebrew libomp at ${HOMEBREW_PATH}") + + # Create imported targets. GLOBAL so they outlive the directory + # scope of whichever CMakeLists.txt includes this finder first. + # include_guard(GLOBAL) above means the finder body runs exactly + # once (the legacy engine includes it before src/engine); without + # GLOBAL the targets would be visible only in that first directory + # subtree, so the src/engine consume site — and the transitive + # SUNDIALS -> Kokkos find_dependency(OpenMP REQUIRED) it triggers — + # would not see OpenMP::OpenMP_CXX and would fail to configure. + if(NOT TARGET OpenMP::OpenMP_C) + add_library(OpenMP::OpenMP_C INTERFACE IMPORTED GLOBAL) + target_compile_options(OpenMP::OpenMP_C INTERFACE -Xpreprocessor -fopenmp) + target_include_directories(OpenMP::OpenMP_C INTERFACE "${HOMEBREW_PATH}/include") + target_link_libraries(OpenMP::OpenMP_C INTERFACE "${HOMEBREW_PATH}/lib/libomp.dylib") + endif() + + if(NOT TARGET OpenMP::OpenMP_CXX) + add_library(OpenMP::OpenMP_CXX INTERFACE IMPORTED GLOBAL) + target_compile_options(OpenMP::OpenMP_CXX INTERFACE -Xpreprocessor -fopenmp) + target_include_directories(OpenMP::OpenMP_CXX INTERFACE "${HOMEBREW_PATH}/include") + target_link_libraries(OpenMP::OpenMP_CXX INTERFACE "${HOMEBREW_PATH}/lib/libomp.dylib") + endif() + + # Cache (INTERNAL) so the found-state is visible in every scope, not + # just the caller's. This both satisfies the if(OpenMP_FOUND) gate in + # the including CMakeLists and lets a later find_package(OpenMP) + # (e.g. KokkosConfig's find_dependency) report OpenMP as found after + # include_guard short-circuits the re-run of this module. + set(OpenMP_FOUND TRUE CACHE INTERNAL "OpenMP located via Homebrew libomp") + set(OpenMP_C_FOUND TRUE CACHE INTERNAL "OpenMP C located via Homebrew libomp") + set(OpenMP_CXX_FOUND TRUE CACHE INTERNAL "OpenMP CXX located via Homebrew libomp") + + return() + endif() + endforeach() + + message(WARNING "OpenMP not found. Install via: brew install libomp") +endif() \ No newline at end of file diff --git a/cmake/OpenSWMMEngineConfig.cmake.in b/cmake/OpenSWMMEngineConfig.cmake.in new file mode 100644 index 000000000..db2de0bb5 --- /dev/null +++ b/cmake/OpenSWMMEngineConfig.cmake.in @@ -0,0 +1,31 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +# Threads is required on all platforms: MSVC resolves it via the C runtime; +# GCC/Clang via -lpthread. Declaring it here ensures the exported Targets +# file never references an undefined CMake target in downstream consumers. +find_dependency(Threads) + +# Include the targets file +include("${CMAKE_CURRENT_LIST_DIR}/OpenSWMMEngineTargets.cmake") + +# Provide portable variables for consumers. +# On Windows a shared-library build produces both an import library (.lib → +# CMAKE_INSTALL_LIBDIR) and a DLL (.dll → CMAKE_INSTALL_BINDIR). Downstream +# projects needing the DLL at runtime should use OPENSWMMEngine_BIN_DIR; +# link-time consumers use OPENSWMMEngine_LIB_DIR. +set_and_check(OPENSWMMEngine_INCLUDE_DIR "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@") +set_and_check(OPENSWMMEngine_LIB_DIR "@PACKAGE_CMAKE_INSTALL_LIBDIR@") +# BIN_DIR is optional: not created when only libraries are installed (e.g. +# Python wheel builds that don't include the openswmm CLI executable). +set(OPENSWMMEngine_BIN_DIR "@PACKAGE_CMAKE_INSTALL_BINDIR@") + +# Set convenience variables +set(OPENSWMMEngine_INCLUDE_DIRS "${OPENSWMMEngine_INCLUDE_DIR}") +set(OPENSWMMEngine_LIBRARY_DIRS "${OPENSWMMEngine_LIB_DIR}") +set(OPENSWMMEngine_VERSION "@PROJECT_VERSION@") + +# Check required components +check_required_components(OPENSWMMEngine) + diff --git a/docs/1D_2D_COUPLING_CONFIGURATION.md b/docs/1D_2D_COUPLING_CONFIGURATION.md new file mode 100644 index 000000000..0ced2bdcf --- /dev/null +++ b/docs/1D_2D_COUPLING_CONFIGURATION.md @@ -0,0 +1,319 @@ +# 1D–2D Coupling Configuration + +How a 1D SWMM node is connected to the 2D overland mesh, what the **discharge +coefficient (`Cd`)** and **exchange `Area`** in the coupling map do, and how that +`Area` differs from a node's classic **ponded area (`Aponded`)**. + +> **Audience.** Part 1 is a conceptual guide for model builders. Part 2 is a +> reference with the exact equations and source `file:line` citations. +> +> See also: [`two_dimensional_model.md`](two_dimensional_model.md) (full `[2D_*]` +> INP grammar) and [`1D_2D_COUPLING_GATE_REVIEW.md`](1D_2D_COUPLING_GATE_REVIEW.md) +> (design review). + +--- + +# Part 1 — Concepts + +## 1. The big picture + +The 1D drainage network (pipes, junctions, outfalls) and the 2D overland surface +(a triangular mesh) are separate solvers that exchange water at **coupling +points** — specific mesh vertices or triangles tied to a SWMM node. You declare +those links in the INP; the engine builds the exchange automatically. + +```mermaid +flowchart LR + subgraph ONED["1D network (pipes, in feet internally)"] + J["Junction / inlet"] + O["Outfall"] + end + subgraph IFACE["Coupling interface"] + ORI["Orifice exchange
Q = Cd · A · sign(dh) · sqrt(2g·dh)"] + BC["Outfall tailwater
head boundary"] + end + subgraph TWOD["2D surface mesh (SI, metres)"] + CELLS["Triangle cells"] + end + J <-->|"surcharge spill / inlet capture"| ORI + ORI <--> CELLS + O -->|"pipe discharge"| BC + BC --> CELLS + CELLS -->|"tailwater"| BC + + MAP["2D_VERTEX_NODE_MAP
2D_TRIANGLE_NODE_MAP
Node, Cd, Area"] -. configures .-> ORI + OPT["2D_OPTIONS
COUPLING_CD, DRY_DEPTH"] -. configures .-> IFACE +``` + +Two coupling families behave differently: + +- **Junctions / inlets** exchange through a **bidirectional orifice** (this doc's + focus). Surface water drains in; surcharge spills out. +- **Outfalls** couple through a **prescribed tailwater head** (the 2D water + surface becomes the outfall's downstream boundary) plus injection of the pipe's + discharge onto the mesh. See §6. + +## 2. Wiring a node to the mesh + +Add the node to one of the coupling-map sections: + +``` +[2D_VERTEX_NODE_MAP] +;;Vertex Node Cd Area +0 J1 0.65 1.0 + +[2D_TRIANGLE_NODE_MAP] +;;Triangle Node Cd Area +12 J2 0.65 0.5 +``` + +| Column | Meaning | Units | Default | +|--------|---------|-------|---------| +| Vertex / Triangle | Mesh vertex index/tag, or triangle index/tag, to couple | — | required | +| Node | SWMM node name the cell exchanges with | — | required | +| `Cd` | Orifice **discharge coefficient** of the connection | – | `0.65` (or `COUPLING_CD`) | +| `Area` | Effective **exchange (orifice throat) area** of the connection | m² (SI) | `1.0` | + +**Vertex** coupling shares the exchange across the ring of cells around that +vertex (the *stencil*); **triangle** coupling uses the single cell. A node may be +mapped to several vertices — each becomes its own coupling point. + +```mermaid +flowchart TB + subgraph V["Vertex coupling (node ↔ vertex stencil)"] + VN["Node J1"] --- VV(("vertex 0")) + VV --- T0["cell A"] + VV --- T1["cell B"] + VV --- T2["cell C"] + end + subgraph T["Triangle coupling (node ↔ one cell)"] + TN["Node J2"] --- TC["cell 12"] + end +``` + +## 3. What `Cd` and `Area` do + +When the connection carries flow, it is metered like an orifice: + +$$Q = C_d \cdot A_{\text{eff}} \cdot \operatorname{sign}(\Delta h) \cdot \sqrt{2g}\,\sqrt{|\Delta h|}$$ + +- **`Cd`** scales the flow linearly — the head-loss coefficient of the grate / + manhole connection. Lower `Cd` ⇒ a more throttled, lossier inlet. +- **`Area`** (`A_eff`) is the **hydraulic opening** the water passes through — the + inlet-grate / manhole-throat area. It scales the flow but **stores no water**. +- `Δh = h_2d − h_1d` is the head difference that drives the flow, gated by the + capped-pipe rim — see §4. + +`A_eff` widens automatically from the configured inlet area to a manhole opening +(`2 × Area`) once the water surcharges past the rim, modelling a popped cover. + +> **Picking values.** `Cd ≈ 0.6–0.65` is a typical sharp-edged orifice +> coefficient. Set `Area` to the physical inlet/grate open area (m²). They affect +> *how fast* water exchanges, never *how much* the surface stores. + +## 4. The capped-pipe exchange model + +A coupled junction is modelled as a **pipe sealed by a cover at the crown** — the +same depth at which the 1D dynamic-wave solver begins to surcharge (engages its +Preissmann slot): + +``` +z_top = invert + full_depth (= the crown; the surcharge / slot-engagement point) +``` + +Below the crown the pipe flows sub-full and there is **no exchange** across the +interface. The cover only connects the two domains once water **reaches the crown**; +above it the exchange is the **bidirectional** orifice on the head difference. + +```mermaid +flowchart TD + A{"max(h1d, h2d) vs crown"} + A -->|"below crown"| B["NO EXCHANGE
pipe flows sub-full
(no spilling, no capture)"] + A -->|"above crown"| C["ORIFICE EXCHANGE
bidirectional, driven by
head diff h2d − h1d"] + C -->|"pipe higher"| E["SPILL OUT onto the mesh"] + C -->|"surface higher"| F["DRAIN IN to the pipe"] +``` + +- **Capped (below the crown):** the pipe is sealed. It flows sub-full and neither + spills onto the surface nor captures surface water. This is the user's *"capped + pipe allowing pressurisation but no spilling"*. +- **Overtopped (at/above the crown):** once water reaches the crown the cover is + submerged and the orifice connects the domains. Flow is **bidirectional** on + `h2d − h1d` — spilling out when the pipe is higher, draining in when the surface + is higher (so when `h2d > h1d` with both above the crown, flow is still passed + *into* the 1D node). + +A C¹ Hermite ramp on `max(h1d, h2d)` across a 5 cm band above the crown opens the +gate without a flux or derivative jump (CVODE/BDF stability). + +```mermaid +flowchart TB + Z3["z_top = invert + full_depth ── crown / cover (gate opens here)"] + Z2["above the crown: 1D surcharge storage lives in the slot (sur_depth headroom)"] + Z1["invert"] + Z3 --- Z2 --- Z1 + Z3 -. "exchange only above the crown; A widens to manhole (2·Area)" .-> Z3 +``` + +> Elevation ladder: **invert → crown (`+full_depth`, the gate) → slot headroom +> (`+sur_depth` above the crown)**. The gate is tied to the **crown**, *not* +> `crown + sur_depth`: it opens exactly when the pipe surcharges, keeping the inlet +> consistent with the slot. `sur_depth` is left **free to size the 1D slot's +> storage headroom** above the crown (where the surcharge volume is stored by the +> dynamic-wave solver), and no longer shifts the exchange threshold. +> +> **Geometry consistency.** The crown should match the **2D mesh bed elevation** at +> the coupling node — that is where surface water meets the cover. For a culvert +> that daylights at grade, the crown coincides with the road surface. + +## 5. Exchange `Area` vs ponded area (`Aponded`) — the key distinction + +These two parameters are often confused. They are completely different. + +| | **Exchange `Area`** (coupling map) | **Ponded area `Aponded`** (`[JUNCTIONS]`) | +|---|---|---| +| Role | Orifice **throat** the water flows *through* | Surface **footprint** the water is *stored on* above the rim | +| Governs | Exchange **flow rate** (with `Cd`) | How fast the **HGL rises** above the crown (`dH = dV / Aponded`) | +| Stores water? | No | Yes (a flat pond on top of the node) | +| Units | m² (SI) | project area units (ft² / m²) | +| Where | `[2D_VERTEX_NODE_MAP]` / `[2D_TRIANGLE_NODE_MAP]` | `[JUNCTIONS]` column 6 | + +```mermaid +flowchart LR + SURF["Surface water column
(storage footprint = ponded area)"] + THROAT["Orifice throat
(opening = exchange Area, loss = Cd)"] + PIPE["Pipe HGL"] + SURF -->|"flows THROUGH the throat"| THROAT --> PIPE +``` + +### Auto-aligned ponded area for coupled nodes + +For an **uncoupled** junction, `Aponded` is a user-supplied flat pond that only +acts when `ALLOW_PONDING` is on. For a **2D-coupled** junction the surface storage +*is* the 2D mesh, so the engine manages `Aponded` for you: + +- It **overrides** any user `Aponded` on a coupled junction with the **footprint of + the surrounding 2D cells** (the median-dual area, `Σ incident cell area / 3`), + converted to internal units. +- It lets that node **pond above its crown regardless of the global + `ALLOW_PONDING`** flag, so the 1D HGL can rise above the crown and track the + overlying 2D water surface once the exchange gate opens at the crown (§4). This + is the storage that lets surcharge spill and inlet capture work; the 1D + Preissmann slot (sized by `sur_depth`) provides the additional pressurised + surcharge store inside the pipe. + +> **Why this matters.** Earlier the coupling *zeroed* `Aponded` on coupled nodes. +> That pinned the 1D head at the crown, so the node could neither store its +> surcharge nor track the 2D surface. Auto-sizing `Aponded` to the 2D footprint +> frees the HGL to rise above the crown and align with the 2D surface. + +> **Tradeoff (bounded).** Above the crown the 1D pond and the stencil 2D cells +> exchange and equilibrate, so they represent the same near-manhole surface and +> storage there is mildly double-counted. The median-dual share keeps that area +> small, and a real flood spreads onto the broader mesh (cells beyond the +> stencil), which stays single-counted. You do **not** set `Aponded` yourself on +> coupled nodes — a value +> in the INP is overridden (with a warning). + +## 6. Junctions vs outfalls + +| | **Junction / inlet** | **Outfall** | +|---|---|---| +| Exchange | Capped-pipe gated orifice (§4): no exchange below the crown, bidirectional above | Pipe discharge injected onto the mesh | +| 2D → 1D | Drain in once surface rises above the crown (`h2d > h1d`) | **Tailwater head BC**: the 2D surface sets the outfall stage (only when the cell is genuinely wet; a dry surface ⇒ free discharge) | +| `Aponded` | Auto-set to 2D footprint (HGL rises above the crown to track the 2D surface) | Not used (outfalls don't pond) | + +## 7. `[2D_OPTIONS]` coupling keys + +| Key | Meaning | Default | +|-----|---------|---------| +| `COUPLING_CD` | Default `Cd` when a map row omits it | `0.65` | +| `COUPLING_INTERVAL` | SWMM steps between exchanges (`0` = every step) | `0` | +| `DRY_DEPTH` | Wet/dry threshold (m); a cell shallower than this neither captures nor receives | `0.001` | + +## 8. Worked example + +``` +[OPTIONS] +FLOW_UNITS CMS +ALLOW_PONDING NO ; coupled nodes still pond — the engine handles it + +[JUNCTIONS] +;;Name Elev MaxDepth InitDepth SurDepth Aponded +J1 0.0 1.0 0 0 0 ; Aponded auto-set from 2D +``` +Here the exchange gate sits at the 1 m crown (set the 2D mesh bed there to ~1.0, +the inlet daylighting at grade). `SurDepth` does **not** move the gate; raise it +(e.g. `SurDepth 2.0`) to give the 1D Preissmann slot headroom to store surcharge +inside the pipe above the crown. +``` +[2D_OPTIONS] +DRY_DEPTH 0.002 +COUPLING_CD 0.7 + +[2D_VERTEX_NODE_MAP] +;;Vertex Node Cd Area +0 J1 0.7 1.0 ; 0.7 loss, 1 m² inlet throat +``` + +When rain ponds on the cells around vertex 0 and the surface rises above the crown +(= 1 m here), water is drained into `J1` through the orifice. If the pipe network +surcharges `J1`, the HGL rises above the crown and spills back onto those same +cells; with `SurDepth > 0` the slot stores part of that surcharge inside the pipe. + +--- + +# Part 2 — Reference + +All paths are under `src/engine/`. Heads/areas/flows on the **1D** side are in +internal **feet / ft² / cfs** for every project; the **2D** solver is **SI** +(m, m², m³/s, g = 9.80665). The coupling always converts feet⇄metres +([`SolverOptions2D.hpp`](../src/engine/2d/data/SolverOptions2D.hpp), `len_*_to_*`, +`flow_*_to_*`; `area = len²`). + +### Orifice flow and effective area +`2d/coupling/NodeCoupling.cpp` +- `orificeFlow` / `orificePhi` (≈30–44): `Q = Cd·A·sign(Δh)·√(2g)·φ(|Δh|)`, with + `φ` a C¹-regularized √ below a 2 cm head (bounded sensitivity at `Δh → 0`). +- `effectiveArea` (≈47–53): `A_inlet` (= map `Area`) below `z_top`, ramping to + `A_manhole = 2·Area` over a 5 cm band above it. + +### Capped-pipe junction exchange +`2d/coupling/NodeCoupling.cpp` `computeCouplingExchange` (≈218–388): +- `crown = (invert_elev + full_depth)·len_1d_to_2d`; `z_top = crown` (the gate is + tied to the crown — the slot-engagement point — *not* `crown + sur_depth`). +- Driver `Q = orificeFlow(h_2d − h_1d, Cd, A_eff)` — bidirectional head difference. +- **Capped-pipe gate** `capRamp` = Hermite smoothstep of `(max(h_1d,h_2d) − crown)` + over a 5 cm band: `Q *= capRamp`. Below the crown ⇒ `Q = 0` (pipe flows sub-full, + no spilling); above ⇒ full bidirectional exchange. `sur_depth` no longer shifts + the gate — it sizes the 1D slot's surcharge-storage headroom above the crown. +- Source-side wet/dry `wetRamp` on `DRY_DEPTH` suppresses dry-cell exchange and + kills spurious inflow from a dry low-spot vertex. +- Node-capacity and 2D-cell-volume caps throttle 2D→1D drainage conservatively. +- `scatterCouplingFlux` (≈55–121) distributes the exchange across the vertex + stencil weighted by upwind HGL slope (conservative; geometric fallback on flat + surfaces); a sink is drawn from the surrounding cells. + +### Coupling map parsing and options +`2d/input/SectionHandlers2D.cpp` — `[2D_VERTEX_NODE_MAP]` / `[2D_TRIANGLE_NODE_MAP]` +(`Cd` default 0.65, `Area` default 1.0 m²), `COUPLING_CD`, `COUPLING_INTERVAL`. +`2d/data/SolverOptions2D.hpp` — `coupling_cd`, `coupling_interval`, `dry_depth`. + +### Auto-aligned ponded area and the coupled-node pond exception +- `2d/SurfaceRouter2D.cpp` (≈264–320): overrides `nodes.ponded_area` on coupled + non-outfall nodes with the median-dual footprint (`Σ stencil tri_area / 3`, + converted by `len_2d_to_1d²`); sets `ctx.coupled_node[ni] = 1`. +- `hydraulics/DynamicWave.cpp`: `initNodeStates` (≈856) and `setNodeDepth` (≈2238) + treat a `coupled_node` as pond-capable regardless of `ALLOW_PONDING`, so the HGL + rises above the crown over `ponded_area` instead of being capped. + +### Ponded-area mechanics (for contrast) +- `data/NodeData.hpp` `ponded_area`; `hydraulics/Node.cpp` `getPondedArea` + (returns `ponded_area` only when flooded above the rim). +- Legacy `Aponded` is a flat pond that raises the head by `excess_volume / + ponded_area`; gated by the `[OPTIONS] ALLOW_PONDING` flag for uncoupled nodes. + +### Outfall coupling +`2d/coupling/NodeCoupling.cpp` `updateOutfallBoundaries` / `transferOutfallDischarges`; +`hydraulics/Outfall.cpp` `setAllOutfallDepths` (tailwater head BC with a wet/dry +ramp so a dry surface yields free discharge). diff --git a/docs/2dModelStrategy.md b/docs/2dModelStrategy.md new file mode 100644 index 000000000..5ce4c33a2 --- /dev/null +++ b/docs/2dModelStrategy.md @@ -0,0 +1,2962 @@ +# Two-Dimensional Surface Routing — Implementation Strategy + +## Overview + +This document details the implementation strategy for an optional 2D surface routing module coupled to the OpenSWMM engine. The module implements the **second-order accurate, semi-discrete finite volume formulation** from Kumar, Duffy, and Salvage (2009) — initially limited to the **diffusion-wave surface flow** component. The design follows the same data-oriented, cache-friendly, Structure-of-Arrays (SoA) patterns used throughout the engine. + +**Scope — Phase 1 (this document):** +- 2D diffusion-wave surface routing on a triangular mesh +- Coupling to SWMM nodes/junctions via orifice equation with + surcharge gate and dedicated `coupling_inflow[]` channel (see §6) +- Rainfall from system rain gages +- Time integration via SUNDIALS CVODE (BDF) with GMRES linear solver + +**Future phases (strategy outlined, not implemented):** +- Subsurface (Richards' equation) coupling +- Infiltration (Green-Ampt / Horton / SCS) +- Evapotranspiration, snowmelt +- Natural neighbour interpolation for rainfall spatial distribution + +--- + +## 1. Input File Sections + +The 2D model is specified via optional sections in the `.inp` file. All section names are prefixed with `2D_` for clarity and to avoid collisions with existing SWMM sections. The sections are registered via `SectionRegistry::register_custom()`. + +### 1.1 Section Names + +| Section | Status | Purpose | +|---------|--------|---------| +| `[2D_MESH_FILE]` | implemented | Reference an external file containing 2D mesh and configuration sections | +| `[2D_OPTIONS]` | implemented | Solver options, tolerances, time-stepping parameters, HDF5 output path | +| `[2D_VERTICES]` | implemented | Mesh vertex coordinates | +| `[2D_TRIANGLES]` | implemented | Triangle connectivity and surface roughness | +| `[2D_VERTEX_NODE_MAP]` | implemented | Vertex-to-SWMM-node coupling (with optional `Cd` / area) | +| `[2D_TRIANGLE_NODE_MAP]` | implemented | Triangle-centroid-to-SWMM-node coupling (with optional `Cd` / area) | +| `[2D_BOUNDARY_CONDITIONS]` | implemented (storage + parser) | Per-edge boundary type and parameter | +| `[2D_EDGE_CONVEYANCE]` | implemented | Per-edge `[0, 1]` multiplier on the diffusion-wave flux (§11A) | +| `[2D_INITIAL_CONDITIONS]` | *(future)* | Per-cell initial water depth | +| `[2D_INFILTRATION]` | *(future)* | Per-cell or per-zone infiltration params | + +Registration of all implemented sections happens through +`openswmm::twoD::register2DSections` (`src/engine/2d/input/SectionHandlers2D.cpp`), +called from `SWMMEngine::open` under `#ifdef OPENSWMM_HAS_2D`. + +> **Optional header comments.** Any line starting with `;;` is treated as +> a comment by the standard tokenizer. Two such comments are recognised +> by the engine's pre-scan (see §2A Unit Conversion Strategy): +> `;; UNITS: ` and `;; SOURCE_CRS: `. They are not +> required, but producers (GUI, hand-edited files) should emit them so +> the file is self-describing and the engine can correctly handle +> SI-on-disk meshes. + +### 1.2 `[2D_MESH_FILE]` + +An optional section that redirects the engine to read all 2D mesh and configuration sections from a separate file instead of (or in addition to) the main `.inp`. + +``` +[2D_MESH_FILE] +FILE +``` + +| Token | Type | Description | +|-------|------|-------------| +| `FILE` | keyword | Case-insensitive keyword | +| `` | string | Absolute path, or path relative to the directory of the parent `.inp` file | + +**Rules:** + +- Only one `FILE` line is read; any additional lines in the section are ignored. +- If `[2D_MESH_FILE]` is absent, mesh sections are read inline from the main `.inp` (existing behaviour, unchanged). +- The external file may contain any combination of `[2D_OPTIONS]`, `[2D_VERTICES]`, `[2D_TRIANGLES]`, `[2D_VERTEX_NODE_MAP]`, and `[2D_TRIANGLE_NODE_MAP]`. +- If `[2D_OPTIONS]` appears in both the main `.inp` and the external file, the external file values are applied **after** the main file values (external file wins). +- The external file is parsed with the same section parser as the main file. `[2D_MESH_FILE]` is **not** registered in the sub-parser — recursive references are not supported. +- A missing or unreadable external file is a fatal parse error. + +**Example — relative path:** + +``` +[2D_MESH_FILE] +FILE meshes/city_basin.2dm +``` + +**Example — absolute path:** + +``` +[2D_MESH_FILE] +FILE /data/shared_meshes/city_basin.2dm +``` + +**Typical external file layout:** + +``` +;; OpenSWMM 2D Mesh File — city_basin.2dm + +[2D_OPTIONS] +MAX_TIMESTEP 10.0 +DRY_DEPTH 0.001 +REPORT_2D YES + +[2D_VERTICES] +;; X Y Z TAG + 0.0 0.0 10.5 V0 + 10.0 0.0 10.3 V1 + 5.0 8.66 10.1 V2 + +[2D_TRIANGLES] +;; V1 V2 V3 Manning_n TAG + 0 1 2 0.030 T0 + +[2D_VERTEX_NODE_MAP] +;; VERTEX NODE [CD] [AREA] + V0 J1 0.65 + +[2D_TRIANGLE_NODE_MAP] +;; TRIANGLE NODE [CD] [AREA] + T0 ST1 0.60 +``` + +--- + +### 1.3 `[2D_OPTIONS]` + +Parsed by `openswmm::twoD::parse2DOptionsLine`. One `KEY VALUE` pair per +line. Unknown keys are an error (the parser refuses silently dropped +options to avoid mis-typed keys being ignored). + +``` +;; Solver and timestepping options for the 2D surface routing module +;; +;; Parameter Value ;; Notes +;; ---------------------- ---------- ;; -------------------------------- +MAX_TIMESTEP 10.0 ;; Max CVODE internal step (s) +MIN_TIMESTEP 0.001 ;; Min CVODE internal step (s) +REL_TOLERANCE 1.0e-4 ;; CVODE relative tolerance +ABS_TOLERANCE 1.0e-6 ;; CVODE absolute tolerance (m of depth) +DRY_DEPTH 0.001 ;; Wet/dry threshold (m of depth) +LIMITER_EPSILON 1.0e-6 ;; Jawahar-Kamath slope-limiter ε +COUPLING_CD 0.65 ;; Default discharge coefficient (orifice) +LINEAR_SOLVER GMRES ;; GMRES (wired) | BICGSTAB / TFQMR (reserved) +PRECONDITIONER NONE ;; NONE | JACOBI (wired) | ILU (reserved) +MAX_KRYLOV_DIM 30 ;; Max Krylov subspace dim (GMRES restart) +MAX_CVODE_STEPS 500 ;; Max CVODE internal steps per advance call +COUPLING_INTERVAL 0 ;; 0 = couple every SWMM routing step +REPORT_2D YES ;; YES/NO — toggles internal 2D reporting +OUTPUT_FILE run.h5 ;; Optional HDF5 output path; relative paths + ;; resolve against the .inp directory +``` + +**Defaults** (see `SolverOptions2D.hpp`): `MAX_TIMESTEP=10.0`, +`MIN_TIMESTEP=0.001`, `REL_TOLERANCE=1e-4`, `ABS_TOLERANCE=1e-6`, +`DRY_DEPTH=0.001`, `LIMITER_EPSILON=1e-6`, `COUPLING_CD=0.65`, +`LINEAR_SOLVER=GMRES`, `PRECONDITIONER=NONE`, `MAX_KRYLOV_DIM=30`, +`MAX_CVODE_STEPS=500`, `COUPLING_INTERVAL=0`, `REPORT_2D=YES`, +`OUTPUT_FILE` empty (no 2D output). + +**Phase-1 reservation:** `BICGSTAB` / `TFQMR` for `LINEAR_SOLVER` and +`ILU` for `PRECONDITIONER` are accepted by the parser but rejected at +`CvodeSurfaceSolver::initialize` with a clear runtime error. The slots +are reserved for the planned hypre/BoomerAMG integration; see +`docs/2D_KNOWN_STIFFNESS_ISSUE.md` for the rationale. + +### 1.4 `[2D_VERTICES]` + +Parsed by `parse2DVertexLine`. Each row defines a mesh vertex. Vertices +are indexed in order of appearance (0-based). Format: + +``` +;; X Y Z TAG (optional) +100.0 200.0 10.5 +100.5 200.5 10.3 inlet_region +101.0 200.0 10.1 +``` + +- **X, Y** — Horizontal coordinates. **Unit policy (see §2A):** the + default contract is "project length units" — feet when SWMM + `FLOW_UNITS` is US, metres otherwise. The engine multiplies XY/Z by + 0.3048 at load time for US projects (`SurfaceRouter2D::initialize`). + A producer that has already written SI metres can declare + `;; UNITS: SI (m)` near the top of the file, and the engine will + skip the load-time scaling. +- **Z** — Ground surface elevation, in the same unit policy as XY. +- **TAG** — Optional string tag. Referenced by `[2D_VERTEX_NODE_MAP]` + and `[2D_TRIANGLE_NODE_MAP]` as an alternative to the 0-based index. + +### 1.5 `[2D_TRIANGLES]` + +Parsed by `parse2DTriangleLine`. Each row defines a triangle by +referencing three vertex indices (0-based) and a Manning's roughness +coefficient. + +``` +;; V1 V2 V3 MANNINGS_N TAG (optional) +0 1 2 0.035 +3 4 5 0.025 road_surface +``` + +- **V1, V2, V3** — Vertex indices (0-based) into `[2D_VERTICES]`. +- **MANNINGS_N** — Manning's roughness coefficient (s/m^{1/3}). + Default if unset: 0.035. **No unit conversion is applied** — the + coefficient is dimensionally consistent under both SI and US + Manning's equations because the engine's internal solver runs in SI + and the constant 1.486 conversion lives entirely in the 1D side. +- **TAG** — Optional string tag. + +### 1.6 `[2D_VERTEX_NODE_MAP]` + +Parsed by `parse2DVertexNodeMapLine`. Maps mesh vertices to SWMM +coupling nodes — exchange flow uses an orifice equation at each +mapped point. + +``` +;; VERTEX_INDEX_OR_TAG SWMM_NODE_NAME [CD] [AREA] +5 J1 +inlet_region J2 0.70 1.5 +``` + +- **VERTEX_INDEX_OR_TAG** — 0-based integer OR a tag string defined in + `[2D_VERTICES]`. Tag form is more robust to mesh edits that + renumber vertices. +- **SWMM_NODE_NAME** — Name of an existing SWMM `[JUNCTIONS]`, + `[OUTFALLS]`, or `[STORAGE]` node. Resolved to a node index at + `SurfaceRouter2D::initialize` time; an unknown name is a fatal + error. +- **CD** *(optional)* — Discharge coefficient. Default 0.65. +- **AREA** *(optional)* — Effective exchange area (m² in SI projects, + ft² in US projects — same unit policy as the mesh XY, scaled by + 0.3048² at load time when the mesh declares non-SI units). + Default 1.0. + +### 1.7 `[2D_TRIANGLE_NODE_MAP]` + +Parsed by `parse2DTriangleNodeMapLine`. Maps triangle centroids to +SWMM coupling nodes. Same parameter set and unit policy as +`[2D_VERTEX_NODE_MAP]`. + +``` +;; TRIANGLE_INDEX_OR_TAG SWMM_NODE_NAME [CD] [AREA] +0 J3 +road_surface J4 0.60 4.0 +``` + +### 1.8 `[2D_BOUNDARY_CONDITIONS]` + +Parsed by `parse2DBoundaryConditionsLine`. Per-edge boundary type and +parameter, indexed by `(TRI, EDGE)` where `EDGE ∈ {0,1,2}` is the +local edge index opposite the corresponding vertex. Rows are +accumulated into a pending-rows buffer during parsing +(`SurfaceRouter2D::PendingBoundaryRow`) and drained into the per-edge +`BoundaryData` SoA inside `SurfaceRouter2D::initialize` once the mesh +has been sized. + +``` +;; TRI EDGE TYPE PARAM_1 PARAM_2 GROUP +;; --- ---- --------------- -------------- ------- -------- + 12 0 NORMAL_FLOW 0.0020 * * + 12 1 SPECIFIED_STAGE 95.4 * * + 45 2 TS_STAGE DownstreamTS * Outlet + 77 0 SPECIFIED_FLOW 0.150 * * + 77 1 TS_FLOW Hydrograph_A * Inlet + 90 2 RATING_CURVE Outfall_Q_H * Outlet +``` + +**TYPE** is one of: + +| Token | `BoundaryType` enum | PARAM_1 meaning | +|-------|---------------------|-----------------| +| `WALL` | `WALL` (default) | unused — zero-flux | +| `NORMAL_FLOW` | `NORMAL_FLOW` | bed slope (≥ 0, dimensionless) | +| `SPECIFIED_STAGE` | `SPECIFIED_STAGE` | constant total head (m, SI) | +| `TS_STAGE` | `SPECIFIED_STAGE` | timeseries name (head varies in time) | +| `SPECIFIED_FLOW` | `SPECIFIED_FLOW` | discharge per metre of edge (m³/s/m, outward positive) | +| `TS_FLOW` | `SPECIFIED_FLOW` | timeseries name (per-metre flow varies in time) | +| `RATING_CURVE` | `RATING_CURVE` | curve registry name (stage → flow) | + +**PARAM_2** is currently reserved (always `*`). **GROUP** is an +optional named group (`*` = none) used by GUI workflows for bulk edits; +the engine stores it but does not act on it today. + +> **Current solver behaviour.** Per the comment on +> `BoundaryType` in `BoundaryData.hpp`: storage, parsing, and the C +> API for all five types are implemented. **The FV-SWE flux integration +> for non-Wall BCs is deferred to a follow-up slice (V-E-FLUX)** — the +> flux calculator in `SurfaceFluxCalculator.cpp` (line 131) currently +> treats every boundary edge as Wall regardless of declared type. This +> is intentional: it lets GUI / I/O round-trip work proceed in parallel +> with the FV-SWE BC integration. + +### 1.9 `[2D_EDGE_CONVEYANCE]` + +Parsed by `parse2DEdgeConveyanceLine`. Per-edge multiplicative factor +in `[0, 1]` that attenuates the diffusion-wave flux across the named +edge. Default 1.0 (unrestricted) for every edge not listed. Motivating +use cases: culverted embankments, partially-permeable hedgerows, +perforated fences, vegetation strips, "leaky" internal weirs. + +The row format mirrors SWMM `[CONDUITS]` `From-Node` / `To-Node` +convention but identifies a **mesh edge** by its endpoint vertex +indices. The pair is unordered — swapping `FROM_VERTEX` and +`TO_VERTEX` does not change the value (Q3 silent partner-slot +mirroring; see §11A). + +``` +;; Per-edge conveyance multiplier in [0, 1]. Default 1.0 (unrestricted). +;; FROM/TO is the (unordered) pair of mesh vertex indices at the edge's +;; endpoints. Authoring an obstruction as a polyline is the GUI's job: +;; walk the polyline, emit one row per shared interior edge. +[2D_EDGE_CONVEYANCE] + +;; FROM_VERTEX TO_VERTEX CONVEYANCE +;; ----------- --------- ---------- + 17 18 0.40 ;; hedgerow segment + 18 19 0.40 + 42 87 0.30 ;; culverted embankment + 55 61 0.00 ;; fully blocked → equivalent to Wall +``` + +Parser rules (5d): + +- Exactly three tokens per row: `FROM_VERTEX TO_VERTEX CONVEYANCE`. +- `FROM_VERTEX` and `TO_VERTEX` are non-negative integers in + `[0, n_vertices)`, **must differ**. Equal endpoints raise a + parse-time error; out-of-range endpoints raise an init-time error. +- `CONVEYANCE` parses as a `double`; values outside `[0, 1]` are + rejected at parse time (Q1 strict clamp). +- Rows accumulate into `SurfaceRouter2D::pendingEdgeConveyanceRows()` + during parsing. `SurfaceRouter2D::initialize` drains them after + `buildMeshTopology`, builds a one-shot vertex-pair → slot lookup + in one O(n_triangles) pass, and writes the factor into every slot + the edge resolves to (interior = 2 slots, boundary = 1). +- Duplicate rows naming the same edge: last-write-wins. +- A vertex pair that does not form a real mesh edge raises a fatal + init-time error. + +The factor is applied in `SurfaceFluxCalculator::computeEdgeFluxes` +immediately before `state.edge_flux[idx] = F_e`, **after** the +wet/dry Hermite shutoff. Today it has no effect on boundary edges +(the calculator early-returns at `nbr < 0`); once the V-E-FLUX slice +lands the factor will also multiply BC-derived boundary fluxes. + +C API: `swmm_2d_get_edge_conveyance` / `swmm_2d_set_edge_conveyance` +/ `swmm_2d_get_edge_conveyance_bulk` / `swmm_2d_reset_edge_conveyance`. +`set` clamps to `[0, 1]` and silently mirrors to the partner slot +for interior edges. Safe between routing steps; calling DURING a +routing step is undefined (the CVODE sub-stepper holds a const +reference to the mesh). + +--- + +## 2. Data Structures (SoA Layout) + +All 2D data structures follow the existing OpenSWMM SoA pattern: parallel `std::vector` arrays, a single `resize()` method, `save_state()` / `reset_state()` lifecycle methods, and flat 2D arrays where needed. + +### 2.1 File: `src/engine/2d/data/MeshData.hpp` + +The actual struct (abbreviated; comments preserved): + +```cpp +namespace openswmm::twoD { + +struct MeshData { + + // Vertex arrays — indexed by vertex index [0, n_vertices) + std::vector vx, vy, vz; // coords + ground elevation + std::vector vtag; // optional tag + + // Triangle connectivity (3 vertex indices per triangle) + std::vector tri_v0, tri_v1, tri_v2; + // Neighbour connectivity (3 adjacent triangle indices, -1 = boundary) + std::vector tri_nbr0, tri_nbr1, tri_nbr2; + + // Precomputed cell geometry + std::vector tri_area; // Planimetric area (m²) + std::vector tri_cx, tri_cy, tri_cz; // Centroid xyz + + // Edge geometry — flat 2D, [tri * 3 + edge_local] + std::vector edge_length; + std::vector edge_nx, edge_ny; // outward unit normal + std::vector edge_mx, edge_my, edge_mz; // edge midpoint xyz + + // §11A — per-edge conveyance factor in [0,1] (default 1.0). + // Mirrored across interior edges for mass conservation. + std::vector edge_conveyance; + + // Surface properties + std::vector mannings_n; + std::vector tri_tag; + + // Vertex reconstruction stencil — CSR (pseudo-Laplacian weights) + std::vector vert_stencil_ptr; + std::vector vert_stencil_idx; + std::vector vert_stencil_wt; + + // Coupling maps (resolved indices, -1 = none) + std::vector vert_coupled_node; + std::vector tri_coupled_node; + + // Coupling parameters per coupling point + std::vector vert_coupling_cd; // Discharge coefficient (default 0.65) + std::vector vert_coupling_area; // Effective exchange area + std::vector tri_coupling_cd; + std::vector tri_coupling_area; + + // Deferred-resolution names (cleared by SurfaceRouter2D::initialize) + std::vector vert_coupled_node_name; + std::vector tri_coupled_node_name; + + int n_vertices() const noexcept { return static_cast(vx.size()); } + int n_triangles() const noexcept { return static_cast(tri_v0.size()); } + + void resize_vertices(int nv); // also resizes vert_coupled_node[_name], + // vert_coupling_cd / area + void resize_triangles(int nt); // also resizes edge_*, mannings_n, + // tri_coupled_node[_name], tri_coupling_cd / area +}; + +} // namespace openswmm::twoD +``` + +> **Implementation note.** Topology and stencil construction are NOT +> member functions of `MeshData`. They live in free functions +> `openswmm::twoD::buildMeshTopology(MeshData&)` (in `mesh/MeshBuilder.cpp`) +> and `openswmm::twoD::buildVertexStencils(MeshData&)` (in +> `mesh/VertexReconstruction.cpp`), called from +> `SurfaceRouter2D::initialize` in the order: optional ft→m mesh scaling +> → `buildMeshTopology` → `validateMesh` → `buildVertexStencils`. + +### 2.2 File: `src/engine/2d/data/SurfaceStateData.hpp` + +All values below are in the **2D solver's SI internal units** (m, m/s, +m²/s, etc.). Any conversion from the SWMM 1D side (which runs in +project units) happens at the boundary; see §2A Unit Conversion +Strategy. + +```cpp +namespace openswmm::twoD { + +struct SurfaceStateData { + + // State variables — per triangle [0, n_triangles) + std::vector depth; // Overland flow depth ψ_o (m) + std::vector head; // Total head h_o = z_s + ψ_o (m) + + // Gradient fields (per triangle) + std::vector grad_hx, grad_hy; // unlimited + std::vector grad_hx_lim, grad_hy_lim; // Jawahar-Kamath limited + + // Reconstructed head at vertices — [0, n_vertices) + std::vector vert_head; + + // Cell-centred velocity (RT0 reconstruction from edge fluxes) + std::vector face_vx, face_vy; // (m/s) + + // Per-cell continuity residual (m³/s; ≈0 when conservative) + std::vector cell_continuity_err; + + // Edge fluxes — flat 2D: [tri * 3 + edge] + std::vector edge_flux; + + // Source / sink terms — per triangle (m/s of depth) + std::vector rainfall; // from rain gages + std::vector coupling_flux; // exchange with SWMM nodes (+ = into 2D) + std::vector net_source; // accumulated source / sink + + // ----------------------------------------------------------------------- + // Forcing-override channels (C API — external control) + // mode: 0=computed, 1=override, 2=add; persist: 0=reset, 1=persist + // ----------------------------------------------------------------------- + std::vector rainfall_forced, rainfall_persist; + std::vector rainfall_force_val; + std::vector coupling_forced, coupling_persist; + std::vector coupling_force_val; + + // Previous step state (for restart / Picard reset) + std::vector old_depth; + + // Cumulative statistics / rendering envelopes — per cell. + // These are monotone (max) or accumulating (volume) over the run; a single + // snapshot of them at any time is the envelope up to that time, so the GUI + // can draw a max-depth / max-velocity flood map without scanning the time + // series. See §14A. + std::vector stat_max_depth; // max overland depth ψ_o (m) + std::vector stat_max_velocity; // max cell speed |v| = √(vx²+vy²) (m/s) + std::vector stat_max_cont_err; // max |cell_continuity_err| (m³/s) + std::vector stat_cum_volume; // ∫ depth·area dt (m³) + + void resize(int n_triangles, int n_vertices); // assigns all arrays + void save_state() noexcept; // depth → old_depth + void reset_state() noexcept; // old_depth → depth + void clear_reset_forcings() noexcept; // honour persist=0 flags + + // Fuses all envelope updates into the single per-cell loop already walked + // for stat_cum_volume — no extra passes. MUST be called AFTER + // computeFaceVelocity and computeCellContinuity so face_vx/vy and + // cell_continuity_err hold the accepted end-of-step values (see §8.5). + void update_statistics(const std::vector& tri_area, + double dt) noexcept; +}; + +} // namespace openswmm::twoD +``` + +### 2.3 File: `src/engine/2d/data/SolverOptions2D.hpp` + +```cpp +namespace openswmm::twoD { + +enum class LinearSolverType : int8_t { + GMRES = 0, // Phase 1 — WIRED + BICGSTAB = 1, // Reserved; initialize() rejects + TFQMR = 2 // Reserved; initialize() rejects +}; + +enum class PreconditionerType : int8_t { + NONE = 0, // Phase 1 — WIRED + JACOBI = 1, // Phase 1 — WIRED (diagonal heuristic) + ILU = 2 // Reserved; initialize() rejects +}; + +struct SolverOptions2D { + // CVODE / linear-solver knobs (parsed from [2D_OPTIONS]; see §1.3) + double max_timestep = 10.0; // Max CVODE internal step (s) + double min_timestep = 0.001; // Min CVODE internal step (s) + double rel_tolerance = 1.0e-4; + double abs_tolerance = 1.0e-6; + double dry_depth = 0.001; // Wet/dry threshold (m) + double limiter_epsilon = 1.0e-6; // Slope-limiter ε + double coupling_cd = 0.65; // Default discharge coefficient + int max_krylov_dim = 30; + int coupling_interval = 0; // 0 = couple every SWMM step + int max_cvode_steps = 500; + bool report_2d = true; + + LinearSolverType linear_solver = LinearSolverType::GMRES; + PreconditionerType preconditioner = PreconditionerType::NONE; + + // File paths from input + std::string mesh_file; // [2D_MESH_FILE] FILE token (may be relative) + std::string output_file; // [2D_OPTIONS] OUTPUT_FILE (HDF5; may be relative) + + // ----------------------------------------------------------------------- + // Unit-system bridge — NOT parsed from input; computed in + // SurfaceRouter2D::initialize() from the project FLOW_UNITS. + // The 2D solver runs internally in SI; these convert at the coupling + // boundary (see NodeCoupling.cpp). SI projects (CMS/LPS/MLD) yield 1.0. + // ----------------------------------------------------------------------- + double len_1d_to_2d = 1.0; // ft → m (= 0.3048 for US) + double len_2d_to_1d = 1.0; // m → ft + double vol_1d_to_2d = 1.0; // ft³ → m³ + double flow_1d_to_2d = 1.0; // ft³/s → m³/s + double flow_2d_to_1d = 1.0; // m³/s → ft³/s + + // ----------------------------------------------------------------------- + // ;; UNITS: header flag — set by prescan2DUnitsHeader, NOT parsed from + // [2D_OPTIONS]. When true the mesh on disk is already SI (m), so + // SurfaceRouter2D::initialize SKIPS the ft→m mesh scaling. The + // coupling-side factors above stay FLOW_UNITS-driven because they + // describe the 1D side of the boundary, not the mesh. + // ----------------------------------------------------------------------- + bool mesh_units_si = false; +}; + +} // namespace openswmm::twoD +``` + +### 2.4 File: `src/engine/2d/data/BoundaryData.hpp` + +Per-edge boundary-condition SoA, flat-indexed `[tri * 3 + edge_local]` +to match `edge_flux`, `edge_length`, etc. Sized to `n_triangles * 3` +inside `SurfaceRouter2D::initialize` and initialised to `WALL` defaults; +parsed rows from `[2D_BOUNDARY_CONDITIONS]` are then drained from the +`PendingBoundaryRow` scratch buffer into the appropriate slot. + +```cpp +namespace openswmm::twoD { + +enum class BoundaryType : int8_t { + WALL = 0, // Zero-flux wall (default) + NORMAL_FLOW = 1, // Manning outflow using bed slope + SPECIFIED_STAGE = 2, // Prescribed water surface elevation (constant or TS) + SPECIFIED_FLOW = 3, // Per-metre discharge (constant or TS) + RATING_CURVE = 4 // Stage → flow lookup +}; + +struct BoundaryData { + + std::vector edge_bc_type; // BoundaryType cast + + // NORMAL_FLOW + std::vector edge_bed_slope; // dimensionless, ≥ 0 + + // SPECIFIED_STAGE + std::vector edge_bc_head; // total head (m) + std::vector edge_bc_tseries; // -1 const, -2 unresolved name, ≥0 table idx + std::vector edge_bc_tseries_name; + + // SPECIFIED_FLOW + std::vector edge_bc_flow; // per-metre discharge (m³/s/m, outward+) + std::vector edge_bc_flow_tseries; + std::vector edge_bc_flow_tseries_name; + + // RATING_CURVE + std::vector edge_bc_rating_curve; // -1, -2, or curve index + std::vector edge_bc_rating_curve_name; + + // Cumulative boundary flux (m³, outflow positive) — mass-balance + std::vector edge_bc_cum_flux; + + void resize(int n_edges); + int size() const noexcept; +}; + +} // namespace openswmm::twoD +``` + +> **Current solver behaviour.** Storage + parsing + C API are wired for +> all five `BoundaryType` values, but +> `SurfaceFluxCalculator::computeEdgeFluxes` treats every boundary edge +> as `WALL` regardless of declared type. The FV-SWE non-Wall flux +> integration is deferred to slice V-E-FLUX. `edge_bc_cum_flux` is +> updated only when that slice lands; today it stays at 0 for non-Wall +> edges. + +--- + +## 2A. Unit Conversion Strategy + +This section is referenced from §1.4 / §1.6 / §2.3 / §6 / §8. It is the +single authoritative description of how units flow between the SWMM 1D +engine, the 2D mesh on disk, and the 2D solver's internal SI world. + +### 2A.1 The two clocks + +| Side | Internal unit system | Where it lives | +|------|----------------------|----------------| +| 1D SWMM engine | **Project units** — feet / ft³ / ft³·s⁻¹ for US `FLOW_UNITS` (CFS, GPM, MGD); metres / m³ / m³·s⁻¹ for SI `FLOW_UNITS` (CMS, LPS, MLD). Manning's *g* = 32.2 ft·s⁻², *φ* = 1.486 for US. | `ctx.nodes.*`, `ctx.links.*`, `ctx.options.flow_units`. | +| 2D solver | **SI** — metres / m² / m³ / m³·s⁻¹, *g* = 9.80665 m·s⁻². | `mesh_`, `state_`, `NodeCoupling.cpp` after multiplication. | + +The 1D engine is unit-aware throughout — it converts to display units +only at output. The 2D solver is intentionally unit-naive: every double +inside `MeshData`, `SurfaceStateData`, and the Kumar et al. (2009) FV +math is in SI. The bridge between the two systems lives in two narrow +places: (a) the mesh-load scaling in `SurfaceRouter2D::initialize`, +and (b) the per-quantity multiplications in `NodeCoupling.cpp`. + +### 2A.2 The five bridge factors + +Defined in `SolverOptions2D`, computed once at the top of +`SurfaceRouter2D::initialize` from `ctx.options.flow_units`: + +```cpp +const int us = ucf::getUnitSystem(static_cast(ctx.options.flow_units)); +const double ft_to_m = (us == 0) ? 0.3048 : 1.0; // us==0 → US +options_.len_1d_to_2d = ft_to_m; +options_.len_2d_to_1d = 1.0 / ft_to_m; +options_.vol_1d_to_2d = ft_to_m * ft_to_m * ft_to_m; +options_.flow_1d_to_2d = options_.vol_1d_to_2d; +options_.flow_2d_to_1d = 1.0 / options_.vol_1d_to_2d; +``` + +For SI projects every factor is 1.0 — no work happens at the boundary. + +### 2A.3 Mesh load: optional one-shot ft→m scaling + +Immediately after the factors are computed: + +```cpp +if (!options_.mesh_units_si && options_.len_1d_to_2d != 1.0) { + const double f = options_.len_1d_to_2d; + const double f2 = f * f; + for (auto& v : mesh_.vx) v *= f; + for (auto& v : mesh_.vy) v *= f; + for (auto& v : mesh_.vz) v *= f; + for (auto& a : mesh_.vert_coupling_area) a *= f2; + for (auto& a : mesh_.tri_coupling_area) a *= f2; +} +``` + +The default assumption is that `[2D_VERTICES]` / `[2D_TRIANGLES]` and +the optional `[CD] [AREA]` columns on `[2D_*_NODE_MAP]` are in project +length units (feet for US, metres for SI). The scaling brings them into +SI before `buildMeshTopology` runs, so every derived quantity +(`tri_area`, `edge_length`, centroids, midpoint Z) is computed in SI. + +When `options_.mesh_units_si == true` the loop is skipped entirely — +the mesh is already SI and applying the factor a second time would +scale a US project's mesh down by 0.3048 (a 10× error in area). + +### 2A.4 Header pre-scan — opting into SI + +`mesh_units_si` is **not** parsed from `[2D_OPTIONS]`. It is set by +`openswmm::twoD::prescan2DUnitsHeader(path, opts)` in +`src/engine/2d/input/SectionHandlers2D.cpp`. The helper opens the file, +walks `;;`-prefixed comment lines, and looks for: + +``` +;; UNITS: +;; SOURCE_CRS: ;; informational only; engine ignores +``` + +If `` matches one of `SI (m)`, `m`, `metre`, `metres`, `meter`, +`meters` (case-insensitive), the flag is set to `true`. Any other value +— including an explicit `ft`, `foot`, `feet`, or absent header — +leaves the flag at its prior value. + +Call sites: + +| Caller | File | When | +|--------|------|------| +| `SWMMEngine::open` | `src/engine/core/SWMMEngine.cpp` (~L142) | Inline `.inp`, right after `register2DSections`. | +| `load2DMeshExternalFile` | `src/engine/2d/input/SectionHandlers2D.cpp` (~L472) | External `.2dm`, after path resolution, before parsing. | + +The external file scan runs *after* the inline scan, so an external +`.2dm` declaration overrides whatever the main `.inp` claimed. This +matches how `[2D_OPTIONS]` works (external file wins on key +collisions). + +### 2A.5 1D ⇄ 2D coupling boundary (per-quantity) + +Every conversion in `NodeCoupling.cpp` traces back to one of the five +factors above. The full list, in the order it appears in +`computeCouplingExchange` / `updateOutfallBoundaries` / +`transferOutfallDischarges`: + +| Quantity | Direction | Source | Multiplication | Result unit | +|----------|-----------|--------|----------------|-------------| +| 1D node head (`nodes.head[ni]`) | 1D → 2D | SWMM | `× len_1d_to_2d` | m | +| 1D node depth (`nodes.depth[ni]`) | 1D → 2D | SWMM | `× len_1d_to_2d` | m | +| 1D rim/cap elevation (`invert_elev + full_depth + sur_depth`) | 1D → 2D | SWMM | `× len_1d_to_2d` | m | +| 1D node available volume (`full_volume − volume`) | 1D → 2D | SWMM | `× vol_1d_to_2d` | m³ | +| Head difference `dh = h_2d − h_1d` | computed | — | both already in m | m | +| Orifice `Q = Cd·A·sign(dh)·√(2g·│dh│)`, *g*=9.80665 | computed | — | inputs in SI | m³·s⁻¹ | +| 2D → 1D coupling flow (`coupling_inflow[ni]`) | 2D → 1D | NodeCoupling | `× flow_2d_to_1d` | ft³·s⁻¹ (US) or m³·s⁻¹ (SI) | +| 2D outfall head feedback (`outfall_2d_head[ni]`) | 2D → 1D | NodeCoupling | `× len_2d_to_1d` | ft (US) or m (SI) | +| 1D outfall discharge (`nodes.outflow[ni]`) | 1D → 2D | NodeCoupling | `× flow_1d_to_2d` | m³·s⁻¹ | + +The 2D side never sees feet. The 1D side never sees an unconverted SI +value entering a continuity accumulator. + +### 2A.6 Manning's *n* + +Manning's *n* is the only mesh-side quantity that is **not** multiplied +at load time. The Manning equation has a built-in *φ* = 1.486 +unit-conversion constant in US engines that absorbs the foot/metre +mismatch; in the SI 2D solver the constant is 1.0 and the same numeric +*n* value gives the same physical roughness. Conventional values (e.g. +*n* = 0.035 for natural channels) are unit-agnostic. + +### 2A.7 What this leaves unprotected + +The bridge assumes the mesh's XY linear unit and the SWMM +`FLOW_UNITS`-implied length unit *agree* in the legacy +(header-absent) case. Two failure modes the engine does **not** guard +against today: + +1. SWMM `FLOW_UNITS = CFS` with a mesh stored in metres (no header). + → Engine multiplies metres by 0.3048 → mesh becomes 30.48% of true + size. +2. SWMM `FLOW_UNITS = CMS` with a mesh stored in feet (no header). + → Engine leaves values untouched → solver treats feet as metres. + +Both are fixable by adding `;; UNITS:` to the producer side. A future +project-open warning that cross-checks the project CRS linear unit +against `FLOW_UNITS` is tracked in +`docs/MESH_CRS_UNIT_CONVERSION_PLAN.md` follow-up F2. + +--- + +## 3. Mathematical Formulation — Diffusion-Wave Surface Flow + +### 3.1 Governing Equation + +The 2D diffusion-wave approximation of St. Venant's equation (Kumar et al., 2009, Eq. [1]): + +``` +∂ψ_o/∂t = ∇·(ψ_o · K(ψ_o) · ∇h_o) - Q_og + Q_ss +``` + +Where: +- `ψ_o` = overland flow depth (m) +- `h_o = z_s + ψ_o` = total overland flow head (m) +- `z_s` = ground surface elevation (m) +- `K(ψ_o)` = diffusive conductance (m/s) +- `Q_og` = vertical flux exchange with subsurface (m/s) — **zero in Phase 1** +- `Q_ss` = sources/sinks (rainfall, evaporation) (m/s) + +### 3.2 Diffusive Conductance + +``` +K(ψ_o) = (ψ_o^{2/3}) / (n · |∂h_o/∂s|^{1/2}) +``` + +Where `n` is Manning's roughness and `s` is the direction of maximum slope. + +### 3.3 Semi-Discrete Finite Volume Form (Eq. [10]) + +For each triangular cell `i`: + +``` +A_i · dψ_o/dt = Σ_{j=1}^{3} n_j · F_j + Q_ss · V_i +``` + +Where: +- `A_i` = planimetric area of triangle `i` +- `F_j` = lateral flux vector on edge `j` +- `n_j` = outward normal to edge `j` +- The coupling flux `G_k` (vertical flux to subsurface) is zero in Phase 1 + +### 3.4 Lateral Flux Calculation (Eq. [15a]) + +``` +n_j · F_j = UW[ψ_o · K(ψ_o) · ∇h_o]_ξ · ξ_ij +``` + +Where `UW[]` is the upwind function (flux computed at the upstream cell face) and `ξ_ij` is the edge length. + +### 3.5 Second-Order Accuracy Components + +1. **Edge Gradient Calculation** (Eq. [16]–[18]): Green-Gauss theorem on variational triangles +2. **Vertex Reconstruction** (Eq. [19]–[21]): Pseudo-Laplacian weighted interpolation from cell centres to vertices +3. **Linear Reconstruction at Edges** (Eq. [22]): `h_ξ = h_c + r · ∇h_l` +4. **Limited Gradient** (Eq. [23]–[24]): Jawahar-Kamath multidimensional limiter with weights based on L2 norms of unlimited gradients +5. **Unlimited Gradient** (Eq. [25]–[26]): Area-weighted average of edge gradients + +--- + +## 4. Solver Architecture + +### 4.1 File Organization + +``` +src/engine/2d/ +├── CMakeLists.txt +├── data/ +│ ├── MeshData.hpp +│ ├── SurfaceStateData.hpp +│ └── SolverOptions2D.hpp +├── mesh/ +│ ├── MeshBuilder.hpp // Topology, neighbours, edge geometry +│ ├── MeshBuilder.cpp +│ ├── VertexReconstruction.hpp // Pseudo-Laplacian stencil weights +│ └── VertexReconstruction.cpp +├── solver/ +│ ├── SurfaceFluxCalculator.hpp // Edge flux, gradient, limiter +│ ├── SurfaceFluxCalculator.cpp +│ ├── CvodeSurfaceSolver.hpp // CVODE wrapper for surface ODE system +│ ├── CvodeSurfaceSolver.cpp +│ └── DiffusiveConductance.hpp // K(ψ_o) computation +├── coupling/ +│ ├── NodeCoupling.hpp // Orifice-equation exchange with SWMM +│ └── NodeCoupling.cpp +├── input/ +│ ├── SectionHandlers2D.hpp // Input section parsers +│ └── SectionHandlers2D.cpp +├── SurfaceRouter2D.hpp // Top-level orchestrator +└── SurfaceRouter2D.cpp +``` + +### 4.2 CVODE Integration + +**Dependency:** SUNDIALS (via vcpkg: `sundials[cvode]`) + +The surface routing ODE system is: + +``` +dy/dt = f(t, y) +``` + +where `y[i] = ψ_o[i]` (overland flow depth at triangle `i`), and `f(t, y)` computes the right-hand side from the semi-discrete finite volume formulation. + +#### CVODE Setup + +```cpp +class CvodeSurfaceSolver { +public: + /// Initialize CVODE with the ODE system of size n_triangles + void initialize(const MeshData& mesh, const SolverOptions2D& opts); + + /// Advance the solution from t_current to t_target + /// Returns actual time reached + double advance(double t_current, double t_target, + SurfaceStateData& state, const MeshData& mesh); + + /// Clean up CVODE memory + void finalize(); + +private: + void* cvode_mem_ = nullptr; // CVODE memory block + SUNLinearSolver ls_ = nullptr; // GMRES (or alternative) + N_Vector y_ = nullptr; // State vector (wraps state.depth) + SUNContext ctx_ = nullptr; // SUNDIALS context + + /// RHS function: f(t, y, ydot) + /// Registered as CVRhsFn callback + static int rhs_fn(sunrealtype t, N_Vector y, N_Vector ydot, void* user_data); +}; +``` + +#### RHS Function Pseudocode + +``` +rhs_fn(t, y, ydot, user_data): + solver_ctx = (SolverContext*)user_data + mesh = solver_ctx->mesh + state = solver_ctx->state + opts = solver_ctx->opts + + // 1. Copy y into state.depth, compute head = z + depth + for i in 0..n_triangles: + state.depth[i] = max(y[i], 0) + state.head[i] = mesh.tri_cz[i] + state.depth[i] + + // 2. Reconstruct head at vertices (pseudo-Laplacian, Eq. [19]) + reconstruct_vertex_heads(mesh, state) + + // 3. Compute unlimited gradients (Eq. [25]-[26]) + compute_unlimited_gradients(mesh, state) + + // 4. Apply slope limiter (Eq. [23]-[24]) + compute_limited_gradients(mesh, state) + + // 5. Compute edge fluxes (Eq. [15a], [22], [30]) + for i in 0..n_triangles: + for e in 0..3: + compute_edge_flux(i, e, mesh, state, opts) + + // 6. Assemble RHS: A_i * dψ/dt = Σ fluxes + sources + for i in 0..n_triangles: + rhs = 0 + for e in 0..3: + rhs += state.edge_flux[i * 3 + e] + rhs += state.rainfall[i] * mesh.tri_area[i] + rhs += state.coupling_flux[i] * mesh.tri_area[i] + ydot[i] = rhs / mesh.tri_area[i] +``` + +#### Why CVODE with GMRES + +- **CVODE (BDF)** is designed for stiff ODE systems. The nonlinearity of Manning's equation (depth-dependent conductance) and the potentially stiff coupling between wet and dry cells makes this an appropriate choice. +- **GMRES** (Generalized Minimal Residual) is a Krylov iterative solver that avoids forming the full Jacobian matrix. CVODE uses a difference-quotient approximation for Jacobian-vector products, so the Jacobian is never explicitly stored. +- **Alternatives available:** SUNDIALS also provides BiCGStab (`SUNLinearSolver_SPBCGS`) and TFQMR (`SUNLinearSolver_SPTFQMR`) — exposed via `[2D_OPTIONS]`. + +#### vcpkg Integration + +Add to `vcpkg.json`: +```json +{ + "name": "sundials", + "version>=": "7.0.0", + "features": ["cvode"] +} +``` + +CMake: +```cmake +option(OPENSWMM_BUILD_2D "Build optional 2D surface routing module" OFF) + +if(OPENSWMM_BUILD_2D) + find_package(SUNDIALS REQUIRED COMPONENTS cvode) + target_link_libraries(openswmm_engine PRIVATE SUNDIALS::cvode) + target_compile_definitions(openswmm_engine PRIVATE OPENSWMM_HAS_2D=1) +endif() +``` + +The 2D module is **compile-time optional** — when `OPENSWMM_BUILD_2D=OFF`, no SUNDIALS dependency is required and all 2D code is excluded via `#ifdef OPENSWMM_HAS_2D`. + +--- + +## 5. Mesh Processing Pipeline + +### 5.1 Topology Construction (`MeshBuilder`) + +After parsing `[2D_VERTICES]` and `[2D_TRIANGLES]`: + +1. **Build edge-neighbour adjacency** — For each triangle, find the adjacent triangle sharing each edge. Use a hash map keyed by sorted vertex-pair `(min(va, vb), max(va, vb))` → first triangle sets the entry, second triangle completes the pair. + +2. **Compute edge geometry** — For each edge of each triangle: + - Edge length `ξ_ij` + - Outward unit normal `(nx, ny)` — perpendicular to edge, pointing away from cell centre + - Edge midpoint `(mx, my, mz)` + +3. **Compute cell geometry** — For each triangle: + - Planimetric area `A_i` via cross product + - Centroid `(cx, cy, cz)` as average of vertex coordinates + +4. **Identify boundary edges** — Edges with no neighbour (`nbr = -1`) are domain boundaries. Default: zero-flux (wall). Future: configurable via `[2D_BOUNDARY_CONDITIONS]`. + +### 5.2 Vertex Reconstruction Stencils (`VertexReconstruction`) + +For each vertex `b`, build the pseudo-Laplacian reconstruction stencil (Eq. [19]–[21]): + +1. Collect all triangles sharing vertex `b` → stencil cells `{1, ..., M}` +2. Compute moments: `I_xx`, `I_yy`, `I_xy`, `R_x`, `R_y` +3. Compute Lagrange multipliers: `λ_x`, `λ_y` +4. Compute weights: `ω_i = 1 + λ_x(x_i - x_b) + λ_y(y_i - y_b)` +5. Clip extraneous weights at boundaries (Jawahar & Kamath, 2000) +6. Store in CSR format: `vert_stencil_ptr`, `vert_stencil_idx`, `vert_stencil_wt` + +--- + +## 6. Coupling to SWMM + +This section reflects the as-built implementation in +`openswmm.engine/src/engine/2d/coupling/NodeCoupling.cpp`. See also +§2A on unit conversion and `docs/1D_2D_COUPLING_GATE_REVIEW.md` +for the design rationale behind the surcharge gate (C1/C2) and the +ponded-area suppression (C3a). + +### 6.1 Coupling Philosophy + +- The 2D module communicates with SWMM **via a dedicated + `nodes.coupling_inflow[]` channel** (not the generic forcing API). + The earlier design that routed 2D coupling through + `forcing.node_lat_inflow_value` with `OVERRIDE+PERSIST` was reverted + per review §11: it conflated 2D coupling with user-API forcing and + silently dropped the negative (1D→2D) half from routing continuity. +- `coupling_inflow[ni]` is signed: positive = 2D → 1D (drain into + pipe); negative = 1D → 2D (surcharge spill). The value is drained + inside `assembleLateralInflows` at the start of the next routing + step, where the sign is split into `routing_external` (positive + side) and `routing_flooding` (negative side, |Q|) for mass balance. +- The forcing API remains free for user-controlled inflows and is + unaffected by 2D coupling. + +### 6.2 Coupling at Nodes — Orifice Equation with Gates + +The per-coupling-point exchange is built up in six pieces inside +`computeCouplingExchange`. Numbered identifiers (C1, C2, C3a) match +`docs/1D_2D_COUPLING_GATE_REVIEW.md`. + +**1. Heads, in SI.** All 1D quantities are multiplied by the unit +bridge factors of §2A: + +``` +h_1d = nodes.head[ni] * opts.len_1d_to_2d // m +depth_1d_avail = nodes.depth[ni] * opts.len_1d_to_2d // m +z_top = (invert_elev + full_depth + sur_depth) + * opts.len_1d_to_2d // m +``` + +`h_2d` and `z_2d` come from `state.vert_head[v]` / `mesh.vz[v]` +(vertex coupling) or `state.head[ci]` / `mesh.tri_cz[ci]` (triangle +centroid coupling) — both already SI. + +**2. Available depth (max over the vertex stencil).** For +vertex-coupled points the 2D-side available depth is the **maximum** +depth across every cell in the vertex's reconstruction stencil. Two +failure modes this resolves: + +- (a) Spurious positive `vert_head − vert_z` on a fully dry mesh when + the vertex sits at a local low spot. The pseudo-Laplacian + reconstruction averages neighbour cell heads (all equal to their + centroid z), so the reconstructed head exceeds the vertex z by the + bed-relief amount alone. +- (b) Spurious zero `state.depth[first_tri_containing_v]` on a wet + bowl when the vertex sits below every touching cell's centroid: the + FV grid has no cell at the bowl bottom, so each touching cell's + depth = max(0, head − centroid_z) stays at 0 even when surrounding + cells hold water. + +Taking the max over the stencil ramps to 1 as soon as any neighbour +holds water; truly dry → every stencil cell has depth 0 → ramp 0. + +**3. Orifice + smooth wet/dry ramp.** + +``` +dh = h_2d - h_1d +Q_raw = Cd * A_eff * sign(dh) * sqrt(2 * g * |dh|) // g = 9.80665 +Q = Q_raw * wetRamp(source-side available depth) +``` + +The wet/dry ramp is a Hermite C¹ ramp, +`t² · (3 − 2t)` with `t = clamp(d / opts.dry_depth, 0, 1)`. The +ramp is applied to the source side (2D side when `Q > 0`, 1D side +when `Q < 0`). A hard wet/dry cutoff at `opts.dry_depth` would +introduce a step-discontinuity in `ydot` that breaks CVODE's BDF +corrector. + +**4. Surcharge gate (C1/C2) — capped vs. uncapped nodes.** + +- **C1 (effective-area widening).** Below `z_top` the exchange uses + `A_inlet` (the user-supplied `[CD] [AREA]`). Above `z_top` the area + widens to `A_manhole = 2 · A_inlet` over a 5 cm transition — the + manhole bolt has yielded and water reaches the surface. +- **C2 (surcharge gate ramp).** The orifice flow is additionally + multiplied by a direction-symmetric Hermite ramp on + `surcharge_excess = max(h_1d, h_2d) − z_top`. For an uncapped node + (`sur_depth = 0`) `z_top` reduces to the rim elevation, so the gate + opens the instant water reaches the rim and the ramp is a no-op + thereafter. For a capped node (`sur_depth > 0`) the gate stays + shut until either side rises above the cap. + +``` +A_eff = effectiveArea(h_max, z_top, full_depth, A_inlet, A_inlet * 2) +capRamp = hermite( (h_max - z_top) / 0.05 ) +Q = Q * capRamp +``` + +**5. Volume throttle when the 1D node is full (Q > 0 only).** Drain +flow into a node already at full volume is capped at +`(full_volume − volume) / dt`. If the node has zero remaining capacity, +Q is allowed only when it still represents surcharge drain-back +(`h_1d ≥ h_2d` → set Q = 0; otherwise pass it through so a +pressurised pipe can spill out). + +``` +available = (nodes.full_volume[ni] - nodes.volume[ni]) * opts.vol_1d_to_2d; +if (Q > 0 && available > 0 && dt > 0) { + Q = std::min(Q, available / dt); +} else if (Q > 0 && available <= 0 && h_1d >= h_2d) { + Q = 0.0; // node full; no drain-in allowed +} +``` + +**6. Inject — both sides of the bridge.** + +```cpp +// 1D side — drained into routing_external / routing_flooding next step. +// SI Q (m³/s) → 1D units (ft³/s for US). +nodes.coupling_inflow[ni] += Q * opts.flow_2d_to_1d; + +// 2D side — sink for the cell beneath this coupling point. +state.coupling_flux[ci] += -Q / mesh.tri_area[ci]; // m/s +``` + +`coupling_flux` accumulates over multiple coupling points sharing the +same cell. The CVODE RHS picks it up as the per-cell source/sink. + +### 6.3 Coupling at Outfalls — Dynamic Tailwater + Flap Gates + +Outfalls take two distinct paths through `NodeCoupling.cpp`: + +**Pre-routing (`updateOutfallBoundaries`).** Cache the 2D head at the +outfall coupling cell into `nodes.outfall_2d_head[ni]` so +`Outfall::setAllOutfallDepths` can apply +`max(h_standard, h_2d)` on every Picard iteration. The cached value +is in 1D units (`× opts.len_2d_to_1d`) because the consumer compares +against `h_standard` in feet for US projects. + +```cpp +double depth_2d = h_2d - bed_z; +if (depth_2d > 1.0e-4) { // wet — 0.1 mm threshold + nodes.outfall_2d_head[ni] = h_2d * opts.len_2d_to_1d; +} else { + nodes.outfall_2d_head[ni] = -1.0e30; // sentinel — no override +} +``` + +The dry-mesh sentinel is essential: a naïve check `h_2d > z_inv` +inside `setAllOutfallDepths` is true whenever `bed_z > z_inv`, which +is the common physical case (outfall pipe enters underground beneath +the surface mesh). Without the sentinel, every dry outfall would +fire the override and the 2D bed elevation would be backflowing into +1D. + +**Post-routing (`transferOutfallDischarges`).** The 1D outflow +computed during routing is injected as a source on the outfall's +coupling cell: + +```cpp +double Q_outfall = nodes.outflow[ni] * opts.flow_1d_to_2d; // ft³/s → m³/s +if (Q_outfall > 0.0) { + state.coupling_flux[ci] += Q_outfall / mesh.tri_area[ci]; // m/s source +} +``` + +Flap-gate logic lives inside `setAllOutfallDepths` itself (not in +`NodeCoupling.cpp`) so it can see the just-computed `h_standard` for +the iteration. `updateOutfallBoundaries` caches the raw `h_2d`; the +gate decision happens at consumption time. + +### 6.4 Coupling Point Construction + +`buildCouplingPoints(mesh, ctx)` is run once at the end of +`SurfaceRouter2D::initialize`. For each resolved +`vert_coupled_node[v] ≥ 0` it creates a `CouplingPoint` with the +vertex index, a representative containing-triangle index (first hit +wins), the per-vertex Cd / area from `[2D_VERTEX_NODE_MAP]`, and the +outfall flags from `ctx.nodes.type` / `outfall_has_flap_gate`. The +triangle-centroid path is identical but uses `tri_coupled_node[t]` +and `tri_coupling_*`. + +### 6.5 Coupling Sequence per Routing Step + +This is the actual sequence orchestrated by `SurfaceRouter2D` and +`SWMMEngine`. Compare with the older revisions of this section — they +listed a forcing-API path that no longer exists. + +``` +Pre-routing (SurfaceRouter2D::updateOutfallsPreRouting): + 1. For each outfall coupling point, cache state.head[cell] into + ctx.nodes.outfall_2d_head[ni]. Outfall::setAllOutfallDepths + then applies max(h_standard, h_2d) inside the 1D Picard loop. + +1D routing (SWMM core, unchanged): + 2. assembleLateralInflows drains nodes.coupling_inflow[] from the + previous step into routing_external / routing_flooding. + 3. DW solver advances by dt. + 4. nodes.head / depth / volume / outflow updated. + +Post-routing (SurfaceRouter2D::advancePostRouting): + 5. computeCouplingExchange — per coupling point, build orifice Q + with wet/dry ramp + surcharge gate + volume throttle, deposit + signed Q into ctx.nodes.coupling_inflow[ni] (× flow_2d_to_1d) + and into state.coupling_flux[ci] (m/s sink, negated). + 6. transferOutfallDischarges — push outfall outflow back into the + mesh as a positive coupling_flux source. + 7. updateRainfall — read ctx.gages.rainfall, convert to m/s, apply + any C-API rainfall overrides. + 8. cvode_solver_.advance(t, t+dt, state, mesh) — BDF + GMRES / + JACOBI inner solve, picks up coupling_flux + rainfall as + per-cell source terms. + 9. update_statistics + accumulateMassBalance. +``` + +Operator-splitting note: the 1D routing step in (2)–(4) uses the +*previous* step's `coupling_inflow`, while the 2D advance in (8) uses +the *current* step's `coupling_flux`. This is a first-order operator +split. Mass conservation is maintained because the signed Q is +recorded on both sides at the same instant (step 5), and the 1D side +only picks it up after a full `dt` has elapsed. + +--- + +## 7. Rainfall + +### 7.1 Phase 1: System Rainfall + +Each 2D triangle receives rainfall from the nearest rain gage (or a user-specified gage assignment): + +```cpp +// Simple: use first available gage's current rainfall for all cells +double rain_intensity = ctx.gages.rainfall[0]; // user units (in/hr or mm/hr) +double rain_m_per_s = convert_to_m_per_s(rain_intensity, ctx.options); + +for (int i = 0; i < mesh.n_triangles(); ++i) { + state.rainfall[i] = rain_m_per_s; +} +``` + +### 7.2 Future: Natural Neighbour Interpolation + +For spatially distributed rainfall across multiple gages: + +1. Compute Voronoi diagram of gage locations +2. For each triangle centroid, find its natural neighbour weights among gages +3. Interpolate rainfall intensity using natural neighbour weights + +This provides smooth, data-adaptive spatial interpolation that: +- Reproduces exact values at gage locations +- Provides C1-continuous interpolation between gages +- Adapts automatically to irregular gage spacing + +Implementation will use the gage coordinates from `ctx.spatial.gage_x/y` and rainfall from `ctx.gages.rainfall[]`. + +--- + +## 8. Integration into the Engine + +This section reflects how the 2D module is actually wired into +`SWMMEngine`. It does **not** match earlier revisions that placed +`MeshData` / `SurfaceStateData` directly on `SimulationContext`. + +### 8.1 Ownership + +2D state lives on a dedicated `openswmm::twoD::SurfaceRouter2D` +member of `SWMMEngine`, not on `SimulationContext`. The router owns: + +```cpp +class SurfaceRouter2D { + MeshData mesh_; + SurfaceStateData state_; + SolverOptions2D options_; + BoundaryData boundary_; + std::vector pending_bc_rows_; // parse-time scratch + std::vector coupling_points_; + bool active_ = false; +#ifdef OPENSWMM_HAS_2D + CvodeSurfaceSolver cvode_solver_; +#endif + // ... +}; +``` + +The mesh / state / options are exposed via `mesh()`, `state()`, +`options()`, `boundary()`, `pendingBCRows()` accessors so input +parsers and the C API can populate them by reference. + +`SimulationContext` carries no 2D-specific fields — it stays +unit-naïve and unaware of the 2D module. The 2D module reads from +`ctx.nodes`, `ctx.gages`, `ctx.options.flow_units`, and +`ctx.node_names`; it writes back into `ctx.nodes.coupling_inflow[]` +and `ctx.nodes.outfall_2d_head[]` (both pre-existing fields used by +SWMM's routing and outfall code). + +### 8.2 Section Registration + +`openswmm::twoD::register2DSections` (in +`src/engine/2d/input/SectionHandlers2D.cpp`) wires all seven `[2D_*]` +sections into the `DefaultInputPlugin` registry. The wrappers use a +`makeSectionHandler` lambda factory that tokenises each body line and +delegates to the per-line `parse2D…Line` functions, surfacing errors +through `ctx.error_message`. + +```cpp +#ifdef OPENSWMM_HAS_2D +if (auto* dip = dynamic_cast(input_plugin)) { + twoD::register2DSections(surface_router_.mesh(), + surface_router_.options(), + surface_router_.pendingBCRows(), + dip->registry()); +} +#endif +``` + +The registered set: `2D_OPTIONS`, `2D_VERTICES`, `2D_TRIANGLES`, +`2D_VERTEX_NODE_MAP`, `2D_TRIANGLE_NODE_MAP`, +`2D_BOUNDARY_CONDITIONS`, `2D_MESH_FILE`. `2D_MESH_FILE` only +captures the `FILE ` token into `options.mesh_file`; the +external file is loaded later (see §8.3 step 4). + +### 8.3 Engine Lifecycle — `SWMMEngine::open` + +Real call sequence under `#ifdef OPENSWMM_HAS_2D` (paraphrased from +`src/engine/core/SWMMEngine.cpp` ~L120–L210): + +``` +1. register2DSections(mesh_, options_, pending_bc_rows_, dip->registry()) +2. prescan2DUnitsHeader(inp_path, options_) // sets mesh_units_si +3. input_plugin->read(inp_path, ctx_) // parses all sections +4. if (options_.mesh_file non-empty): + load2DMeshExternalFile(mesh_, options_, pending_bc_rows_, + mesh_file, base_dir) + └─ prescan2DUnitsHeader(resolved_path, options_) // external overrides inline + └─ second InputReader pass on the resolved .2dm +5. resolve_cross_references(ctx_) +6. if (options_.output_file non-empty): + add_output_plugin(new Default2DOutputPlugin(resolved_path)) +``` + +### 8.4 Engine Lifecycle — `SurfaceRouter2D::initialize` + +Called from `SWMMEngine::initialize` after the 1D side is initialised +(`src/engine/2d/SurfaceRouter2D.cpp`): + +``` +1. Guard: if (n_vertices() < 3 || n_triangles() < 1) → active_ = false; return. +2. Compute unit bridge: + ft_to_m = (US FLOW_UNITS) ? 0.3048 : 1.0 + len_1d_to_2d = ft_to_m + len_2d_to_1d = 1.0 / ft_to_m + vol_1d_to_2d = ft_to_m^3 + flow_1d_to_2d = ft_to_m^3 + flow_2d_to_1d = 1.0 / ft_to_m^3 +3. Optional mesh ft→m scaling — only if (!mesh_units_si && len_1d_to_2d != 1): + vx, vy, vz *= ft_to_m + vert_coupling_area, tri_coupling_area *= ft_to_m^2 +4. buildMeshTopology(mesh_) // neighbours, edges, areas +5. validateMesh(mesh_) // throws on degenerate input +6. buildVertexStencils(mesh_) // pseudo-Laplacian weights +7. Resolve deferred coupling node names: + vert_coupled_node[v] = ctx.node_names.find(vert_coupled_node_name[v]) + tri_coupled_node[t] = ctx.node_names.find(tri_coupled_node_name[t]) + Unknown name throws. +8. state_.resize(n_triangles, n_vertices) +9. boundary_.resize(n_triangles * 3) // all WALL defaults +10. Drain pending_bc_rows_ into boundary_: + for each row → set edge_bc_type and the type-specific param + (slope for NORMAL_FLOW, head for SPECIFIED_STAGE, etc.) + Out-of-range rows silently skipped. +10a. §11A — drain pending_edge_conveyance_rows_ into + mesh_.edge_conveyance. Builds a one-shot vertex-pair → slot + map (O(n_triangles)), then writes each parsed factor into + every slot the (FROM, TO) pair resolves to (interior = 2 + slots, boundary = 1). Out-of-range vertices or non-existent + edges throw a runtime_error. +11. Set initial heads: state_.head[i] = mesh_.tri_cz[i] (dry bed) +12. coupling_points_ = buildCouplingPoints(mesh_, ctx) +13. For each coupled node: warn on sur_depth > 0 / ponded_area > 0 + and force ponded_area = 0 (C3a). +14. cvode_solver_.initialize(mesh_, state_, options_) +15. active_ = true +16. Seed 2D mass-balance init_storage from the initial cell volumes. +``` + +### 8.5 Engine Lifecycle — per step / finalize + +``` +step(ctx, dt, t): + 1. updateOutfallsPreRouting(ctx) + └─ (SWMM core runs assembleLateralInflows + DW routing here) + 2. advancePostRouting(ctx, dt, t) + └─ computeCouplingExchange + └─ transferOutfallDischarges + └─ updateRainfall + └─ cvode_solver_.advance(t, t+dt, state_, mesh_) + └─ state_.update_statistics + accumulateMassBalance + └─ state_.clear_reset_forcings() + +finalize(): + 1. cvode_solver_.finalize() + 2. active_ = false +``` + +--- + +## 9. Timestep Synchronization + +The 2D solver and the 1D SWMM solver operate on different timescales and must be carefully synchronized. CVODE internally sub-steps within the SWMM routing interval, while coupling exchange must remain consistent across both solvers. + +### 9.1 Two-Clock Architecture + +``` +SWMM clock: |----dt_swmm----|----dt_swmm----|----dt_swmm----| + t0 t1 t2 t3 + +CVODE clock: |--Δt--|--Δt--|--Δt--|-Δt-|--Δt--|--Δt--|--Δt--| + t0 t1 t2 + + CVODE sub-steps internally to reach each t_swmm boundary +``` + +- **SWMM routing step (`dt_swmm`):** Determined by `TimestepController::compute_next()` as the minimum of CFL, output boundary, control events, and user max step. Typically 1–30 seconds. +- **CVODE internal steps:** Variable-order, variable-step BDF steps taken internally by CVODE. CVODE is called with `CVode(cvode_mem, t_target, ...)` where `t_target = t_current + dt_swmm`. CVODE sub-steps as needed to meet error tolerances, but guarantees arrival at `t_target` exactly. + +### 9.2 Synchronization Modes + +Two modes are supported, controlled by `coupling_interval` in `[2D_OPTIONS]`: + +#### Mode A: Tight Coupling (default, `coupling_interval = 0`) + +The 2D solver advances **every SWMM routing step**. This is the most accurate but most expensive mode. + +``` +for each SWMM routing step dt_swmm: + 1. SWMM computes dt_swmm via TimestepController::compute_next() + 2. SWMM saves state, applies forcings, advances 1D routing by dt_swmm + 3. TimestepController::advance(ctx, dt_swmm) + 4. Read SWMM node heads at t_new + 5. Compute coupling exchange Q via orifice equation + 6. Update 2D rainfall from current gage state + 7. Set coupling_flux[] in 2D state (held constant over CVODE sub-steps) + 8. CVODE advances 2D from t to t + dt_swmm (internal sub-stepping) + 9. Inject exchange Q into nodes.coupling_inflow[ni] (× flow_2d_to_1d) + for next SWMM step's assembleLateralInflows drain + 10. If output_due: snapshot includes both 1D and 2D state +``` + +#### Mode B: Subcycled Coupling (`coupling_interval = N`) + +The 2D solver advances every `N` SWMM routing steps. Coupling exchange is computed once per `N` steps and held constant. This reduces computational cost for cases where the 2D domain evolves slowly relative to the pipe network. + +``` +coupling_counter = 0 + +for each SWMM routing step dt_swmm: + 1. SWMM routing step (normal) + 2. coupling_counter++ + 3. if coupling_counter >= N: + a. Compute accumulated dt_2d = sum of last N dt_swmm values + b. Read SWMM node heads + c. Compute coupling exchange + d. CVODE advances 2D from t to t + dt_2d + e. Inject exchange Q into nodes.coupling_inflow[ni] + (× flow_2d_to_1d) + f. coupling_counter = 0 +``` + +### 9.3 Coupling Exchange Timing — Operator Splitting + +The coupling uses **sequential operator splitting** (Lie splitting): + +``` +t_n → t_{n+1}: + Step 1: Advance SWMM 1D: y^{1D}_{n+1} = S_{1D}(dt, y^{1D}_n, Q^{2D→1D}_n) + Step 2: Read h^{1D}_{n+1} + Step 3: Compute Q^{exchange}_{n+1} = orifice(h^{2D}_n, h^{1D}_{n+1}) + Step 4: Advance 2D surface: y^{2D}_{n+1} = S_{2D}(dt, y^{2D}_n, Q^{exchange}_{n+1}) + Step 5: Set Q^{2D→1D}_{n+1} for next SWMM step +``` + +The exchange flow computed at Step 3 uses the **latest SWMM head** (post-routing) but the **previous 2D head** (pre-advance). This is first-order in time for the coupling but avoids implicit coupling iterations. For small `dt_swmm` (as enforced by CFL), the splitting error is small. + +**Alternative (future):** Strang splitting (half-step 1D → full-step 2D → half-step 1D) would give second-order coupling accuracy but requires two 1D half-steps per coupling cycle. + +### 9.4 Interaction with TimestepController + +The 2D solver does **not** modify `TimestepController::compute_next()`. Instead: + +1. **CFL from 2D** can optionally constrain `dt_swmm`. The 2D solver computes a CFL-like stability estimate: + ``` + dt_cfl_2d = min over all cells: (cell_diameter / max_wave_speed) + ``` + This is passed as an additional constraint: + ```cpp + double dt_cfl_1d = dynwave.compute_cfl_step(ctx); + double dt_cfl_2d = ctx.has_2d ? surface_router.compute_cfl_hint(ctx) : 1e30; + double dt_cfl = std::min(dt_cfl_1d, dt_cfl_2d); + double dt_next = TimestepController::compute_next(ctx, dt_cfl); + ``` + Note: Since CVODE handles its own sub-stepping adaptively, this CFL hint is **advisory** — it prevents the coupling interval from being too large, not the internal CVODE steps. + +2. **Output alignment** is unchanged. Both 1D and 2D states are snapshotted when `TimestepController::output_due()` returns true, since the 2D solver has already been advanced to the same time. + +3. **Simulation end** is unchanged. When `TimestepController::simulation_complete()` returns true, the 2D solver finalizes. + +### 9.5 CVODE Internal Stepping Details + +CVODE is configured to: + +```cpp +// Set CVODE to stop exactly at t_target (no overshooting) +CVodeSetStopTime(cvode_mem, t_target); + +// Advance — CVODE takes as many internal steps as needed +int flag = CVode(cvode_mem, t_target, y, &t_reached, CV_NORMAL); +// t_reached == t_target (guaranteed by SetStopTime) +``` + +Key CVODE settings: +- **Method:** BDF (backward differentiation formula) — appropriate for stiff systems +- **Max internal steps:** Configurable, default 500 per call (CVODE's default) +- **Min/max internal step size:** From `[2D_OPTIONS]` `MIN_TIMESTEP` / `MAX_TIMESTEP` +- **Order:** Dynamically adjusted by CVODE between 1 and 5 for optimal efficiency +- **Error control:** Per-cell relative + absolute tolerance + +During each CVODE internal step, the coupling flux and rainfall are **held constant** (they are frozen at the values computed at the start of the coupling interval). This is consistent with the operator-splitting approach. + +### 9.6 Mass Conservation at the Coupling Interface + +To ensure global mass conservation across the 1D↔2D boundary: + +``` +Volume removed from 2D = Volume added to 1D (and vice versa) +``` + +The same `Q_exchange` value (in m³/s on the 2D side) is: +- Subtracted from the 2D cell as `state.coupling_flux[i] += -Q / A_i` + (m/s sink) — see §6.2 step 6. +- Added to the SWMM node as + `nodes.coupling_inflow[j] += Q * opts.flow_2d_to_1d` (m³/s in SI + projects, ft³/s in US projects). On the next routing step, + `assembleLateralInflows` drains the signed value into + `routing_external` (positive part) and `routing_flooding` + (negative part, |Q|), preserving global continuity. + +Both use the same `dt_swmm` interval. The CVODE solver integrates the +coupling flux as a constant source/sink over its internal sub-steps, +which preserves the total volume exchange. `coupling_inflow[ni]` is +reset to zero by `computeCouplingExchange` itself at the start of each +coupling cycle for every coupled junction, so there is no double-count +between cycles. + +### 9.7 Handling Mismatched Timescales + +| Scenario | Behaviour | +|----------|-----------| +| 2D is stiff (small CVODE steps) | CVODE takes many internal sub-steps within `dt_swmm`. No impact on SWMM step. | +| 2D CFL < SWMM CFL | `dt_cfl_2d` constrains `dt_swmm` via `compute_next()`. Both solvers use the smaller step. | +| SWMM step limited by output boundary | 2D solver advances to the same output boundary. Both snapshots are synchronized. | +| SWMM step limited by control rules | 2D solver advances to the control-event time. Coupling is recomputed at the new state. | +| 2D goes dry everywhere | CVODE converges in 1–2 internal steps (trivial RHS). Minimal overhead. | +| Large 2D domain, small pipe network | Use `coupling_interval > 0` to subcycle. 2D advances less frequently. | + +--- + +## 10. Numerical Implementation Details + +### 10.1 Dry Cell Handling + +Cells with `depth < dry_depth` require special treatment to avoid division by zero in Manning's equation: + +```cpp +inline double diffusive_conductance(double depth, double mannings_n, + double grad_h_mag, double dry_depth) { + if (depth < dry_depth) return 0.0; + double denom = mannings_n * std::sqrt(std::max(grad_h_mag, 1e-12)); + return std::pow(depth, 2.0/3.0) / denom; +} +``` + +### 10.2 Upwind Flux Selection + +The upwind function `UW[]` selects the cell from which flow exits through the edge: + +```cpp +// For edge between cell L and cell R: +double h_L = state.head[L]; +double h_R = (R >= 0) ? state.head[R] : h_boundary; + +int upstream = (h_L >= h_R) ? L : R; +// Compute flux using upstream cell's depth and gradient +``` + +### 10.3 Slope Limiter (Jawahar-Kamath) + +The continuously differentiable limiter (Eq. [23]–[24]): + +```cpp +// g1, g2, g3 = squared L2 norms of unlimited gradients in cell and its neighbours +double eps2 = epsilon * epsilon; +double denom = g1*g1 + g2*g2 + g3*g3 + 3.0 * eps2; +double w1 = (g2*g3 + eps2) / denom; +double w2 = (g3*g1 + eps2) / denom; +double w3 = (g1*g2 + eps2) / denom; + +grad_lim_x = w1 * grad_u1_x + w2 * grad_u2_x + w3 * grad_u3_x; +grad_lim_y = w1 * grad_u1_y + w2 * grad_u2_y + w3 * grad_u3_y; +``` + +When all three gradients are equal, the weights reduce to 1/3 each (no limiting). + +### 10.4 C-Property Preservation + +To satisfy the C-property (still water on non-flat bed produces zero flux), reconstruct **total head** at edges, not depth: + +```cpp +double h_edge = h_center + r_dot_grad_h_limited; +double depth_edge = std::max(h_edge - z_edge, 0.0); +``` + +--- + +## 11. Future Extension Strategy + +### 11.1 Subsurface Flow (Richards' Equation) + +Add vertical prismatic layers below each triangle. The subsurface state vector extends the CVODE system: + +``` +y = [ψ_o(1), ..., ψ_o(N), ψ(1,1), ..., ψ(N,M)] +``` + +Where `ψ(i,m)` is the pressure head in triangle `i`, layer `m`. Coupling between surface and subsurface follows Eq. [4] and [14] from the paper. + +**Data structure:** Add `SubsurfaceStateData` with per-layer arrays following the same SoA pattern. + +### 11.2 Infiltration + +Per-cell infiltration models (Green-Ampt, Horton, SCS Curve Number) computed as a source/sink term in the surface ODE: + +``` +Q_infil[i] = infiltration_model(depth[i], soil_params[i], t) +net_source[i] = rainfall[i] - Q_infil[i] +``` + +**Data structure:** Add infiltration parameters to `MeshData` or a new `InfiltrationData2D` SoA struct. + +### 11.3 Evapotranspiration + +Per-cell ET as a sink term, using the system-level ET rate from SWMM options: + +``` +Q_et[i] = min(et_rate, depth[i] / dt) +``` + +### 11.4 Snowmelt + +Per-cell snowpack tracking following SWMM's existing snow model but applied to 2D cells. Would require a `SnowState2D` SoA struct. + +### 11.5 Anisotropic Roughness + +The formulation already supports full-tensor anisotropy (Eq. [28]–[30]). To enable: +- Add `aniso_k1`, `aniso_k2`, `aniso_angle` arrays to `MeshData` +- Modify flux calculation to use Eq. [30] instead of scalar conductance + +--- + +## 11A. Edge Conveyance Factor — Implemented + +Status: **IMPLEMENTED 2026-05-30.** Q1-Q6 resolved as follows (see +§11A.3 / §11A.10 for full discussion). This section is kept as the +authoritative reference for the feature; §1.9 documents the input +syntax, §2.1 documents the data field, and the C API surface is in +`include/openswmm/engine/openswmm_2d.h` as documented in §11A.7. + +| Q | Decision | +|---|----------| +| Q1 (range) | Strict `[0, 1]` clamp at parse + C-API time. Out-of-range raises an error. | +| Q2 (boundary edges) | Factor applies to non-Wall boundary edges once the V-E-FLUX slice lands. Today it has no effect on boundary edges (early-return in the flux loop). | +| Q3 (symmetry) | Silent partner-slot mirroring inside `SurfaceRouter2D::initialize`. | +| Q4 (order) | Conveyance multiplies LAST in `computeEdgeFluxes`, after the wet/dry Hermite shutoff. | +| Q5 (input format) | **5d** — SWMM-style `FROM_VERTEX TO_VERTEX CONVEYANCE` rows in `[2D_EDGE_CONVEYANCE]`. Order of From / To does not affect the value. | +| Q6 (C API) | Mutable at runtime: `swmm_2d_get / set / reset_edge_conveyance`. | + +This section lays out the design for a per-edge scalar in [0, 1] that +attenuates the diffusion-wave flux across that edge. The motivating +use case is "leaky" linear features in the floodplain: culverted +embankments, partially-permeable hedgerows, perforated fences, +buildings with garage openings, vegetation strips, internal weir +structures — any obstruction that reduces but does not eliminate +overland conveyance. + +### 11A.1 Naming choice + +"Leaky" is descriptive but unanchored to 2D-hydraulics convention. +Established names from the porosity-SWE literature (Sanders 2008; +Bruwier et al. 2017; Soares-Frazão; Guinot) and from groundwater / +civil-engineering practice: + +| Candidate | Notes | +|-----------|-------| +| **`edge_conveyance` factor** *(recommended)* | "Conveyance" is the standard civil-engineering term for the capacity of a section to carry flow (Manning's `K = (1/n)·A·R^(2/3)`). A multiplicative `[0,1]` factor reads naturally as "this edge carries `c × 100%` of its physics-derived conveyance". Default `1.0` is intuitively "unrestricted". | +| `edge_porosity` / `edge_transmissivity` ψ | Academic / IPSW (Integral-Porosity Shallow-Water) convention. `ψ = 1` is unobstructed; `ψ = 0` is wall. Most precedent in the literature, but the symbol ψ collides with the depth symbol already used in this doc. | +| `edge_permeability` | Groundwater analog; suggests dimensional permeability `k` (m²) which it is not. Misleading. | +| `edge_blockage` (= `1 − conveyance`) | Inverts the polarity. Natural for "this edge is X% blocked", but defaults flip to 0.0 instead of 1.0 and the multiplication site becomes `flux *= (1 − blockage)`. Less surgical. | +| `edge_attenuation` | Polarity-correct but ambiguous about whether it's the loss fraction or the surviving fraction. | + +**Recommendation:** adopt `edge_conveyance` as the field name, +`[2D_EDGE_CONVEYANCE]` as the section, `CONVEYANCE` as the per-row +token. Cross-reference the porosity-SWE term `ψ` in code comments for +academic readers. + +### 11A.2 What it is and is not + +| It IS | It IS NOT | +|-------|-----------| +| A static per-edge mesh property (set at parse time, immutable during a run unless the C API mutates it). | A boundary condition. The existing `[2D_BOUNDARY_CONDITIONS]` system handles domain-edge inflow/outflow types; the conveyance factor multiplies the *computed* interior flux. | +| Symmetric across the edge: if interior edge `(A,k)` is shared with `(B,k')`, both slots in the flat `[tri*3+edge]` array MUST carry the same factor — otherwise antisymmetry breaks and the FV scheme stops being conservative. | Direction-dependent. A 0.4 factor attenuates inflow and outflow identically. (Direction-asymmetric obstructions — e.g., flap-gated culverts — would require a separate signed-flux mechanism not in scope for this plan.) | +| Dimensionless, scaling the entire physical flux: `F_e_eff = c · F_e_physics`. | A discharge coefficient `C_d` on an orifice equation. The orifice equation is what `[2D_VERTEX_NODE_MAP]` uses for SWMM coupling (§6.2); this factor is for *overland* edge fluxes between cells. | +| Default 1.0 for every edge (no behavioural change unless the user opts in). | Required input. Sections, fields, and C API all stay backward-compatible. | + +### 11A.3 Decision questions for review + +These need answers before code starts (per CLAUDE.md §1). + +- **Q1. Range.** You said `[0, 1]`. Confirm `> 1` is also forbidden — i.e., the factor cannot *amplify* conveyance. (Amplification ≥ 1 would imply preferential channels carrying more than the Manning equation predicts, which would be unphysical without a separate calibration story. Recommend hard-clamping to [0, 1] with a parse-time error.) +- **Q2. Boundary edges.** Today boundary edges are handled by the BC system and the flux-calculator early-returns `0.0` at `nbr < 0` before any conveyance multiplication would run. Should the conveyance factor also apply to *non-Wall* boundary edges once the V-E-FLUX slice lands (so a partially-blocked outflow weir-edge has both a `SPECIFIED_FLOW` BC and a 0.3 conveyance)? Recommend YES for orthogonality, but the question deserves an explicit decision because it widens the cross-feature interaction surface. +- **Q3. Symmetry enforcement.** When the user declares `TRI=A EDGE=k CONVEYANCE=0.4`, should the parser (a) silently mirror the value to the partner slot on neighbour `B`, (b) require the user to declare both slots and error if they disagree, or (c) accept either and let `SurfaceRouter2D::initialize` fill in the partner from a "first-touch wins" rule? Recommend (a) — silent mirroring keeps the input concise and removes a footgun. Mention (b) as a pedantic-validation toggle if needed later. +- **Q4. Interaction with the wet/dry Hermite shutoff.** Today the shutoff multiplies `F_e` *before* the value is stored. The conveyance factor would multiply *after* the shutoff (or before — both give the same result mathematically, but the code order matters for clarity). Recommend applying conveyance as the last step before `state.edge_flux[idx] = F_e`. This keeps a `c = 0` edge bit-identical to a wall and the multiplication is visibly "the last thing that happens to F_e". +- **Q5. Input-format granularity.** A `TRI EDGE CONVEYANCE` row and a + `V_A V_B CONVEYANCE` row both have the shape `int int double`, so a + bare two-form dispatch is ambiguous (the early draft of this plan + got this wrong — see the discussion below). Three coherent options: + - **5a (recommend) — TRI/EDGE only.** Engine accepts only + `TRI EDGE CONVEYANCE`. Authoring an obstruction by polyline is + the GUI's job: it knows the mesh, walks the polyline, looks up + each shared interior edge, and emits the corresponding TRI/EDGE + rows. Engine parser stays trivial; no ambiguity possible. + - **5b — both forms with mandatory leading keyword.** Every row + starts with `TRI` or `V`: `TRI 42 1 0.40` or `V 17 18 0.30`. + Unambiguous, slightly more verbose, scales cleanly if a future + form (e.g., `EDGE_ID 123 0.40` for a global edge ID) is added. + - **5c — separate sections.** `[2D_EDGE_CONVEYANCE]` for the + triangle/edge form, `[2D_EDGE_CONVEYANCE_VERTEX]` for the + vertex-pair form. Each section's parser is unambiguous on its + own. Two section names for one feature is awkward but workable. + + > **Why "both, dispatched by parse result" does not work.** Almost + > every form-A row has `EDGE ∈ {0, 1, 2}`, which is also a valid + > vertex index in any non-trivial mesh, so a row like `42 1 0.40` + > could be either form. A row like `17 18 0.30` is unambiguously + > form B only because `EDGE = 18` is out of range for form A — but + > that signal only fires for the small minority of form-B rows + > whose second vertex happens to be > 2. Most rows collide. +- **Q6. C API.** Mutable through the C API like `coupling_inflow[]`, or read-only like static mesh geometry? Recommend mutable — supports time-varying obstructions (e.g., a flap-gate opening over the course of a storm) at near-zero implementation cost, since the value is just a multiplier looked up every flux evaluation. + +If any of Q1–Q6 lands differently, the §11A.4–11A.8 sections below +need to be revised before any code is written. + +### 11A.4 Proposed data model + +Single new field on `MeshData`, mirroring the existing edge SoA layout: + +```cpp +struct MeshData { + // ... existing fields ... + + // Per-edge conveyance factor in [0, 1] (default 1.0 = unrestricted). + // Flat 2D: [tri * 3 + edge_local]. Symmetric across interior edges — + // resize_triangles initialises to 1.0 and SurfaceRouter2D::initialize + // mirrors any user-declared partial conveyance to the matching slot + // on the neighbour triangle so interior flux antisymmetry is + // preserved (see §11A.6 mass-conservation argument). + // + // Cross-reference: corresponds to ψ in the Integral-Porosity SWE + // literature (Sanders 2008; Bruwier et al. 2017). + std::vector edge_conveyance; +}; +``` + +`resize_triangles(nt)` adds `edge_conveyance.assign(nt * 3, 1.0);` +right next to the existing `edge_length.resize(...)` line. + +No new struct, no new lifecycle method. The factor is mesh geometry, +so it belongs on `MeshData` rather than `BoundaryData` (which is for +edge boundary conditions) or `SurfaceStateData` (which is for time- +varying state). + +### 11A.5 Proposed input format + +New optional section `[2D_EDGE_CONVEYANCE]`. Parsed by a new +`parse2DEdgeConveyanceLine` function in +`src/engine/2d/input/SectionHandlers2D.cpp`, registered the same way +as the other `[2D_*]` sections in `register2DSections` and +`load2DMeshExternalFile`. Per Q5 → 5d, the row format mirrors SWMM +`[CONDUITS]` `From-Node` / `To-Node` convention. + +``` +;; Per-edge conveyance multiplier in [0, 1]. Default 1.0 (unrestricted) +;; for every interior edge that is NOT listed here. Multiplies the +;; diffusion-wave flux across the edge. +;; +;; FROM_VERTEX / TO_VERTEX is the (unordered) pair of mesh vertex +;; indices at the edge's endpoints — the conveyance is direction- +;; symmetric, so swapping FROM and TO does not change the value. +;; +;; Authoring obstructions as a polyline (a hedgerow that crosses many +;; edges) is the GUI's job: walk the polyline, emit one row per shared +;; mesh edge. +[2D_EDGE_CONVEYANCE] + +;; FROM_VERTEX TO_VERTEX CONVEYANCE +;; ----------- --------- ---------- + 17 18 0.40 ;; hedgerow segment + 18 19 0.40 + 42 87 0.30 ;; culverted embankment + 55 61 0.00 ;; fully blocked → equivalent to a Wall +``` + +Parser rules (5d): + +- Exactly three tokens per row: `FROM_VERTEX TO_VERTEX CONVEYANCE`. +- `FROM_VERTEX` and `TO_VERTEX` must be non-negative integers in + `[0, n_vertices)` and must differ. Out-of-range or equal vertices + raise a clear error at parse time (range) or at + `SurfaceRouter2D::initialize` time (vertex-pair does not form a + real mesh edge). +- `CONVEYANCE` must parse as a `double` in `[0.0, 1.0]` (Q1, strict + clamp at parse time); out-of-range raises a clear error. +- Vertex-pair resolution (Q3 silent mirror) happens in + `SurfaceRouter2D::initialize`: an edge-key hash map built in one + O(n_triangles) pass over the mesh maps `(min(v_from, v_to), + max(v_from, v_to))` → list of `(tri, edge_local)` slot indices. + An interior edge resolves to two slots (one in each adjacent + triangle); a boundary edge resolves to one. The factor is written + to every matching slot. +- Duplicate rows naming the same edge: last-write-wins, with a + one-shot warning per duplicate. +- A vertex-pair that does not form a mesh edge raises a fatal + init-time error (the obstruction reference is wrong; fail loudly + rather than silently dropping). + +**Alternative parsers (not implemented, retained for reference).** + +- **5a (TRI / EDGE explicit).** `TRI EDGE CONVEYANCE` per row. Author + must know triangle indices. +- **5b (leading keyword).** First token is `TRI` or `V`; rest of the + row is ` ` interpreted accordingly. +- **5c (separate sections).** Sibling + `[2D_EDGE_CONVEYANCE]` (TRI/EDGE) and + `[2D_EDGE_CONVEYANCE_VERTEX]` (V_A/V_B) sections. + +### 11A.6 Proposed math integration + +Single insertion in `SurfaceFluxCalculator::computeEdgeFluxes` — +multiply once, immediately before the flux is stored: + +```cpp +// Cubic Hermite wet/dry shutoff on the source-side depth (unchanged) … +if (depth_up < opts.dry_depth) { + double t = depth_up / opts.dry_depth; + F_e *= t * t * (3.0 - 2.0 * t); +} + +// NEW — per-edge conveyance factor. No-op when c == 1.0. +// +// Mass-conservation argument: +// - For an interior edge shared by cells A and B, this loop writes +// edge_flux[A*3 + e_A] and edge_flux[B*3 + e_B] independently. +// The two slots compute the SAME F_e (up to sign) because the FV +// scheme uses centroid-to-centroid Δh / Δx, which is antisymmetric +// in (A ↔ B). Multiplying BOTH slots by the SAME edge_conveyance +// preserves antisymmetry → no spurious mass source. +// - SurfaceRouter2D::initialize is responsible for the "same factor +// in both slots" invariant (§11A.4 mirroring). computeEdgeFluxes +// trusts it and does not re-look up the partner. +// - c == 0 → F_e == 0, identical to the boundary early-return. An +// interior edge with conveyance 0 is a wall in everything but its +// storage location (still in the interior edge list, still has a +// neighbour, but carries no flux). +F_e *= mesh.edge_conveyance[idx]; + +state.edge_flux[idx] = F_e; +``` + +That is the entire mathematical change. The downstream consumers +(`assembleRHS`, `face_vx / face_vy` reconstruction, +`cell_continuity_err`, `edge_bc_cum_flux`) see the attenuated flux +and integrate it correctly without further modification. + +CVODE Jacobian: the diffusion-wave Jacobian is currently approximated +by GMRES Jacobian-vector products with no explicit assembly. A +per-edge constant multiplier does not change the sparsity pattern, +just the coefficient, so the existing Krylov + JACOBI preconditioner +keeps working. The conditioning is *better* on heavily-blocked +meshes (smaller off-diagonal entries → more diagonally dominant). + +### 11A.7 Proposed C API surface + +Three new functions in `include/openswmm/engine/openswmm_2d.h`, +matching the style of the existing `swmm_2d_set_coupling_*` family: + +```c +/* Read the per-edge conveyance factor for one edge. + * tri ∈ [0, n_triangles), edge ∈ {0,1,2}. + * Returns 1.0 for out-of-range arguments and emits a warning. + */ +double swmm_2d_get_edge_conveyance(SWMM_Engine eng, int tri, int edge); + +/* Set the per-edge conveyance factor for one edge. + * Value is clamped to [0, 1]; out-of-range arguments are no-ops with + * a warning. When the edge is interior (has a neighbour) the value + * is mirrored to the partner slot so antisymmetry is preserved. + * Safe to call between routing steps; calling DURING a routing step + * is undefined (the CVODE sub-stepper holds a const reference). + */ +int swmm_2d_set_edge_conveyance(SWMM_Engine eng, int tri, int edge, double c); + +/* Bulk reset: every edge → 1.0. O(n_triangles). */ +int swmm_2d_reset_edge_conveyance(SWMM_Engine eng); +``` + +Cython / Python bindings follow the existing pattern in +`python/openswmm/engine/_2d.pxd / _2d.pyx`. + +### 11A.8 Validation plan + +| Step | Verification | +|------|--------------| +| 1. `edge_conveyance` defaults to 1.0 after `resize_triangles` | Unit test on `MeshData` directly. | +| 2. Parser round-trips form A and form B | Unit test on `parse2DEdgeConveyanceLine` with both syntaxes; assert the same internal state. | +| 3. Out-of-range value → parse error | Test with `CONVEYANCE = -0.1` and `CONVEYANCE = 1.5`; assert non-empty error. | +| 4. Mirroring across interior edges | After `SurfaceRouter2D::initialize`, walk every interior edge and assert `edge_conveyance[A*3+e_A] == edge_conveyance[B*3+e_B]`. | +| 5. `c = 0` ≡ Wall | Generate a test case where one interior edge is fully blocked; compare against the same case with that edge moved to the boundary (`tri_nbr = -1`). Depth and flux fields must agree to solver tolerance. | +| 6. Partial attenuation | Steady-state diffusion problem with an analytical solution; verify the flux through a `c = 0.5` edge is exactly half the unattenuated reference. | +| 7. Mass balance | A 12-hour rainfall run with a mixed-conveyance domain (some edges at 1.0, some at 0.3, some at 0.0); assert `accumulateMassBalance` continues to balance within the existing tolerance (no spurious source / sink from antisymmetry breaking). | +| 8. C API round-trip | `set` → `get` returns the clamped value; `set` on an interior edge mirrors to the partner. | + +### 11A.9 Affected files (forecast) + +If Q1–Q6 land per recommendation: + +- `src/engine/2d/data/MeshData.hpp` — add `edge_conveyance` vector, init in `resize_triangles`. +- `src/engine/2d/input/SectionHandlers2D.hpp / .cpp` — add `parse2DEdgeConveyanceLine`; register in `register2DSections` and `load2DMeshExternalFile`. +- `src/engine/2d/SurfaceRouter2D.cpp` — mirror form-A / form-B declarations to partner slots inside `initialize()`, immediately after `buildMeshTopology` (the neighbour table is then available). +- `src/engine/2d/solver/SurfaceFluxCalculator.cpp` — one-line multiplication before `state.edge_flux[idx] = F_e`. +- `include/openswmm/engine/openswmm_2d.h` + impl — three new C API entry points. +- `python/openswmm/engine/_2d.pxd / _2d.pyx` — Cython / Python bindings. +- `tests/` — unit + verification + mass-balance tests per §11A.8. +- `docs/2dModelStrategy.md` §1 — once approved, promote this plan into §1.9 `[2D_EDGE_CONVEYANCE]` and a §2.5 `MeshData` field; flip the status banner above to "implemented". +- `docs/2d_external_mesh_file.md` — extend the affected-sections list with `[2D_EDGE_CONVEYANCE]`. + +Estimated diff size: ~150 lines of engine source, ~80 lines of tests, +~60 lines of docs. No new dependencies. + +### 11A.10 Decision log + +Q1-Q6 resolved 2026-05-30 — see status banner at the top of §11A. +Q5 changed from the original 5a recommendation to 5d after the +ambiguity in the bare two-form dispatch was identified and the +SWMM-style `FROM_VERTEX` / `TO_VERTEX` convention was proposed as a +cleaner alternative. + +--- + +## 12. Testing Strategy + +### 12.1 Unit Tests + +| Test | Validates | +|------|-----------| +| `test_mesh_builder` | Topology, neighbours, edge normals, areas | +| `test_vertex_reconstruction` | Pseudo-Laplacian weights sum to 1, linear exactness | +| `test_gradient_calculation` | Green-Gauss gradients exact for linear fields | +| `test_slope_limiter` | Reduces to 1/3 weights for uniform gradients | +| `test_diffusive_conductance` | Correct K(ψ) values, dry cell handling | +| `test_orifice_coupling` | Correct Q for various head differences | +| `test_backflow_prevention` | Zero flow when flap gate active and h_swmm > h_2d | +| `test_input_parsing` | Correct parse of all 2D sections | +| `test_section_registration` | Custom sections registered and dispatched | + +### 12.2 Verification Tests (from Kumar et al., 2009) + +| Test | Reference | Description | +|------|-----------|-------------| +| Still water (C-property) | — | Zero flux on non-flat bed with uniform head | +| Tilted plane | Analytical | Steady-state flow on constant-slope plane | +| Dam break | Analytical | 1D dam break on flat bed (Ritter solution) | +| Abdul & Gillham (1984) | Lab data | Coupled hillslope surface-subsurface flow (Phase 2) | + +### 12.3 Benchmark Tests + +| Benchmark | Purpose | +|-----------|---------| +| `bench_flux_calculation` | Throughput of edge flux computation | +| `bench_vertex_reconstruction` | Stencil evaluation performance | +| `bench_cvode_advance` | Full solver step timing | + +--- + +## 13. Lateral Exchange — Uncapped Nodes and Surcharge Feedback + +### 13.1 Problem Statement + +When the 1D pipe network surcharges, water rises above the node crown and "caps" at the ground surface in a conventional SWMM simulation (ponding or flooding). With a coupled 2D surface model, **uncapped nodes** must allow bidirectional exchange: surcharge water spills onto the 2D surface, and 2D overland flow can drain back into the pipe network when capacity is available. + +The challenge is ensuring **consistent, mass-conservative, numerically stable feedback** between the 1D node head (which can exceed ground elevation during surcharge) and the 2D surface head at the coupling point. + +### 13.2 Node Classification for 2D Coupling + +Each coupled SWMM node falls into one of these categories: + +| Category | Condition | 2D Exchange Behaviour | +|----------|-----------|----------------------| +| **Sub-surface** | `h_1D < z_ground` | Normal orifice exchange; 2D surface can drain into node | +| **At-grade** | `h_1D ≈ z_ground` | Transition zone; exchange approaches zero as heads equalize | +| **Surcharged (uncapped)** | `h_1D > z_ground` | Surcharge spills onto 2D surface; bidirectional exchange | +| **Flooded (capped, no 2D)** | `h_1D > z_ground`, no 2D coupling | Legacy SWMM flooding/ponding (unchanged) | + +For coupled nodes, the `ponded_area` parameter is effectively replaced by the 2D surface domain — water that would pond in 1D instead flows onto the 2D mesh. + +### 13.3 Uncapped Node Exchange Equation + +The orifice exchange equation from §6.2 is extended for uncapped nodes: + +``` +Q_exchange = C_d · A_eff(h) · sign(Δh) · sqrt(2g · |Δh|) +``` + +Where `A_eff(h)` is a **head-dependent effective area** that transitions smoothly between regimes: + +```cpp +double effective_area(double h_1d, double h_2d, double z_ground, + double z_invert, double A_inlet, double A_manhole) { + double h_max = std::max(h_1d, h_2d); + + if (h_max < z_ground) { + // Sub-surface: flow through inlet grate/opening + return A_inlet; + } else { + // Surcharged: full manhole opening area + // Smooth transition over a small depth range + double d_trans = 0.05; // 5 cm transition depth + double frac = std::min((h_max - z_ground) / d_trans, 1.0); + return A_inlet + frac * (A_manhole - A_inlet); + } +} +``` + +### 13.4 Surcharge Spill Dynamics + +When `h_1D > z_ground + ψ_2D` (1D surcharge head exceeds 2D surface head): + +1. **Spill flow** is computed via the orifice equation with `Δh = h_1D - h_2D` +2. The spill is injected as a **negative coupling flux** into the 2D cell: `coupling_flux[i] = +Q / A_tri` +3. The same flow is removed from the 1D node via `forcing.node_lat_inflow -= Q` + +When `h_2D > h_1D` (2D surface head exceeds 1D node head): + +1. **Return flow** drains from 2D surface back into the pipe network +2. Subject to capacity: if the node is full (`volume >= full_volume`), return flow is throttled +3. Throttling: `Q_return = min(Q_orifice, (full_volume - volume) / dt)` + +### 13.5 Ponding Suppression for Coupled Nodes + +For nodes coupled to the 2D domain, the engine must **suppress the default SWMM ponding/flooding behaviour**: + +```cpp +// In initNodeFlows(), skip overflow computation for 2D-coupled nodes +if (is_2d_coupled[i]) { + nodes.overflow[i] = 0.0; // 2D surface handles the excess + // Do NOT cap depth at full_depth for coupled nodes +} else { + // Standard SWMM overflow logic + if (nodes.volume[ui] > nodes.full_volume[ui] && dt > 0.0) { + nodes.overflow[ui] = (nodes.volume[ui] - nodes.full_volume[ui]) / dt; + } +} +``` + +This is critical: without suppression, the 1D solver would flood water that should instead exchange with the 2D surface, causing double-counting. + +### 13.6 Head Clamping Strategy + +To prevent numerical instability during surcharge, the 1D node head for coupled nodes is allowed to **exceed `z_ground + full_depth`** — the 2D surface acts as the "cap" instead of the ponded area: + +```cpp +// In DWSolver: for 2D-coupled surcharged nodes, do NOT clamp head +if (is_2d_coupled[node_idx]) { + // Head is free to rise — 2D coupling will drain excess + // Still enforce a safety maximum to prevent runaway + double safety_max = z_ground + 2.0 * full_depth; + nodes.head[node_idx] = std::min(nodes.head[node_idx], safety_max); +} +``` + +--- + +## 14. Outfall Boundary Feedback with 2D Surface + +### 14.1 Problem Statement + +Outfall nodes define downstream boundary conditions for the 1D pipe network. When a 2D surface domain is present, **outfalls at the domain boundary** must account for the 2D water level as a dynamic boundary condition rather than using a fixed or tidal stage. + +### 14.2 Outfall Types and 2D Interaction + +| Outfall Type | Without 2D | With 2D Coupling | +|-------------|-----------|-----------------| +| **FREE** | `h = z + min(yNorm, yCrit)` | 2D head at outfall vertex; if 2D head > critical depth, use 2D head as tailwater | +| **NORMAL** | `h = z + yNorm` | Same, but check if 2D head creates backwater exceeding normal depth | +| **FIXED** | `h = z_fixed` | Max of fixed stage and 2D surface head (2D can raise tailwater above fixed stage) | +| **TIDAL** | `h = tidal_curve(t)` | Max of tidal stage and 2D head (tidal flooding propagates through 2D) | +| **TIMESERIES** | `h = ts(t)` | Max of timeseries stage and 2D head | + +### 14.3 Dynamic Tailwater from 2D Surface + +For each outfall coupled to a 2D vertex or triangle: + +```cpp +void setOutfallDepthWith2D(SimulationContext& ctx, int outfall_idx, + const MeshData& mesh, const SurfaceStateData& state) { + auto& nodes = ctx.nodes; + int vert_idx = outfall_2d_vertex[outfall_idx]; // -1 if not coupled + + if (vert_idx < 0) { + // No 2D coupling — use standard outfall logic + outfall::setOutfallDepth(ctx, outfall_idx, ctx.current_date); + return; + } + + // Get 2D surface head at the outfall coupling point + double h_2d = state.vert_head[vert_idx]; + double z_inv = nodes.invert_elev[outfall_idx]; + + // Compute standard outfall depth (without 2D) + double h_standard = computeStandardOutfallHead(ctx, outfall_idx); + + // The effective boundary head is the MAXIMUM of standard and 2D + // This ensures the 2D surface can raise the tailwater (backwater effect) + // but cannot lower it below the standard boundary condition + double h_effective = std::max(h_standard, h_2d); + + nodes.depth[outfall_idx] = std::max(h_effective - z_inv, 0.0); + nodes.head[outfall_idx] = z_inv + nodes.depth[outfall_idx]; +} +``` + +### 14.4 Backflow Prevention at Outfalls + +When a 2D-coupled outfall has a **flap gate**, the gate prevents backflow from the 2D surface into the pipe network: + +```cpp +if (nodes.outfall_has_flap_gate[outfall_idx] && h_2d > h_pipe) { + // Flap gate closed: no backflow from 2D → pipe + // Outfall acts as a wall boundary for the pipe network + // The 2D surface still receives spill from uncapped upstream nodes + Q_exchange = 0.0; + + // Set outfall depth to critical/normal (independent of 2D) + nodes.depth[outfall_idx] = computeStandardOutfallDepth(ctx, outfall_idx); +} +``` + +Without a flap gate, the outfall allows **bidirectional flow**: +- **Pipe → 2D**: Normal pipe discharge enters the 2D surface domain +- **2D → Pipe**: High 2D water levels push water back into the pipe (backwater effect) + +### 14.5 Mass Balance at Outfall Boundaries + +Outfall coupling must preserve mass balance across the 1D↔2D boundary: + +``` +Volume leaving pipe at outfall = Volume entering 2D at outfall vertex/triangle +``` + +The outfall discharge `Q_outfall` (computed by the 1D solver based on boundary head) becomes a **positive source** in the 2D cell containing the outfall coupling point: + +```cpp +// After 1D routing step, transfer outfall discharge to 2D +for (auto& ocp : outfall_coupling_points) { + double Q_pipe = computeOutfallDischarge(ctx, ocp.outfall_idx); + + // Inject pipe outflow as source into 2D cell + state.coupling_flux[ocp.cell_idx] += Q_pipe / mesh.tri_area[ocp.cell_idx]; + + // Track in mass balance + ctx.mass_balance.outfall_to_2d_volume += Q_pipe * dt; +} +``` + +### 14.6 Outfall Coupling Sequence + +``` +Per SWMM routing step: + +1. Read 2D surface heads at all outfall coupling points +2. Compute effective outfall boundary heads: + h_boundary = max(h_standard, h_2d) [unless flap gate blocks backflow] +3. Set outfall depths using effective heads (before 1D routing) +4. Run 1D routing with updated outfall boundaries +5. Compute outfall discharges from 1D solution +6. Inject outfall discharges into 2D coupling cells +7. Advance 2D solver (outfall cells receive pipe discharge as source) +8. Repeat +``` + +This creates a **feedback loop**: 2D surface levels influence outfall boundary conditions → outfall boundaries affect pipe flows → pipe flows discharge into 2D → 2D levels change → next step boundary conditions update. + +--- + +## 14A. Continuity Diagnostics and Max-Value Rendering Envelopes + +This section is the authoritative description of how the 2D module tracks +**local** and **global** continuity error and how it tracks and persists the +**maximum depth** and **maximum velocity** envelopes used to render flood maps. +It consolidates behaviour that is partly already wired (local residual, global +mass balance, max depth) and specifies the remaining pieces (max-velocity +envelope, HDF5 persistence of envelopes, HDF5 persistence of the global +balance) so the whole feature is described in one place. + +### 14A.1 What already exists vs. what this section adds + +| Quantity | Tracked today | Persisted for rendering today | This section adds | +|----------|---------------|-------------------------------|-------------------| +| **Local continuity** (per cell) | ✅ `state_.cell_continuity_err[]` via `computeCellContinuity` (§8.5) | ✅ per-step `/Mesh2_face_continuity_err` | Cumulative `stat_max_cont_err[]` envelope + `/Mesh2_face_max_continuity_err` | +| **Global continuity** (domain) | ✅ `ctx.mass_balance_2d` + `MassBalance2D::error()` | ❌ report only (`DefaultReportPlugin`) | `/mass_balance_2d` HDF5 group + scalar `continuity_error` | +| **Max depth** (per cell) | ✅ `state_.stat_max_depth[]` via `update_statistics` | ❌ not in HDF5 | `/Mesh2_face_max_depth` envelope dataset | +| **Max velocity** (per cell) | ❌ not tracked | ❌ | `state_.stat_max_velocity[]` + `/Mesh2_face_max_velocity` | + +The design principle is **no new full passes over the mesh and no new time +dimension**: every envelope folds into the per-cell loop already walked by +`update_statistics`, and every envelope is stored as a single `[nFace]` +(time-invariant) HDF5 dataset that is overwritten in place rather than appended. + +### 14A.2 Local continuity residual (per cell) + +Unchanged from §8.5 — documented here for completeness. After CVODE accepts the +step, `computeEdgeFluxes` refreshes the edge fluxes at the accepted +`(head, depth)`, then `computeCellContinuity(mesh_, state_, dt)` writes the +discrete residual + +``` +cell_continuity_err[i] = + A_i · (depth[i] − old_depth[i]) / dt // storage change + + Σ_edges edge_flux[i, e] · edge_length[i, e] // net efflux (m³/s) + − net_source[i] · A_i // rainfall + coupling source +``` + +into `state_.cell_continuity_err[i]` (m³/s; ≈ 0 for a conservative scheme). +This is already streamed to the HDF5 time series as `/Mesh2_face_continuity_err` +(see §14A.5), so the GUI can animate the instantaneous local imbalance and +locate cells where the limiter or the wet/dry treatment is leaking volume. + +### 14A.3 Global continuity (domain mass balance) + +Unchanged in mechanism from §9.6 — the per-step accumulation already lives in +`SurfaceRouter2D::accumulateMassBalance` and writes into the +`ctx.mass_balance_2d` struct (`SimulationContext.hpp`): + +```cpp +struct MassBalance2D { + double init_storage, final_storage; // m³ + double rainfall_in; // m³ + double coupling_1d_to_2d_in, coupling_2d_to_1d_out; + double outfall_in; + double boundary_in, boundary_out; + bool active; + double error() const { // fraction in [-1, 1] + double in = rainfall_in + coupling_1d_to_2d_in + outfall_in + + boundary_in + init_storage; + double out = coupling_2d_to_1d_out + boundary_out + final_storage; + return (in > 0.0) ? (in - out) / in : 0.0; + } +}; +``` + +**The only gap is persistence.** Today these terms reach the user only through +`DefaultReportPlugin` (the text `.rpt`). For rendering and post-processing they +must also land in the HDF5 file. Because the terms are scalars known in full at +the end of the run, they are written **once**, in +`Default2DOutputPlugin::finalize(const SimulationContext& ctx)` — which already +receives `ctx` — into a small `/mass_balance_2d` group: + +``` +/mass_balance_2d (group) + ├─ init_storage scalar double (m³) + ├─ final_storage scalar double (m³) + ├─ rainfall_in scalar double (m³) + ├─ coupling_1d_to_2d_in scalar double (m³) + ├─ coupling_2d_to_1d_out scalar double (m³) + ├─ outfall_in scalar double (m³) + ├─ boundary_in scalar double (m³) + ├─ boundary_out scalar double (m³) + └─ @continuity_error attribute double (fraction, = ctx.mass_balance_2d.error()) +``` + +This is O(1) work at finalize, no per-step cost, and keeps the binary file +self-describing for the same balance the report prints. + +### 14A.4 Max depth and max velocity envelopes (per cell) + +Two new cumulative arrays join the existing `stat_max_depth` / +`stat_cum_volume` in `SurfaceStateData` (§2.2): `stat_max_velocity` and +`stat_max_cont_err`. All envelope updates fuse into the **single** per-cell loop +that `update_statistics` already runs, so the only added cost is two `max` +comparisons and one `sqrt` per cell per coupling step: + +```cpp +void SurfaceStateData::update_statistics(const std::vector& tri_area, + double dt) noexcept { + for (std::size_t i = 0; i < depth.size(); ++i) { + if (depth[i] > stat_max_depth[i]) stat_max_depth[i] = depth[i]; + + // face_vx/face_vy were refreshed by computeFaceVelocity earlier in + // step() (§8.5), so the speed magnitude below is the accepted value. + const double speed = std::sqrt(face_vx[i]*face_vx[i] + + face_vy[i]*face_vy[i]); + if (speed > stat_max_velocity[i]) stat_max_velocity[i] = speed; + + const double aerr = std::abs(cell_continuity_err[i]); + if (aerr > stat_max_cont_err[i]) stat_max_cont_err[i] = aerr; + + stat_cum_volume[i] += depth[i] * tri_area[i] * dt; + } +} +``` + +**Ordering guarantee (critical).** In `SurfaceRouter2D::step` the call sequence +is already `computeEdgeFluxes → computeCellContinuity → computeFaceVelocity → +update_statistics → accumulateMassBalance` (§8.5). `update_statistics` therefore +sees the accepted, post-step `face_vx/vy` and `cell_continuity_err`; **no +reordering is required**, and the envelopes never sample a Newton/JvP +perturbation state. `resize()` must `assign(nt, 0.0)` the two new arrays +alongside the existing ones. + +**Sampling resolution — fixed at the model (routing) timestep.** Envelopes are +aggregated once per **SWMM model/routing step** (`dt = dt_swmm`), at the +accepted end-of-step state. They are **never** sampled at CVODE internal +sub-steps: the envelope update lives only in `update_statistics`, called once +per `SurfaceRouter2D::step` (§8.5), and nothing in the CVODE right-hand-side +callback touches the `stat_*` arrays. This is a deliberate, final design +choice — it keeps the envelope cost at one fused per-cell pass per model step +and avoids any per-RHS-evaluation overhead. + +### 14A.5 Efficient HDF5 persistence of the envelopes + +The per-step time series (`/Mesh2_face_depth`, `/Mesh2_face_vx`, … , +`/Mesh2_face_continuity_err`) is unchanged. Envelopes are added as **fixed +`[nFace]` datasets** — no unlimited time dimension, no chunk extension: + +``` +/Mesh2_face_max_depth [nFace] double, units "m" +/Mesh2_face_max_velocity [nFace] double, units "m s-1" +/Mesh2_face_max_continuity_err [nFace] double, units "m3 s-1" +``` + +Each carries the standard UGRID face attributes (`mesh="Mesh2"`, +`location="face"`, `long_name`, `units`) so a UGRID-aware viewer (QGIS Crayfish, +ParaView) renders them as static result layers. + +Two write strategies were considered: + +1. **Write once at `finalize`.** Cleanest, but `Default2DOutputPlugin::finalize` + only receives `SimulationContext`, not the `SurfaceStateData` arrays, so it + would need a new engine→plugin handoff. +2. **Overwrite in place each output step** *(chosen)*. Carry the three envelope + arrays in `SimulationSnapshot` (filled by `fillSurfaceSnapshot`, like the + other `surface_*` fields) and, in `Default2DOutputPlugin::update`, `H5Dwrite` + the full `[nFace]` slab over the fixed dataset. Because the envelopes are + monotone, the **last** write is the final envelope; an aborted run still + leaves a valid, up-to-date envelope on disk. + +Strategy 2 is chosen: it reuses the existing snapshot/IO-thread path (no new +cross-thread handoff, no main-thread/IO-thread race), and its cost is +`3 × nFace` doubles overwritten per output step — negligible next to the ~12 +time-series face arrays already appended each step, and far cheaper than the +3-D `[nTime, nFace, 3]` edge-flux dataset already being written. + +Datasets are created in `prepareMeshAndDatasets` (fixed, non-unlimited) and +closed in `finalize` alongside the existing `ds_face_*` handles. New members on +`Default2DOutputPlugin`: `ds_face_max_depth_`, `ds_face_max_velocity_`, +`ds_face_max_continuity_err_`. + +### 14A.6 Snapshot plumbing + +`SimulationSnapshot` (consumed on the IO thread) gains three arrays, populated +in `SWMMEngine::fillSurfaceSnapshot` next to the existing assignments: + +```cpp +snap.surface_stat_max_depth = st.stat_max_depth; +snap.surface_stat_max_velocity = st.stat_max_velocity; +snap.surface_stat_max_cont_err = st.stat_max_cont_err; +``` + +These are SI-native and **must not** pass through `convertSnapshotToDisplay` +(the 1D-only display conversion in `SWMMEngine.cpp`), matching how the other +`surface_*` fields are treated. + +### 14A.7 C API surface (`openswmm_2d.h`) + +Bulk getters mirror the existing `swmm_2d_get_stat_max_depths` so an external +driver can read the live envelope without the HDF5 file: + +```c +/** @brief Per-triangle max velocity magnitude envelope (m/s). Cumulative. */ +SWMM_ENGINE_API int swmm_2d_get_stat_max_velocities(SWMM_Engine engine, + double* max_velocities); + +/** @brief Per-triangle max |continuity residual| envelope (m³/s). Cumulative. */ +SWMM_ENGINE_API int swmm_2d_get_stat_max_continuity_err(SWMM_Engine engine, + double* max_errs); + +/** @brief Global 2D surface continuity error (fraction = mass_balance_2d.error()). */ +SWMM_ENGINE_API int swmm_2d_get_continuity_error(SWMM_Engine engine, double* err); + +/** @brief Global 2D mass-balance terms (all m³). Any out-pointer may be NULL. */ +SWMM_ENGINE_API int swmm_2d_get_mass_balance(SWMM_Engine engine, + double* init_storage, + double* final_storage, + double* rainfall_in, + double* coupling_1d_to_2d_in, + double* coupling_2d_to_1d_out, + double* outfall_in, + double* boundary_in, + double* boundary_out); +``` + +### 14A.8 Python binding (`_2d.pxd` / `_2d.pyx` / `_2d.pyi`) + +`.pyi` stub additions (epytext docstrings, NumPy-typed returns, matching the +existing `get_stat_max_depths` / `max_depth` style): + +```python +def get_stat_max_velocities(self) -> npt.NDArray[np.float64]: + """ + Per-triangle cumulative maximum velocity magnitude envelope. + + @return: Max cell speed |v| seen at each triangle over the run (m/s). + @rtype: numpy.ndarray[float64], shape (n_triangles,) + """ + +def get_stat_max_continuity_err(self) -> npt.NDArray[np.float64]: + """ + Per-triangle cumulative maximum |continuity residual| envelope. + + @return: Worst-case local mass-balance residual at each triangle (m³/s). + @rtype: numpy.ndarray[float64], shape (n_triangles,) + """ + +@property +def continuity_error(self) -> float: + """ + Global 2D surface continuity error. + + @return: (total_in − total_out) / total_in, the domain mass-balance error. + @rtype: float + """ + +def get_mass_balance(self) -> dict[str, float]: + """ + Global 2D mass-balance terms. + + @return: Mapping with keys C{init_storage}, C{final_storage}, + C{rainfall_in}, C{coupling_1d_to_2d_in}, C{coupling_2d_to_1d_out}, + C{outfall_in}, C{boundary_in}, C{boundary_out} (all m³) and + C{continuity_error} (fraction). + @rtype: dict[str, float] + """ +``` + +### 14A.9 Tests (real `openswmm.engine.Solver`, no mocks) + +Per project convention, every item below runs against the real handle-based +Solver on a small triangulated mesh — no engine mocks. + +| Test | Validates | +|------|-----------| +| `test_2d_max_depth_envelope` | After a rising-then-falling hydrograph, `get_stat_max_depths()[i] ≥` every instantaneous `get_depths()[i]` seen during the run, and the envelope is non-decreasing in time. | +| `test_2d_max_velocity_envelope` | Tilted-plane run: `get_stat_max_velocities()` is ≥ the magnitude of every per-step `√(vx²+vy²)`; dry cells stay 0. | +| `test_2d_local_continuity_envelope` | `stat_max_cont_err` equals the running max of `|cell_continuity_err|`; on a closed conservative still-water case it stays below the per-cell tolerance. | +| `test_2d_global_continuity_closed` | Rainfall-only, walled domain: `continuity_error` < 1e-4 (init + rainfall − final_storage balances). | +| `test_2d_global_continuity_coupled` | Coupled spill/drain case: `coupling_1d_to_2d_in − coupling_2d_to_1d_out` matches the 1D side's `coupling_inflow` integral; `continuity_error` within tolerance. | +| `test_2d_hdf5_envelope_datasets` | After a run with `OUTPUT_FILE`, the HDF5 has `/Mesh2_face_max_depth`, `/Mesh2_face_max_velocity`, `/Mesh2_face_max_continuity_err` of shape `[nFace]` whose values equal the C-API envelopes; `/mass_balance_2d` group exists with `@continuity_error`. | + +### 14A.10 Affected files (forecast) + +| File | Change | +|------|--------| +| `src/engine/2d/data/SurfaceStateData.hpp` | Add `stat_max_velocity`, `stat_max_cont_err`; init in `resize`; fold into `update_statistics`. | +| `src/engine/2d/output/Default2DOutputPlugin.hpp` | 3 new `ds_face_max_*_` members. | +| `src/engine/2d/output/Default2DOutputPlugin.cpp` | Create fixed `[nFace]` datasets in `prepareMeshAndDatasets`; overwrite in `update`; write `/mass_balance_2d` group in `finalize`; close handles. | +| `src/engine/core/SimulationContext.hpp` (snapshot) | 3 new `surface_stat_*` arrays on `SimulationSnapshot`. | +| `src/engine/core/SWMMEngine.cpp` | Fill the 3 arrays in `fillSurfaceSnapshot` (SI-native; skip display conversion). | +| `src/engine/2d/api/Api2D.{hpp,cpp}` + `openswmm_2d.h` | New getters (§14A.7). | +| `python/openswmm/engine/_2d.{pxd,pyx,pyi}` | Wrap the new getters (§14A.8). | +| `tests/...` | Tests in §14A.9. | + +--- + +## 15. C API for 2D Module (`openswmm_2d.h`) + +The 2D module exposes a complete C API following the same conventions as the existing engine API. This enables external orchestration of the entire 2D workflow via CFFI/ctypes/Cython without requiring C++ knowledge. + +### 15.1 Design Principles + +1. **Opaque handle pattern** — all 2D state is accessed through the engine handle +2. **Index-based access** — vertices, triangles, and coupling points are accessed by 0-based index +3. **Bulk operations** — array get/set for efficient data transfer across FFI boundary +4. **Lifecycle-aware** — functions check engine state and return error codes +5. **Optional** — all functions return `SWMM_ERR_BADPARAM` if 2D module is not active + +### 15.2 API Header: `include/openswmm/engine/openswmm_2d.h` + +```c +/** + * @file openswmm_2d.h + * @brief Optional 2D surface routing module — C API. + * + * @details Provides query and control of the optional 2D surface routing + * module coupled to the 1D SWMM pipe network. The 2D module is + * active when [2D_VERTICES] and [2D_TRIANGLES] sections are present + * in the input file and the engine was compiled with OPENSWMM_BUILD_2D. + * + * All functions require the engine to be in SWMM_STATE_RUNNING + * unless otherwise noted. Functions return SWMM_ERR_BADPARAM if + * the 2D module is not active. + * + * @defgroup engine_2d 2D Surface Routing API + * @ingroup engine_api + */ + +#ifndef OPENSWMM_2D_H +#define OPENSWMM_2D_H + +#include "openswmm_callbacks.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ========================================================================= + * 2D Module Status + * ========================================================================= */ + +/** @brief Check whether the 2D module is active for this simulation. + * @param engine Engine handle. + * @param active Output: 1 if 2D is active, 0 otherwise. + * @returns SWMM_OK or error code. + * @note Valid after SWMM_STATE_INITIALIZED. */ +SWMM_ENGINE_API int swmm_2d_is_active(SWMM_Engine engine, int* active); + +/* ========================================================================= + * Mesh Geometry — Query (read-only after initialization) + * ========================================================================= */ + +/** @brief Get the number of mesh vertices. */ +SWMM_ENGINE_API int swmm_2d_vertex_count(SWMM_Engine engine, int* count); + +/** @brief Get the number of mesh triangles. */ +SWMM_ENGINE_API int swmm_2d_triangle_count(SWMM_Engine engine, int* count); + +/** @brief Get vertex coordinates. + * @param idx Vertex index (0-based). + * @param x,y,z Output coordinates. */ +SWMM_ENGINE_API int swmm_2d_vertex_get_xyz(SWMM_Engine engine, int idx, + double* x, double* y, double* z); + +/** @brief Bulk get vertex coordinates. + * @param x,y,z Output arrays (must be pre-allocated to vertex_count). */ +SWMM_ENGINE_API int swmm_2d_vertex_get_xyz_bulk(SWMM_Engine engine, + double* x, double* y, double* z); + +/** @brief Get triangle connectivity (3 vertex indices). + * @param idx Triangle index (0-based). + * @param v0,v1,v2 Output vertex indices. */ +SWMM_ENGINE_API int swmm_2d_triangle_get_vertices(SWMM_Engine engine, int idx, + int* v0, int* v1, int* v2); + +/** @brief Get triangle area. + * @param idx Triangle index. + * @param area Output area (m² or ft²). */ +SWMM_ENGINE_API int swmm_2d_triangle_get_area(SWMM_Engine engine, int idx, + double* area); + +/** @brief Get triangle centroid coordinates. */ +SWMM_ENGINE_API int swmm_2d_triangle_get_centroid(SWMM_Engine engine, int idx, + double* cx, double* cy, double* cz); + +/** @brief Get triangle Manning's roughness. */ +SWMM_ENGINE_API int swmm_2d_triangle_get_mannings(SWMM_Engine engine, int idx, + double* n); + +/** @brief Get triangle neighbour indices (-1 = boundary edge). + * @param n0,n1,n2 Adjacent triangle indices across edges opposite v0,v1,v2. */ +SWMM_ENGINE_API int swmm_2d_triangle_get_neighbours(SWMM_Engine engine, int idx, + int* n0, int* n1, int* n2); + +/* ========================================================================= + * Coupling Map — Query + * ========================================================================= */ + +/** @brief Get the number of vertex-to-node coupling points. */ +SWMM_ENGINE_API int swmm_2d_vertex_coupling_count(SWMM_Engine engine, int* count); + +/** @brief Get the number of triangle-to-node coupling points. */ +SWMM_ENGINE_API int swmm_2d_triangle_coupling_count(SWMM_Engine engine, int* count); + +/** @brief Get vertex coupling: which SWMM node is coupled to this vertex. + * @param vertex_idx Vertex index. + * @param node_idx Output: SWMM node index, or -1 if uncoupled. */ +SWMM_ENGINE_API int swmm_2d_vertex_get_coupled_node(SWMM_Engine engine, + int vertex_idx, int* node_idx); + +/** @brief Get triangle coupling: which SWMM node is coupled to this triangle. + * @param tri_idx Triangle index. + * @param node_idx Output: SWMM node index, or -1 if uncoupled. */ +SWMM_ENGINE_API int swmm_2d_triangle_get_coupled_node(SWMM_Engine engine, + int tri_idx, int* node_idx); + +/* ========================================================================= + * 2D State — Per-Triangle (read during RUNNING) + * ========================================================================= */ + +/** @brief Get water depth at a triangle. + * @param idx Triangle index. + * @param depth Output depth (m or ft). */ +SWMM_ENGINE_API int swmm_2d_get_depth(SWMM_Engine engine, int idx, double* depth); + +/** @brief Get total head at a triangle (z + depth). */ +SWMM_ENGINE_API int swmm_2d_get_head(SWMM_Engine engine, int idx, double* head); + +/** @brief Get coupling exchange flux at a triangle (m/s, + = into 2D). */ +SWMM_ENGINE_API int swmm_2d_get_coupling_flux(SWMM_Engine engine, int idx, + double* flux); + +/** @brief Get rainfall intensity at a triangle (m/s). */ +SWMM_ENGINE_API int swmm_2d_get_rainfall(SWMM_Engine engine, int idx, + double* rainfall); + +/** @brief Get net source/sink rate at a triangle (m/s). */ +SWMM_ENGINE_API int swmm_2d_get_net_source(SWMM_Engine engine, int idx, + double* net_source); + +/** @brief Bulk get depths for all triangles. + * @param depths Output array (pre-allocated to triangle_count). */ +SWMM_ENGINE_API int swmm_2d_get_depths_bulk(SWMM_Engine engine, double* depths); + +/** @brief Bulk get heads for all triangles. */ +SWMM_ENGINE_API int swmm_2d_get_heads_bulk(SWMM_Engine engine, double* heads); + +/** @brief Bulk get coupling fluxes for all triangles. */ +SWMM_ENGINE_API int swmm_2d_get_coupling_fluxes_bulk(SWMM_Engine engine, + double* fluxes); + +/* ========================================================================= + * 2D State — Per-Vertex (reconstructed heads) + * ========================================================================= */ + +/** @brief Get reconstructed head at a vertex. */ +SWMM_ENGINE_API int swmm_2d_vertex_get_head(SWMM_Engine engine, int idx, + double* head); + +/** @brief Bulk get reconstructed heads at all vertices. */ +SWMM_ENGINE_API int swmm_2d_vertex_get_heads_bulk(SWMM_Engine engine, + double* heads); + +/* ========================================================================= + * 2D Solver Statistics + * ========================================================================= */ + +/** @brief Get the maximum depth across all triangles. */ +SWMM_ENGINE_API int swmm_2d_get_max_depth(SWMM_Engine engine, double* max_depth); + +/** @brief Get total 2D surface volume (sum of depth * area). */ +SWMM_ENGINE_API int swmm_2d_get_total_volume(SWMM_Engine engine, double* volume); + +/** @brief Get total exchange flow rate (sum of all coupling flows, m³/s). + * Positive = net flow from 2D into 1D network. */ +SWMM_ENGINE_API int swmm_2d_get_total_exchange_flow(SWMM_Engine engine, + double* flow); + +/** @brief Get number of CVODE internal steps taken in the last advance. */ +SWMM_ENGINE_API int swmm_2d_get_cvode_steps(SWMM_Engine engine, long* steps); + +/** @brief Get CVODE last internal step size. */ +SWMM_ENGINE_API int swmm_2d_get_cvode_last_step(SWMM_Engine engine, double* h_last); + +/** @brief Get per-triangle max depth statistics (cumulative). + * @param max_depths Output array (pre-allocated to triangle_count). */ +SWMM_ENGINE_API int swmm_2d_get_stat_max_depths(SWMM_Engine engine, + double* max_depths); + +/* ========================================================================= + * 2D Forcing — Override rainfall or coupling for external control + * ========================================================================= */ + +/** @brief Force rainfall on a specific triangle. + * @param idx Triangle index. + * @param value Rainfall rate (m/s). + * @param mode SWMM_FORCING_OVERRIDE or SWMM_FORCING_ADD. + * @param persist SWMM_FORCING_RESET or SWMM_FORCING_PERSIST. */ +SWMM_ENGINE_API int swmm_2d_force_rainfall(SWMM_Engine engine, int idx, + double value, int mode, int persist); + +/** @brief Force rainfall on all triangles (uniform). */ +SWMM_ENGINE_API int swmm_2d_force_rainfall_uniform(SWMM_Engine engine, + double value, int mode, + int persist); + +/** @brief Force coupling flux on a specific triangle (override computed exchange). + * @param value Flux rate (m/s, + = into 2D). */ +SWMM_ENGINE_API int swmm_2d_force_coupling_flux(SWMM_Engine engine, int idx, + double value, int mode, + int persist); + +/** @brief Clear all 2D forcings. */ +SWMM_ENGINE_API int swmm_2d_force_clear_all(SWMM_Engine engine); + +/* ========================================================================= + * 2D Solver Options — Query/Modify (valid after INITIALIZED) + * ========================================================================= */ + +/** @brief Get the dry depth threshold (m). */ +SWMM_ENGINE_API int swmm_2d_get_dry_depth(SWMM_Engine engine, double* dry_depth); + +/** @brief Set the dry depth threshold (m). */ +SWMM_ENGINE_API int swmm_2d_set_dry_depth(SWMM_Engine engine, double dry_depth); + +/** @brief Get CVODE relative tolerance. */ +SWMM_ENGINE_API int swmm_2d_get_rel_tolerance(SWMM_Engine engine, double* rtol); + +/** @brief Set CVODE relative tolerance. */ +SWMM_ENGINE_API int swmm_2d_set_rel_tolerance(SWMM_Engine engine, double rtol); + +/** @brief Get CVODE absolute tolerance. */ +SWMM_ENGINE_API int swmm_2d_get_abs_tolerance(SWMM_Engine engine, double* atol); + +/** @brief Set CVODE absolute tolerance. */ +SWMM_ENGINE_API int swmm_2d_set_abs_tolerance(SWMM_Engine engine, double atol); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* OPENSWMM_2D_H */ +``` + +### 15.3 Cython Declarations (`python/openswmm/engine/_2d.pxd`) + +```cython +cdef extern from "openswmm_2d.h": + # Status + int swmm_2d_is_active(void* engine, int* active) + + # Mesh geometry + int swmm_2d_vertex_count(void* engine, int* count) + int swmm_2d_triangle_count(void* engine, int* count) + int swmm_2d_vertex_get_xyz(void* engine, int idx, + double* x, double* y, double* z) + int swmm_2d_vertex_get_xyz_bulk(void* engine, + double* x, double* y, double* z) + int swmm_2d_triangle_get_vertices(void* engine, int idx, + int* v0, int* v1, int* v2) + int swmm_2d_triangle_get_area(void* engine, int idx, double* area) + int swmm_2d_triangle_get_centroid(void* engine, int idx, + double* cx, double* cy, double* cz) + int swmm_2d_triangle_get_mannings(void* engine, int idx, double* n) + int swmm_2d_triangle_get_neighbours(void* engine, int idx, + int* n0, int* n1, int* n2) + + # Coupling + int swmm_2d_vertex_coupling_count(void* engine, int* count) + int swmm_2d_triangle_coupling_count(void* engine, int* count) + int swmm_2d_vertex_get_coupled_node(void* engine, int vidx, int* nidx) + int swmm_2d_triangle_get_coupled_node(void* engine, int tidx, int* nidx) + + # State + int swmm_2d_get_depth(void* engine, int idx, double* depth) + int swmm_2d_get_head(void* engine, int idx, double* head) + int swmm_2d_get_coupling_flux(void* engine, int idx, double* flux) + int swmm_2d_get_rainfall(void* engine, int idx, double* rainfall) + int swmm_2d_get_depths_bulk(void* engine, double* depths) + int swmm_2d_get_heads_bulk(void* engine, double* heads) + int swmm_2d_get_coupling_fluxes_bulk(void* engine, double* fluxes) + + # Vertex state + int swmm_2d_vertex_get_head(void* engine, int idx, double* head) + int swmm_2d_vertex_get_heads_bulk(void* engine, double* heads) + + # Statistics + int swmm_2d_get_max_depth(void* engine, double* max_depth) + int swmm_2d_get_total_volume(void* engine, double* volume) + int swmm_2d_get_total_exchange_flow(void* engine, double* flow) + int swmm_2d_get_cvode_steps(void* engine, long* steps) + int swmm_2d_get_cvode_last_step(void* engine, double* h_last) + + # Forcing + int swmm_2d_force_rainfall(void* engine, int idx, + double value, int mode, int persist) + int swmm_2d_force_rainfall_uniform(void* engine, + double value, int mode, int persist) + int swmm_2d_force_coupling_flux(void* engine, int idx, + double value, int mode, int persist) + int swmm_2d_force_clear_all(void* engine) + + # Options + int swmm_2d_get_dry_depth(void* engine, double* dry_depth) + int swmm_2d_set_dry_depth(void* engine, double dry_depth) + int swmm_2d_get_rel_tolerance(void* engine, double* rtol) + int swmm_2d_set_rel_tolerance(void* engine, double rtol) + int swmm_2d_get_abs_tolerance(void* engine, double* atol) + int swmm_2d_set_abs_tolerance(void* engine, double atol) +``` + +### 15.4 Python Wrapper (`python/openswmm/engine/_2d.pyx`) + +```python +# High-level Python class wrapping the 2D C API +cimport numpy as np +import numpy as np + +cdef class Surface2D: + """Read-only view of the 2D surface routing state.""" + + cdef void* _engine + + def __init__(self, engine_handle): + self._engine = engine_handle + + @property + def is_active(self): + cdef int active = 0 + _check(swmm_2d_is_active(self._engine, &active)) + return bool(active) + + @property + def n_vertices(self): + cdef int count = 0 + _check(swmm_2d_vertex_count(self._engine, &count)) + return count + + @property + def n_triangles(self): + cdef int count = 0 + _check(swmm_2d_triangle_count(self._engine, &count)) + return count + + def get_depths(self): + """Return depths for all triangles as a numpy array.""" + cdef int n = self.n_triangles + cdef np.ndarray[double, ndim=1] arr = np.empty(n, dtype=np.float64) + _check(swmm_2d_get_depths_bulk(self._engine, &arr[0])) + return arr + + def get_heads(self): + """Return total heads for all triangles as a numpy array.""" + cdef int n = self.n_triangles + cdef np.ndarray[double, ndim=1] arr = np.empty(n, dtype=np.float64) + _check(swmm_2d_get_heads_bulk(self._engine, &arr[0])) + return arr + + def get_vertex_coords(self): + """Return (x, y, z) arrays for all vertices.""" + cdef int n = self.n_vertices + cdef np.ndarray[double, ndim=1] x = np.empty(n, dtype=np.float64) + cdef np.ndarray[double, ndim=1] y = np.empty(n, dtype=np.float64) + cdef np.ndarray[double, ndim=1] z = np.empty(n, dtype=np.float64) + _check(swmm_2d_vertex_get_xyz_bulk(self._engine, &x[0], &y[0], &z[0])) + return x, y, z + + @property + def total_volume(self): + cdef double vol = 0.0 + _check(swmm_2d_get_total_volume(self._engine, &vol)) + return vol + + @property + def total_exchange_flow(self): + cdef double flow = 0.0 + _check(swmm_2d_get_total_exchange_flow(self._engine, &flow)) + return flow +``` + +### 15.5 End-to-End CFFI Workflow Example + +The C API enables a complete external orchestration workflow: + +```c +/* Example: External Python/C driver controlling the full 2D workflow */ + +SWMM_Engine e = swmm_engine_create(); +swmm_engine_open(e, "model_with_2d.inp", "model.rpt", "model.out", NULL); +swmm_engine_initialize(e); + +/* Check if 2D is active */ +int has_2d = 0; +swmm_2d_is_active(e, &has_2d); + +int n_tri = 0, n_vert = 0; +if (has_2d) { + swmm_2d_triangle_count(e, &n_tri); + swmm_2d_vertex_count(e, &n_vert); +} + +swmm_engine_start(e, 1); + +double elapsed = 0.0; +double* depths = malloc(n_tri * sizeof(double)); + +while (swmm_engine_step(e, &elapsed) == SWMM_OK && elapsed > 0.0) { + if (has_2d) { + /* Read 2D state after each step */ + swmm_2d_get_depths_bulk(e, depths); + + /* Optionally override rainfall for scenario testing */ + swmm_2d_force_rainfall_uniform(e, 0.001, /* 1 mm/s */ + SWMM_FORCING_OVERRIDE, SWMM_FORCING_RESET); + + /* Query coupling statistics */ + double total_exchange = 0.0; + swmm_2d_get_total_exchange_flow(e, &total_exchange); + + /* Query solver diagnostics */ + long cvode_steps = 0; + swmm_2d_get_cvode_steps(e, &cvode_steps); + } +} + +free(depths); +swmm_engine_end(e); +swmm_engine_report(e); +swmm_engine_close(e); +swmm_engine_destroy(e); +``` + +--- + +## 16. Summary of Key Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Mesh type | Constrained Delaunay triangles | Boundary-fitting, adaptive resolution, matches paper | +| Solver | CVODE (BDF) + GMRES | Handles stiffness from Manning's nonlinearity, Jacobian-free | +| Accuracy | Second-order (linear reconstruction + limiter) | Paper formulation, avoids first-order numerical diffusion | +| Coupling mechanism | Forcing API | Clean separation, no modifications to core SWMM solver | +| Exchange equation | Orifice | Standard for manhole/inlet exchange, handles bidirectional flow | +| Uncapped nodes | Suppress ponding, allow head overshoot | 2D surface replaces ponded area for coupled nodes | +| Outfall feedback | max(h_standard, h_2d) | Dynamic tailwater from 2D raises boundary without lowering it | +| C API | `openswmm_2d.h` with opaque handles | Consistent with engine API, enables CFFI/ctypes/Cython | +| Dependency management | vcpkg (SUNDIALS) | Consistent with existing project infrastructure | +| Compile-time optional | `OPENSWMM_BUILD_2D` CMake flag | No penalty for users who don't need 2D | +| Data layout | SoA (parallel vectors) | Matches existing engine pattern, cache-friendly | +| Section naming | `[2D_*]` prefix | Clear, avoids collisions, extensible | + +--- + +## References + +- Kumar, M., Duffy, C.J., and Salvage, K.M. (2009). "A Second-Order Accurate, Finite Volume–Based, Integrated Hydrologic Modeling (FIHM) Framework for Simulation of Surface and Subsurface Flow." *Vadose Zone Journal*, doi:10.2136/vzj2009.0014. +- Jawahar, P. and Kamath, H. (2000). "A high-resolution procedure for Euler and Navier-Stokes computations on unstructured grids." *J. Comput. Phys.*, 164:165–203. +- Abdul, A.S. and Gillham, R.W. (1984). "Laboratory studies of the effects of the capillary fringe on streamflow generation." *Water Resour. Res.*, 20:691–698. +- Cohen, S.D. and Hindmarsh, A.C. (1994). "CVODE user guide." Technical Rep. UCRL-MA-118618. Lawrence Livermore National Lab. diff --git a/docs/Doxyfile b/docs/Doxyfile new file mode 100644 index 000000000..c262cbbb8 --- /dev/null +++ b/docs/Doxyfile @@ -0,0 +1,2998 @@ +# Doxyfile 1.13.2 + +# This file describes the settings to be used by the documentation system +# Doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). +# +# Note: +# +# Use Doxygen to compare the used configuration file with the template +# configuration file: +# doxygen -x [configFile] +# Use Doxygen to compare the used configuration file with the template +# configuration file without replacing the environment variables or CMake type +# replacement variables: +# doxygen -x_noenv [configFile] + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "OpenSWMM Engine" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = 6.0.0-alpha.1 + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewers a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = "Data-oriented, plugin-extensible SWMM Engine (6.0.0-alpha.1)" + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = ./images/hydrocouple_logo.png + +# With the PROJECT_ICON tag one can specify an icon that is included in the tabs +# when the HTML document is shown. Doxygen will copy the logo to the output +# directory. + +PROJECT_ICON = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where Doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = . + +# If the CREATE_SUBDIRS tag is set to YES then Doxygen will create up to 4096 +# sub-directories (in 2 levels) under the output directory of each output format +# and will distribute the generated files over these directories. Enabling this +# option can be useful when feeding Doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise cause +# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to +# control the number of sub-directories. +# The default value is: NO. + +CREATE_SUBDIRS = YES + +# Controls the number of sub-directories that will be created when +# CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every +# level increment doubles the number of directories, resulting in 4096 +# directories at level 8 which is the default and also the maximum value. The +# sub-directories are organized in 2 levels, the first level always has a fixed +# number of 16 directories. +# Minimum value: 0, maximum value: 8, default value: 8. +# This tag requires that the tag CREATE_SUBDIRS is set to YES. + +CREATE_SUBDIRS_LEVEL = 8 + +# If the ALLOW_UNICODE_NAMES tag is set to YES, Doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by Doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian, +# Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English +# (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek, +# Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with +# English messages), Korean, Korean-en (Korean with English messages), Latvian, +# Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, +# Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, +# Swedish, Turkish, Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES, Doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, Doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# Doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, Doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, Doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which Doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where Doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, Doxygen will generate much shorter (but +# less readable) file names. This can be useful if your file system doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen will interpret the +# first line (until the first dot, question mark or exclamation mark) of a +# Javadoc-style comment as the brief description. If set to NO, the Javadoc- +# style will behave just like regular Qt-style comments (thus requiring an +# explicit @brief command for a brief description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the JAVADOC_BANNER tag is set to YES then Doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by Doxygen. +# The default value is: NO. + +JAVADOC_BANNER = NO + +# If the QT_AUTOBRIEF tag is set to YES then Doxygen will interpret the first +# line (until the first dot, question mark or exclamation mark) of a Qt-style +# comment as the brief description. If set to NO, the Qt-style will behave just +# like regular Qt-style comments (thus requiring an explicit \brief command for +# a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# By default Python docstrings are displayed as preformatted text and Doxygen's +# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the +# Doxygen's special commands can be used and the contents of the docstring +# documentation blocks is shown as Doxygen documentation. +# The default value is: YES. + +PYTHON_DOCSTRING = YES + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then Doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:^^" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". Note that you cannot put \n's in the value part of an alias +# to insert newlines (in the resulting output). You can put ^^ in the value part +# of an alias to insert a newline as if a physical newline was in the original +# file. When you need a literal { or } or , in the value part of an alias you +# have to escape them by means of a backslash (\), this can lead to conflicts +# with the commands \{ and \} for these it is advised to use the version @{ and +# @} or use a double escape (\\{ and \\}) + +ALIASES = "license=\par License\n" \ + "description=\par Discription\n" \ + "mod=\par Modified\n" \ + "moditem{3}=\b \date\1 \author\2 \3\n" + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by Doxygen: IDL, Java, JavaScript, +# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice, +# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files). For instance to make Doxygen treat .inc files +# as Fortran files (default is PHP), and .f files as C (default is Fortran), +# use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by Doxygen. When specifying no_extension you should add +# * to the FILE_PATTERNS. +# +# Note see also the list of default file extension mappings. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then Doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by Doxygen, so you can +# mix Doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 6. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 5 + +# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to +# generate identifiers for the Markdown headings. Note: Every identifier is +# unique. +# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a +# sequence number starting at 0 and GITHUB use the lower case version of title +# with any whitespace replaced by '-' and punctuation characters removed. +# The default value is: DOXYGEN. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_ID_STYLE = DOXYGEN + +# When enabled Doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. Words listed in the +# AUTOLINK_IGNORE_WORDS tag are excluded from automatic linking. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# This tag specifies a list of words that, when matching the start of a word in +# the documentation, will suppress auto links generation, if it is enabled via +# AUTOLINK_SUPPORT. This list does not affect affect links explicitly created +# using \# or the \link or commands. +# This tag requires that the tag AUTOLINK_SUPPORT is set to YES. + +AUTOLINK_IGNORE_WORDS = + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let Doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also makes the inheritance and +# collaboration diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# https://www.riverbankcomputing.com/software) sources only. Doxygen will parse +# them like normal C++ but will assume all classes use public instead of private +# inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# Doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then Doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, Doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# Doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run Doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +# The NUM_PROC_THREADS specifies the number of threads Doxygen is allowed to use +# during processing. When set to 0 Doxygen will based this on the number of +# cores available in the system. You can set it explicitly to a value larger +# than 0 to get more control over the balance between CPU load and processing +# speed. At this moment only the input processing can be done using multiple +# threads. Since this is still an experimental feature the default is set to 1, +# which effectively disables parallel processing. Please report any issues you +# encounter. Generating dot graphs in parallel is controlled by the +# DOT_NUM_THREADS setting. +# Minimum value: 0, maximum value: 32, default value: 1. + +NUM_PROC_THREADS = 1 + +# If the TIMESTAMP tag is set different from NO then each generated page will +# contain the date or date and time when the page was generated. Setting this to +# NO can help when comparing the output of multiple runs. +# Possible values are: YES, NO, DATETIME and DATE. +# The default value is: NO. + +TIMESTAMP = NO + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, Doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual +# methods of a class will be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIV_VIRTUAL = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If this flag is set to YES, the name of an unnamed parameter in a declaration +# will be determined by the corresponding definition. By default unnamed +# parameters remain unnamed in the output. +# The default value is: YES. + +RESOLVE_UNNAMED_PARAMS = YES + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# will also hide undocumented C++ concepts if enabled. This option has no effect +# if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_UNDOC_NAMESPACES tag is set to YES, Doxygen will hide all +# undocumented namespaces that are normally visible in the namespace hierarchy. +# If set to NO, these namespaces will be included in the various overviews. This +# option has no effect if EXTRACT_ALL is enabled. +# The default value is: YES. + +HIDE_UNDOC_NAMESPACES = YES + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all friend +# declarations. If set to NO, these declarations will be included in the +# documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# With the correct setting of option CASE_SENSE_NAMES Doxygen will better be +# able to match the capabilities of the underlying filesystem. In case the +# filesystem is case sensitive (i.e. it supports files in the same directory +# whose names only differ in casing), the option must be set to YES to properly +# deal with such files in case they appear in the input. For filesystems that +# are not case sensitive the option should be set to NO to properly deal with +# output files written for symbols that only differ in casing, such as for two +# classes, one named CLASS and the other named Class, and to also support +# references to files without having to specify the exact matching casing. On +# Windows (including Cygwin) and macOS, users should typically set this option +# to NO, whereas on Linux or other Unix flavors it should typically be set to +# YES. +# Possible values are: SYSTEM, NO and YES. +# The default value is: SYSTEM. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO then Doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then Doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class +# will show which file needs to be included to use the class. +# The default value is: YES. + +SHOW_HEADERFILE = YES + +# If the SHOW_INCLUDE_FILES tag is set to YES then Doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then Doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then Doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then Doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then Doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and Doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING Doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# Doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by Doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by Doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents Doxygen's defaults, run Doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. See also section "Changing the +# layout of pages" for information. +# +# Note that if you run Doxygen from a directory containing a file called +# DoxygenLayout.xml, Doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = DoxygenLayout.xml + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +# The EXTERNAL_TOOL_PATH tag can be used to extend the search path (PATH +# environment variable) so that external tools such as latex and gs can be +# found. +# Note: Directories specified with EXTERNAL_TOOL_PATH are added in front of the +# path already specified by the PATH variable, and are added in the order +# specified. +# Note: This option is particularly useful for macOS version 14 (Sonoma) and +# higher, when running Doxygen from Doxywizard, because in this case any user- +# defined changes to the PATH are ignored. A typical example on macOS is to set +# EXTERNAL_TOOL_PATH = /Library/TeX/texbin /usr/local/bin +# together with the standard path, the full search path used by doxygen when +# launching external tools will then become +# PATH=/Library/TeX/texbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + +EXTERNAL_TOOL_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by Doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by Doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then Doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, Doxygen will generate warnings for +# potential errors in the documentation, such as documenting some parameters in +# a documented function twice, or documenting parameters that don't exist or +# using markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# If WARN_IF_INCOMPLETE_DOC is set to YES, Doxygen will warn about incomplete +# function parameter documentation. If set to NO, Doxygen will accept that some +# parameters have no documentation without warning. +# The default value is: YES. + +WARN_IF_INCOMPLETE_DOC = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, Doxygen will only warn about wrong parameter +# documentation, but not about the absence of documentation. If EXTRACT_ALL is +# set to YES then this flag will automatically be disabled. See also +# WARN_IF_INCOMPLETE_DOC +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, Doxygen will warn about +# undocumented enumeration values. If set to NO, Doxygen will accept +# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: NO. + +WARN_IF_UNDOC_ENUM_VAL = NO + +# If WARN_LAYOUT_FILE option is set to YES, Doxygen will warn about issues found +# while parsing the user defined layout file, such as missing or wrong elements. +# See also LAYOUT_FILE for details. If set to NO, problems with the layout file +# will be suppressed. +# The default value is: YES. + +WARN_LAYOUT_FILE = YES + +# If the WARN_AS_ERROR tag is set to YES then Doxygen will immediately stop when +# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS +# then Doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but +# at the end of the Doxygen process Doxygen will return with a non-zero status. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then Doxygen behaves +# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined Doxygen will not +# write the warning messages in between other messages but write them at the end +# of a run, in case a WARN_LOGFILE is defined the warning messages will be +# besides being in the defined file also be shown at the end of a run, unless +# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case +# the behavior will remain as with the setting FAIL_ON_WARNINGS. +# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that Doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# See also: WARN_LINE_FORMAT +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# In the $text part of the WARN_FORMAT command it is possible that a reference +# to a more specific place is given. To make it easier to jump to this place +# (outside of Doxygen) the user can define a custom "cut" / "paste" string. +# Example: +# WARN_LINE_FORMAT = "'vi $file +$line'" +# See also: WARN_FORMAT +# The default value is: at line $line of file $file. + +WARN_LINE_FORMAT = "at line $line of file $file" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). In case the file specified cannot be opened for writing the +# warning and error messages are written to standard error. When as file - is +# specified the warning and error messages are written to standard output +# (stdout). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = ../README.md \ + ../src/legacy/engine \ + ../src/legacy/output \ + ../src/engine \ + ../src/solver \ + ../src/output \ + ../include \ + ./images/*.png \ + ./manuals/user/figures/*.png \ + ./Updates.md \ + ./authors.md \ + ./manuals/manuals.md \ + ./manuals/user \ + ./manuals/reference \ + ./manuals/application + +# This tag can be used to specify the character encoding of the source files +# that Doxygen parses. Internally Doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: +# https://www.gnu.org/software/libiconv/) for the list of possible encodings. +# See also: INPUT_FILE_ENCODING +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# This tag can be used to specify the character encoding of the source files +# that Doxygen parses. The INPUT_FILE_ENCODING tag can be used to specify +# character encoding on a per file pattern basis. Doxygen will compare the file +# name with each pattern and apply the encoding instead of the default +# INPUT_ENCODING if there is a match. The character encodings are a list of the +# form: pattern=encoding (like *.php=ISO-8859-1). +# See also: INPUT_ENCODING for further information on supported encodings. + +INPUT_FILE_ENCODING = + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by Doxygen. +# +# Note the list of default checked file patterns might differ from the list of +# default file extension mappings. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, +# *.cpp, *.cppm, *.ccm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, +# *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, +# *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to +# be provided as Doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. + +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.idl \ + *.ddl \ + *.odl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.cs \ + *.d \ + *.php \ + *.php4 \ + *.php5 \ + *.phtml \ + *.inc \ + *.m \ + *.markdown \ + *.md \ + *.mm \ + *.dox \ + *.py \ + *.pyw \ + *.f90 \ + *.f \ + *.for \ + *.tcl \ + *.vhd \ + *.vhdl \ + *.ucf \ + *.qsf \ + *.as \ + *.js + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which Doxygen is +# run. + +EXCLUDE = ./manuals/reference/hydrology/sections \ + ./manuals/reference/hydraulics/sections \ + ./manuals/reference/quality/sections \ + ./manuals/user/manual + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = manuals/user/manual/* \ + */sections/* \ + */SWMM* \ + manuals/user/figures/* \ + manuals/reference/*/media/* \ + manuals/reference/*/figures/* \ + manuals/application/figures/* \ + implementation-plan.md.bak + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# ANamespace::AClass, ANamespace::*Test + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = ./images \ + ./manuals/user/figures \ + ./manuals/reference/hydrology/media/media \ + ./manuals/reference/hydraulics/media/media \ + ./manuals/reference/quality/media/media + +# The INPUT_FILTER tag can be used to specify a program that Doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that Doxygen will use the data processed and written to standard output +# for further processing, therefore nothing else, like debug statements or used +# commands (so in case of a Windows batch file always use @echo OFF), should be +# written to standard output. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by Doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by Doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the Doxygen output. + +USE_MDFILE_AS_MAINPAGE = ../README.md + +# If the IMPLICIT_DIR_DOCS tag is set to YES, any README.md file found in sub- +# directories of the project's root, is used as the documentation for that sub- +# directory, except when the README.md starts with a \dir, \page or \mainpage +# command. If set to NO, the README.md file needs to start with an explicit \dir +# command in order to be used as directory documentation. +# The default value is: YES. + +IMPLICIT_DIR_DOCS = YES + +# The Fortran standard specifies that for fixed formatted Fortran code all +# characters from position 72 are to be considered as comment. A common +# extension is to allow longer lines before the automatic comment starts. The +# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can +# be processed before the automatic comment starts. +# Minimum value: 7, maximum value: 10000, default value: 72. + +FORTRAN_COMMENT_AFTER = 72 + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# multi-line macros, enums or list initialized variables directly into the +# documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct Doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# entity all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of Doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see https://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by Doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then Doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +# If the CLANG_ASSISTED_PARSING tag is set to YES then Doxygen will use the +# clang parser (see: +# http://clang.llvm.org/) for more accurate parsing at the cost of reduced +# performance. This can be particularly helpful with template rich C++ code for +# which Doxygen's built-in parser lacks the necessary type information. +# Note: The availability of this option depends on whether or not Doxygen was +# generated with the -Duse_libclang=ON option for CMake. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS +# tag is set to YES then Doxygen will add the directory of each input to the +# include path. +# The default value is: YES. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_ADD_INC_PATHS = YES + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by Doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + +# If clang assisted parsing is enabled you can provide the clang parser with the +# path to the directory containing a file called compile_commands.json. This +# file is the compilation database (see: +# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the +# options used when the source files were built. This is equivalent to +# specifying the -p option to a clang tool, such as clang-check. These options +# will then be passed to the parser. Any options specified with CLANG_OPTIONS +# will be added as well. +# Note: The availability of this option depends on whether or not Doxygen was +# generated with the -Duse_libclang=ON option for CMake. + +CLANG_DATABASE_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes) +# that should be ignored while generating the index headers. The IGNORE_PREFIX +# tag works for classes, function and member names. The entity will be placed in +# the alphabetical list under the first letter of the entity name that remains +# after removing the prefix. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = I + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, Doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank Doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that Doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that Doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of Doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = ./custom/html/header.html + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank Doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that Doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank Doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that Doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by Doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). +# Note: Since the styling of scrollbars can currently not be overruled in +# Webkit/Chromium, the styling will be left out of the default doxygen.css if +# one or more extra stylesheets have been specified. So if scrollbar +# customization is desired it has to be added explicitly. For an example see the +# documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = ./custom/css/doxygen-awesome.css + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = custom/js/doxygen-awesome-darkmode-toggle.js \ + custom/js/doxygen-awesome-fragment-copy-button.js \ + custom/js/doxygen-awesome-interactive-toc.js \ + custom/js/doxygen-awesome-paragraph-link.js \ + custom/js/doxygen-awesome-tabs.js \ + custom/mathjax-config.js + +# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output +# should be rendered with a dark or light theme. +# Possible values are: LIGHT always generates light mode output, DARK always +# generates dark mode output, AUTO_LIGHT automatically sets the mode according +# to the user preference, uses light mode if no preference is set (the default), +# AUTO_DARK automatically sets the mode according to the user preference, uses +# dark mode if no preference is set and TOGGLE allows a user to switch between +# light and dark mode via a button. +# The default value is: AUTO_LIGHT. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE = LIGHT + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a color-wheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use gray-scales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 174 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 84 + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via JavaScript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have JavaScript, +# like the Qt help browser. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be +# dynamically folded and expanded in the generated HTML source code. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_CODE_FOLDING = YES + +# If the HTML_COPY_CLIPBOARD tag is set to YES then Doxygen will show an icon in +# the top right corner of code and text fragments that allows the user to copy +# its content to the clipboard. Note this only works if supported by the browser +# and the web page is served via a secure context (see: +# https://www.w3.org/TR/secure-contexts/), i.e. using the https: or file: +# protocol. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COPY_CLIPBOARD = YES + +# Doxygen stores a couple of settings persistently in the browser (via e.g. +# cookies). By default these settings apply to all HTML pages generated by +# Doxygen across all projects. The HTML_PROJECT_COOKIE tag can be used to store +# the settings under a project specific key, such that the user preferences will +# be stored separately. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_PROJECT_COOKIE = + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: +# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To +# create a documentation set, Doxygen will generate a Makefile in the HTML +# output directory. Running make will produce the docset in that directory and +# running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag determines the URL of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDURL = + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then Doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# on Windows. In the beginning of 2021 Microsoft took the original page, with +# a.o. the download links, offline (the HTML help workshop was already many +# years in maintenance mode). You can download the HTML help workshop from the +# web archives at Installation executable (see: +# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo +# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe). +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by Doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# Doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the main .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# The SITEMAP_URL tag is used to specify the full URL of the place where the +# generated documentation will be placed on the server by the user during the +# deployment of the documentation. The generated sitemap is called sitemap.xml +# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL +# is specified no sitemap is generated. For information about the sitemap +# protocol see https://www.sitemaps.org +# This tag requires that the tag GENERATE_HTML is set to YES. + +SITEMAP_URL = + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location (absolute path +# including file name) of Qt's qhelpgenerator. If non-empty Doxygen will try to +# run qhelpgenerator on the generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine tune the look of the index (see "Fine-tuning the output"). As an +# example, the default style sheet generated by Doxygen has an example that +# shows how to put an image at the root of the tree instead of the PROJECT_NAME. +# Since the tree basically has the same information as the tab index, you could +# consider setting DISABLE_INDEX to YES when enabling this option. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = YES + +# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the +# FULL_SIDEBAR option determines if the side bar is limited to only the treeview +# area (value NO) or if it should extend to the full height of the window (value +# YES). Setting this to YES gives a layout similar to +# https://docs.readthedocs.io with more room for contents, but less room for the +# project logo, title, and description. If either GENERATE_TREEVIEW or +# DISABLE_INDEX is set to NO, this option has no effect. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FULL_SIDEBAR = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# Doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 1 + +# When the SHOW_ENUM_VALUES tag is set doxygen will show the specified +# enumeration values besides the enumeration mnemonics. +# The default value is: NO. + +SHOW_ENUM_VALUES = NO + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, Doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# If the OBFUSCATE_EMAILS tag is set to YES, Doxygen will obfuscate email +# addresses. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +OBFUSCATE_EMAILS = YES + +# If the HTML_FORMULA_FORMAT option is set to svg, Doxygen will use the pdf2svg +# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see +# https://inkscape.org) to generate formulas as SVG images instead of PNGs for +# the HTML output. These images will generally look nicer at scaled resolutions. +# Possible values are: png (the default) and svg (looks nicer but requires the +# pdf2svg or inkscape tool). +# The default value is: png. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FORMULA_FORMAT = png + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# Doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands +# to create new LaTeX commands to be used in formulas as building blocks. See +# the section "Including formulas" for details. + +FORMULA_MACROFILE = + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# https://www.mathjax.org) which uses client side JavaScript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = YES + +# With MATHJAX_VERSION it is possible to specify the MathJax version to be used. +# Note that the different versions of MathJax have different requirements with +# regards to the different settings, so it is possible that also other MathJax +# settings have to be changed when switching between the different MathJax +# versions. +# Possible values are: MathJax_2 and MathJax_3. +# The default value is: MathJax_2. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_VERSION = MathJax_3 + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. For more details about the output format see MathJax +# version 2 (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3 +# (see: +# http://docs.mathjax.org/en/latest/web/components/output.html). +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility. This is the name for Mathjax version 2, for MathJax version 3 +# this will be translated into chtml), NativeMML (i.e. MathML. Only supported +# for MathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This +# is the name for Mathjax version 3, for MathJax version 2 this will be +# translated into HTML-CSS) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = chtml + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from https://www.mathjax.org before deployment. The default value is: +# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2 +# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3 +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@3/es5/ + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# for MathJax version 2 (see +# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions): +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# For example for MathJax version 3 (see +# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): +# MATHJAX_EXTENSIONS = ams +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = ams + +# The MATHJAX_CODEFILE tag can be used to specify a file with JavaScript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = ./custom/mathjax-config.js + +# When the SEARCHENGINE tag is enabled Doxygen will generate a search box for +# the HTML output. The underlying search engine uses JavaScript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the JavaScript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /