diff --git a/.github/workflows/benchmark_dispatch.yml b/.github/workflows/benchmark_dispatch.yml new file mode 100644 index 000000000..e6e223e17 --- /dev/null +++ b/.github/workflows/benchmark_dispatch.yml @@ -0,0 +1,122 @@ +# Tell openswmm.engine.benchmarks that this engine moved, so it can build THIS +# commit and sweep the model corpus against it. +# +# Why a dispatch rather than a job here: the benchmark platform owns the corpus, +# the engine registry, the scoring taxonomy and its own Pages site. Keeping the +# sweep there means this repository needs only permission to say "a new commit +# exists" -- it never needs a Pages credential, and the corpus never has to be +# vendored into this tree. +# +# Gated on Unit Testing succeeding. A sweep is a multi-hour, multi-runner job; +# dispatching a commit that does not build wastes it and buries the real signal +# under a matrix of ERROR cells. +name: Benchmark dispatch + +on: + workflow_run: + workflows: ["Unit Testing"] + types: [completed] + branches: [main, develop] + workflow_dispatch: + inputs: + sha: + description: Engine commit to benchmark (blank = this branch's HEAD) + required: false + +permissions: + contents: read + +env: + BENCHMARKS_REPO: HydroCouple/openswmm.engine.benchmarks + +jobs: + dispatch: + name: Notify the benchmark platform + # workflow_run fires on completion regardless of outcome, so filter to the + # successful PUSH runs. Pull-request runs are excluded deliberately: a fork + # PR cannot read secrets, and a sweep per PR would cost more than it tells + # us until the PR tier is curated. + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push') + runs-on: ubuntu-latest + steps: + - name: Resolve the commit to benchmark + id: target + env: + RUN_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_REF: ${{ github.event.workflow_run.head_branch }} + INPUT_SHA: ${{ inputs.sha }} + run: | + set -euo pipefail + sha="${INPUT_SHA:-${RUN_SHA:-${GITHUB_SHA}}}" + ref="${RUN_REF:-${GITHUB_REF_NAME}}" + # A blank sha would make the receiver silently check out its default + # branch and publish results keyed to the wrong commit. + if [ -z "$sha" ]; then + echo "::error::could not resolve an engine SHA to dispatch" + exit 1 + fi + echo "sha=$sha" >> "$GITHUB_OUTPUT" + echo "ref=$ref" >> "$GITHUB_OUTPUT" + echo "Dispatching $ref @ $sha to $BENCHMARKS_REPO" + + - name: Check the dispatch token is configured + env: + TOKEN: ${{ secrets.BENCHMARKS_DISPATCH_TOKEN }} + run: | + if [ -n "${TOKEN:-}" ]; then exit 0; fi + cat >&2 <<'MSG' + The secret BENCHMARKS_DISPATCH_TOKEN is not set on this repository. + + Cross-repository repository_dispatch cannot use the default + GITHUB_TOKEN -- it is scoped to this repository only -- so a separate + credential is required. + + To configure: + 1. Create a token that may POST to + HydroCouple/openswmm.engine.benchmarks: + * fine-grained PAT -> Repository access: that repo, + Permissions: Contents = Read and write + * classic PAT -> scope: repo + (A GitHub App installation token works too, and is preferable + for a shared org: it is not tied to one person's account.) + 2. Add it here as an Actions secret named + BENCHMARKS_DISPATCH_TOKEN. + + Until then the benchmark platform picks this engine up only via its + own nightly cron (07:00 UTC, building `develop`), not per push. + MSG + echo "::error::BENCHMARKS_DISPATCH_TOKEN is not configured -- see the log above" + exit 1 + + - name: Dispatch to openswmm.engine.benchmarks + env: + TOKEN: ${{ secrets.BENCHMARKS_DISPATCH_TOKEN }} + SHA: ${{ steps.target.outputs.sha }} + REF: ${{ steps.target.outputs.ref }} + run: | + set -euo pipefail + # Built with jq, not string interpolation: a branch name is + # attacker-influenced text and must not be able to break out of the + # JSON body. + payload=$(jq -n \ + --arg sha "$SHA" \ + --arg ref "$REF" \ + --arg repo "$GITHUB_REPOSITORY" \ + --arg run_url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/${{ github.event.workflow_run.id || github.run_id }}" \ + '{event_type: "engine-push", + client_payload: {sha: $sha, ref: $ref, repo: $repo, run_url: $run_url}}') + + curl --fail-with-body -sS -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $TOKEN" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "$GITHUB_API_URL/repos/$BENCHMARKS_REPO/dispatches" \ + -d "$payload" + + echo "Dispatched $REF @ $SHA" + echo "### Benchmark sweep dispatched" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "\`$REF\` @ \`$SHA\` -> [$BENCHMARKS_REPO]($GITHUB_SERVER_URL/$BENCHMARKS_REPO/actions)" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f5d0b95da..61dd12534 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,5 +1,8 @@ name: CodeQL +permissions: + contents: read + on: push: branches: [main] # drop develop — covered by PR @@ -52,7 +55,7 @@ jobs: uses: actions/checkout@v5 with: repository: microsoft/vcpkg - ref: 2025.02.14 + ref: 2026.07.29 path: vcpkg - name: Install Ninja diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 05e01d1fa..9a8d4add1 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -1,5 +1,8 @@ name: Deployment +permissions: + contents: read + on: push: branches: [main] @@ -14,7 +17,7 @@ env: 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" + VCPKG_REF: "2026.07.29" # 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 @@ -23,6 +26,9 @@ env: MACOSX_DEPLOYMENT_TARGET: "11.0" jobs: + python-contracts: + uses: ./.github/workflows/python_contracts.yml + # ────────────────────────────────────────────────────────────────────── # Build release binaries # ────────────────────────────────────────────────────────────────────── @@ -30,7 +36,6 @@ jobs: name: "Build (${{ matrix.alias }})" permissions: contents: read - actions: write # x-gha vcpkg binary cache + actions/cache strategy: fail-fast: false matrix: @@ -131,6 +136,15 @@ jobs: with: arch: amd64 + - name: Re-pin VCPKG_ROOT (Windows) + # vcvars64 (VS 2022 17.6+) exports VCPKG_ROOT pointing at Visual + # Studio's bundled vcpkg, and msvc-dev-cmd forwards it to every later + # step, overriding the job-level env — so configure would silently + # build against VS's port snapshot instead of the pinned checkout. + if: runner.os == 'Windows' + shell: pwsh + run: echo "VCPKG_ROOT=${{ github.workspace }}\vcpkg" >> $env:GITHUB_ENV + - 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 @@ -215,7 +229,6 @@ jobs: # not block all wheels. (Previously `needs: build` coupled them.) permissions: contents: read - actions: write strategy: fail-fast: false matrix: @@ -299,7 +312,6 @@ jobs: name: "GPU-omp wheels (${{ matrix.os }})" permissions: contents: read - actions: write strategy: fail-fast: false matrix: @@ -426,7 +438,7 @@ jobs: # numpy (the base wheel's only runtime dep) is pulled from PyPI as a wheel. "$PY" -m pip install --only-binary :all: numpy # The project wheels MUST come from the local wheelhouse, never PyPI. - # openswmm / openswmm-gpu-omp are pre-releases (6.0.0a3), and a *stable* + # openswmm / openswmm-gpu-omp are pre-releases (for example, 6.0.0a4.dev1), and a *stable* # openswmm-gpu-omp 0.0.0 placeholder exists on PyPI; without --no-index pip # prefers that stable stub over our local pre-release and installs an empty # package (ModuleNotFoundError: openswmm_gpu_omp). --no-index also makes a @@ -434,9 +446,14 @@ jobs: # resolving to PyPI. "$PY" -m pip install --only-binary :all: --no-index --find-links wheelhouse openswmm openswmm-gpu-omp "$PY" -m pip show openswmm | tee wheel-smoke-${{ matrix.os }}.txt - # gpu-omp companion must import AND be auto-discovered by base openswmm - # (import openswmm runs _register_gpu_companion → OPENSWMM_GPU_PLUGIN_PATH). - "$PY" -c "import os,openswmm,openswmm_gpu_omp as g; p=os.environ.get('OPENSWMM_GPU_PLUGIN_PATH'); assert p, 'gpu-omp plugin not discovered'; print('OPENSWMM_GPU_PLUGIN_PATH='+p); print('companion at '+g.__file__)" | tee -a wheel-smoke-${{ matrix.os }}.txt + # A plugin must be auto-discovered on `import openswmm` + # (_register_gpu_plugin → OPENSWMM_GPU_PLUGIN_PATH), the companion must + # import, and the discovered directory must actually CONTAIN a built + # plugin binary — a non-empty env var alone would pass even if the + # OpenMP build had silently produced nothing. The base wheel now ships + # the plugin too, so the discovered dir is normally openswmm/engine; + # the shim prefers it and skips the companion (one OpenMP runtime). + "$PY" -c "import os,glob,openswmm,openswmm_gpu_omp as g; p=os.environ.get('OPENSWMM_GPU_PLUGIN_PATH'); assert p, 'no gpu plugin discovered'; d=p.split(os.pathsep)[0]; hits=[f for f in glob.glob(os.path.join(d,'*openswmm_gpu_*')) if f.endswith(('.so','.dylib','.dll'))]; assert hits, 'discovered dir has no plugin binary: '+d; print('OPENSWMM_GPU_PLUGIN_PATH='+p); print('plugin: '+hits[0]); print('companion at '+g.__file__)" | tee -a wheel-smoke-${{ matrix.os }}.txt # Run the example model end-to-end via the shared smoke script. "$PY" python/smoke_test.py 2>&1 | tee -a wheel-smoke-${{ matrix.os }}.txt @@ -463,7 +480,9 @@ jobs: python-version: "3.13" - name: Check version sync (CMake / vcpkg / pyproject) - run: python scripts/check_version_sync.py + run: | + python -m unittest discover -s scripts -p test_version_sync.py + python scripts/check_version_sync.py - name: Install build frontend run: | @@ -494,7 +513,6 @@ jobs: name: "C++ unit tests (release gate)" permissions: contents: read - actions: write runs-on: ubuntu-latest steps: - name: Checkout repository @@ -544,7 +562,7 @@ jobs: release: name: Create GitHub Release if: startsWith(github.ref, 'refs/tags/v') - needs: [build, test, wheels, gpu-omp-wheels, sdist, wheel-smoke] + needs: [build, test, wheels, gpu-omp-wheels, sdist, wheel-smoke, python-contracts] runs-on: ubuntu-latest permissions: contents: write diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 7bc9a7113..ce4aa3d52 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -1,15 +1,25 @@ name: Documentation +permissions: + contents: read + on: push: # branches: [main, develop] branches: [main, develop, swmm6_rel] tags: ["v*.*.*"] - paths: ['docs/**', 'python/docs/**', 'python/openswmm/**', 'include/**', '**/*.md'] + # src/engine is in the filter because the manual lint checks the manuals + # against the parser (registered [SECTION]s, error codes): registering a + # section without documenting it must trip the gate. + paths: ['docs/**', 'python/docs/**', 'python/openswmm/**', 'include/**', '**/*.md', + 'src/engine/**', 'scripts/lint_manual_docs*.py', + 'scripts/gen_manual_frontmatter.py', 'scripts/build_manual_figures.py'] pull_request: - branches: [main, develop] + branches: [main, develop, swmm6_rel] # branches: [main] - paths: ['docs/**', 'python/docs/**', 'python/openswmm/**', 'include/**', '**/*.md'] + paths: ['docs/**', 'python/docs/**', 'python/openswmm/**', 'include/**', '**/*.md', + 'src/engine/**', 'scripts/lint_manual_docs*.py', + 'scripts/gen_manual_frontmatter.py', 'scripts/build_manual_figures.py'] workflow_dispatch: # Allow only one concurrent deployment. Do NOT cancel in-progress runs so @@ -19,9 +29,42 @@ concurrency: cancel-in-progress: false # ───────────────────────────────────────────────────────────────────────────── -# Job 1 – Build Python (Sphinx) documentation +# Job 0 – Lint the manual sources +# Referential integrity, tables, math delimiters, images, [SECTION] and +# error-code coverage, status badges, Mermaid blocks. Runs the negative +# suite too, so a check that silently stopped failing is caught here. +# Doxygen runs with WARN_AS_ERROR = NO (header warnings would drown it), so +# this job is what actually fails the build on a broken manual. # ───────────────────────────────────────────────────────────────────────────── jobs: + lint_manual_docs: + name: Lint Manual Sources + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Lint the manuals + run: python3 scripts/lint_manual_docs.py + + - name: Prove every lint check can fail + run: python3 scripts/lint_manual_docs_negative.py + + - name: Upload negative-test artifacts + if: always() + uses: actions/upload-artifact@v5 + with: + name: docs-lint-negative + path: tests/output/docs_lint_negative_*/*.log + if-no-files-found: ignore + retention-days: 7 + +# ───────────────────────────────────────────────────────────────────────────── +# Job 1 – Build Python (Sphinx) documentation +# ───────────────────────────────────────────────────────────────────────────── build_python_docs: name: Build Python Bindings Docs (Sphinx) runs-on: ubuntu-latest @@ -67,7 +110,7 @@ jobs: # ───────────────────────────────────────────────────────────────────────────── build_doxygen_docs: name: Build C/C++ Engine Docs (Doxygen) - needs: build_python_docs + needs: [lint_manual_docs, build_python_docs] runs-on: ubuntu-latest permissions: contents: read @@ -93,9 +136,61 @@ jobs: sudo mv doxygen-${DOXYGEN_VERSION}/bin/doxygen /usr/local/bin/doxygen doxygen --version + - name: Set up Python 3.12 (figure pipeline) + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install figure-pipeline dependencies + run: pip install "matplotlib>=3.8" numpy + + - name: Prove the synthetic figure generators run + # The committed PNGs are what gets published and were audited by the + # lint job (every embed has a manifest row, every row's file exists, + # bounds, no basename collisions). This renders the synthetic tier into + # a scratch directory to prove each generator still executes — never a + # pixel comparison. Generators that need the compiled openswmm package + # are skipped and listed. + run: python3 scripts/build_manual_figures.py build --tier synthetic --out /tmp/figcheck --skip-missing-deps + + - name: Check the GUI tagfile for drift (advisory) + # docs/tags/swmmvis-manual.tag is committed so the build never depends + # on the GUI Pages site being reachable. This only reports staleness — + # it must not fail the build, and it skips silently when offline. + continue-on-error: true + run: | + python3 scripts/refresh_gui_tagfile.py --check \ + || echo "::warning::docs/tags/swmmvis-manual.tag is stale; run scripts/refresh_gui_tagfile.py" + - name: Build Doxygen HTML docs + # docs/CNAME pins the Pages custom domain. It must sit at the ROOT of + # the published site, which is the Doxygen output — the Python docs + # land under /python and would put it one level too deep. working-directory: docs - run: doxygen Doxyfile + run: | + doxygen Doxyfile 2>&1 | tee doxygen.log + cp CNAME html/ + + - name: Fail on manual-scoped Doxygen problems + # WARN_AS_ERROR stays NO (the C++ headers carry ~1,200 warnings), so + # the gate is scoped: a missing image/input path, a page label declared + # twice (which silently re-points every @ref at the wrong page), a + # dangling @ref into any manual or the GUI tagfile, and a \snippet + # whose marker pair is broken. Each pattern has been seen to fire. + working-directory: docs + run: | + set -euo pipefail + fail=0 + for pat in 'does not exist or is not a file' \ + 'multiple use of page label' \ + "unable to resolve reference to '(application_manual|engine_manual|hydraulics_ref|hydrology_ref|quality_ref|manual_|tutorial_)" \ + 'should appear twice|included file .* is not found'; do + if grep -E "$pat" doxygen.log; then + echo "::error::doxygen.log matches '$pat'" + fail=1 + fi + done + exit $fail - name: Upload Doxygen docs artifact uses: actions/upload-artifact@v5 diff --git a/.github/workflows/python_contracts.yml b/.github/workflows/python_contracts.yml new file mode 100644 index 000000000..a542c8d66 --- /dev/null +++ b/.github/workflows/python_contracts.yml @@ -0,0 +1,55 @@ +name: Python API contracts + +permissions: + contents: read + +on: + workflow_call: + workflow_dispatch: + push: + branches: [main, develop, swmm6_rel] + pull_request: + branches: [main, develop, swmm6_rel] + +jobs: + contracts: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: python -m pip install pytest + - name: Check native API reachability + run: python python/scripts/api_drift_audit.py --strict + - name: Check enums, stubs and Cython attributes + working-directory: python + run: python -m pytest tests/test_api_coverage.py tests/test_enum_drift.py tests/test_solver_pxd_attrs.py -q + + minimal-build: + name: Python without optional features + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: python -m pip install 'cmake>=4.3' ninja 'Cython>=3.0.12' numpy pytest + - name: Configure minimal bindings + run: >- + cmake -S python -B build/python-minimal -G Ninja + -DCMAKE_BUILD_TYPE=Release + -DOPENSWMM_BUILD_2D=OFF + -DOPENSWMM_BUILD_GPU_PLUGIN=OFF + -DOPENSWMM_WITH_GEOPACKAGE=OFF + -DOPENSWMM_WITH_HDF5=OFF + -DPython_EXECUTABLE=$(which python) + - run: cmake --build build/python-minimal --parallel 2 + - run: cmake --install build/python-minimal --prefix "${{ runner.temp }}/openswmm-minimal" + - name: Stage Python sources + run: python python/scripts/stage_python_sources.py "${{ runner.temp }}/openswmm-minimal" + - name: Exercise omitted-feature imports and core solver + run: >- + python python/scripts/test_staged_package.py "${{ runner.temp }}/openswmm-minimal" + python/tests/minimal_features.py python/tests/engine/test_solver_pythonic.py + python/tests/engine/test_transport.py -q diff --git a/.github/workflows/regression_testing.yml b/.github/workflows/regression_testing.yml index 6a6323c8f..fbcf34834 100644 --- a/.github/workflows/regression_testing.yml +++ b/.github/workflows/regression_testing.yml @@ -1,5 +1,8 @@ name: Regression Testing +permissions: + contents: read + on: push: branches: [main] @@ -57,7 +60,7 @@ jobs: uses: actions/checkout@v5 with: repository: microsoft/vcpkg - ref: 2025.02.14 + ref: 2026.07.29 path: vcpkg - name: Install OpenMP (macOS) @@ -108,6 +111,25 @@ jobs: python -m pip install --upgrade pip python -m pip install pytest numpy + - name: Set up MSVC environment (Windows) + # The Windows preset uses the Ninja Multi-Config generator, which needs + # cl.exe (and ninja) on PATH — nothing else puts them there. vcpkg's + # own port builds locate VS by themselves, so the gap only surfaces at + # the outer configure ("CMAKE_C_COMPILER not set"). + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: Re-pin VCPKG_ROOT (Windows) + # vcvars64 (VS 2022 17.6+) exports VCPKG_ROOT pointing at Visual + # Studio's bundled vcpkg, and msvc-dev-cmd forwards it to every later + # step, overriding the job-level env — so configure would silently + # build against VS's port snapshot instead of the pinned checkout. + if: runner.os == 'Windows' + shell: pwsh + run: echo "VCPKG_ROOT=${{ github.workspace }}\vcpkg" >> $env:GITHUB_ENV + - 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 @@ -163,6 +185,27 @@ jobs: OPENSWMM_BUILD_DIR: ${{ github.workspace }}/build-${{ matrix.vcpkg_triplet }} run: python -m pytest -v --tb=short + - name: Manual worked-example decks run to completion + # The manuals' simulated-tier figures and the Application Manual's + # worked examples cite decks under docs/figures/decks/. Every one must + # run clean (exit 0, no ERROR in the .rpt) under a per-deck budget, or + # the prose is describing a model that no longer works. Linux only: + # one platform proves the decks; the figures themselves are committed. + if: runner.os == 'Linux' + env: + OPENSWMM_BUILD_DIR: ${{ github.workspace }}/build-${{ matrix.vcpkg_triplet }} + run: > + python3 scripts/build_manual_figures.py run-decks + --out tests/output/manual_decks_ci --max-seconds 600 + + - name: Upload manual deck run summary + if: always() && runner.os == 'Linux' + uses: actions/upload-artifact@v5 + with: + name: manual-decks-${{ matrix.vcpkg_triplet }} + path: tests/output/manual_decks_ci/summary.tsv + if-no-files-found: ignore + - name: Install Release tree run: > cmake --install build-${{ matrix.vcpkg_triplet }} diff --git a/.github/workflows/typing.yml b/.github/workflows/typing.yml index 80ad77769..ce69a8224 100644 --- a/.github/workflows/typing.yml +++ b/.github/workflows/typing.yml @@ -1,19 +1,28 @@ name: Typing (mypy) +permissions: + contents: read + on: push: - branches: [main, develop] + branches: [main, develop, swmm6_rel] paths: - 'python/openswmm/**/*.py' - 'python/openswmm/**/*.pyi' + - 'python/openswmm/**/*.pyx' + - 'python/openswmm/**/*.pxd' + - 'include/openswmm/engine/**' - 'python/tests/typing/**' - 'python/pyproject.toml' - '.github/workflows/typing.yml' pull_request: - branches: [main, develop] + branches: [main, develop, swmm6_rel] paths: - 'python/openswmm/**/*.py' - 'python/openswmm/**/*.pyi' + - 'python/openswmm/**/*.pyx' + - 'python/openswmm/**/*.pxd' + - 'include/openswmm/engine/**' - 'python/tests/typing/**' - 'python/pyproject.toml' - '.github/workflows/typing.yml' diff --git a/.github/workflows/unit_testing.yml b/.github/workflows/unit_testing.yml index e1e1788e4..b66b39a0f 100644 --- a/.github/workflows/unit_testing.yml +++ b/.github/workflows/unit_testing.yml @@ -1,5 +1,8 @@ name: Unit Testing +permissions: + contents: read + on: push: branches: [main, develop] @@ -23,7 +26,6 @@ jobs: name: "C++ Engine (${{ matrix.alias }})" permissions: contents: read - actions: write strategy: fail-fast: false matrix: @@ -65,7 +67,7 @@ jobs: uses: actions/checkout@v5 with: repository: microsoft/vcpkg - ref: 2025.02.14 + ref: 2026.07.29 path: vcpkg - name: Install OpenMP (macOS) @@ -117,6 +119,15 @@ jobs: with: arch: x64 + - name: Re-pin VCPKG_ROOT (Windows) + # vcvars64 (VS 2022 17.6+) exports VCPKG_ROOT pointing at Visual + # Studio's bundled vcpkg, and msvc-dev-cmd forwards it to every later + # step, overriding the job-level env — so configure would silently + # build against VS's port snapshot instead of the pinned checkout. + if: runner.os == 'Windows' + shell: pwsh + run: echo "VCPKG_ROOT=${{ github.workspace }}\vcpkg" >> $env:GITHUB_ENV + # 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 diff --git a/.github/workflows/unit_testing_python.yml b/.github/workflows/unit_testing_python.yml index af97e94bc..841d11d58 100644 --- a/.github/workflows/unit_testing_python.yml +++ b/.github/workflows/unit_testing_python.yml @@ -1,19 +1,31 @@ name: Unit Testing Python +permissions: + contents: read + on: push: - branches: [main, develop] + branches: [main, develop, swmm6_rel] pull_request: - branches: [main, develop] + branches: [main, develop, swmm6_rel] env: VCPKG_ROOT: ${{ github.workspace }}/vcpkg VCPKG_EXE: ${{ github.workspace }}/vcpkg/vcpkg VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" - VCPKG_MANIFEST_FEATURES: "tests" + # `gpu` (Kokkos) is required, not optional: the base wheel builds the + # Kokkos-OpenMP surface plugin (python/CMakeLists.txt sets + # OPENSWMM_BUILD_GPU_PLUGIN=ON and VCPKG_MANIFEST_FEATURES=2d;gpu), and its + # find_package(Kokkos REQUIRED) fails configure outright if Kokkos is absent. + # Stated here as well so this workflow's feature set cannot drift out of step + # with the wheel's and fail late, in the middle of a matrix. + VCPKG_MANIFEST_FEATURES: "tests;2d;gpu" OMP_NUM_THREADS: 1 jobs: + python-contracts: + uses: ./.github/workflows/python_contracts.yml + # ────────────────────────────────────────────────────────────────────── # Python bindings — cibuildwheel matrix (one runner per OS/arch). # @@ -26,10 +38,10 @@ jobs: # See docs/CIBUILDWHEEL_REVERT_PLAN.md. # ────────────────────────────────────────────────────────────────────── python: + needs: python-contracts name: "Python (${{ matrix.os }})" permissions: contents: read - actions: write strategy: fail-fast: false matrix: @@ -59,7 +71,7 @@ jobs: uses: actions/checkout@v5 with: repository: microsoft/vcpkg - ref: 2025.02.14 + ref: 2026.07.29 path: vcpkg - name: Bootstrap vcpkg (macOS) diff --git a/.gitignore b/.gitignore index be43b5b91..691dc6574 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,17 @@ # Working notes not intended for version control doc/FlashX_SWMM_crosswalk.md doc/Verification_Implementation_Plan.md -/plans/ +# Plans, handoffs, verification notes and generated result write-ups are +# working records, never committed (user decision 2026-09-30; this reverses the +# 2026-09-01 decision to track plans/transport). The plans/parity tooling and +# its .tsv/.json data stay tracked. +/plans/* +*HANDOFF*.md +*_PLAN.md +*_PLAN_*.md +*VERIFICATION*.md +/tests/benchmarks/generated/LOAD_RESULTS_*.md +/tests/manual/*/RESULTS.md /tools/ docs/2D_MULTISCALE_FORMULATION_REVIEW.md # Ignore examples/ contents (not the dir itself, so the fixture below can be @@ -13,6 +23,12 @@ docs/2D_MULTISCALE_FORMULATION_REVIEW.md # Keep the canonical 2D fixture — tests/unit/engine/CMakeLists.txt and # python/tests/engine/{test_2d_evap,test_surface2d_view}.py load it from here. !/examples/2d_complete_example.inp +# Keep the street-inlet / inlet-junction example (deck + README only; the +# .rpt/.out a run writes next to them stay out). +!/examples/inlets/ +/examples/inlets/* +!/examples/inlets/street_inlet_junction.inp +!/examples/inlets/README.md # Eclipse Stuff .metadata/ @@ -66,7 +82,11 @@ python/tests/engine/output/ python/tests/legacy/output/* !python/tests/legacy/output/b8_aq_probe.inp python/tests/output_pet_parity/ -tests/verification/ +# Scratch verification runs (d2_retirement/ is ~88 .rpt/.out/.log artifacts). +# Trailing /* rather than /, so gw2d_gates below can be re-included: git +# cannot re-include anything under an excluded *directory*. +tests/verification/* +!tests/verification/gw2d_gates/ tests/regression_testing/p1c_parity/* !tests/regression_testing/p1c_parity/compare.py tests/regression_testing/p5_parity/ @@ -98,3 +118,18 @@ tests/unit/legacy/engine/data/hotstart/site_drainage_model_save_hotstart.rpt # SQLite side-car files left when a GeoPackage fixture is opened WAL-mode. *.gpkg-shm *.gpkg-wal + +# Model-load benchmark corpus. Generated on demand by +# tests/benchmarks/scripts/gen_load_bench.py (326 MB at the plan's sizes) — +# the generator is committed, its output is not. Results docs written here ARE +# committed, hence the negations. +tests/benchmarks/generated/*.inp +tests/benchmarks/generated/*.rpt +tests/benchmarks/generated/*.out +tests/benchmarks/generated/*.json + +# Working copies written by test_engine_output_reader_live (reviewable, not tracked) +tests/unit/engine/data/live_out/ + +# Figure-pipeline engine runs (regenerated by scripts/build_manual_figures.py build) +docs/figures/cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e69d1b6a2..d0ccbe792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Version boundaries in this file: `6.0.0-alpha.2` covers work merged after the `v6.0.0-alpha.1` tag up to and including 2026-07-12; `6.0.0-alpha.3` covers -everything from the `6.0.0-alpha.3` version bump (2026-07-12) onward. No +everything from the `6.0.0-alpha.3` version bump (2026-07-12) up to the +`v6.0.0-alpha.3` tag (2026-08-11); `6.0.0-alpha.4` covers everything from that +tag to the `6.0.0-alpha.4` version bump (2026-09-08). No `v6.0.0-alpha.2` tag was ever cut — that version string lived only in `CMakeLists.txt` (`OPENSWMM_PRERELEASE`), `vcpkg.json` and `python/pyproject.toml` — so the `[6.0.0-alpha.2]` heading below is @@ -21,8 +23,1587 @@ retroactive. ## [Unreleased] +### Python bindings + +- **Add `openswmm.engine.catalog`**, a machine-readable description + (`catalog.json`, shipped in the wheel) of every service, element kind, + sub-view and standalone class the Python API exposes: each property's type, + access, units and lifecycle phases, each method's signature, the C symbols + behind it and the bulk array that reads a whole collection at once. + `python/scripts/gen_catalog.py` generates it from the type stubs, Cython + sources and header docs; `--check` in the Python contracts workflow fails on + a stale catalog, an uncatalogued public class or a float field without a + unit kind (`dimensionless` is explicit). `catalog.unit_label(kind, + unit_system, flow_units)` gives the label for a model's units, and a runtime + test fails when a public attribute is missing from the stubs. The OpenSWMM + MCP server and `openswmm.gymnasium` address engine fields through it. +- The type stubs now declare `Subcatchment.loadings` (`LoadingsView`), the + container methods of `Aquifers` and `Snowpacks`, and the groundwater and + groundwater-transport option records. +- Add groundwater hydrology and transport views, surface-quality tables, + transport configuration/capability records, 2D rainfall diagnostics, batch + deletion, live output refresh, compatibility/staged serialization and the + remaining metadata and forcing accessors. +- Report snapshots now read live routing diagnostics and convert fractional + continuity errors into their documented percentage fields. +- Retain owners in 2D/editor views, reject stale or concurrent access, propagate + callback failures safely, and synchronize public enums, type stubs and report + totals. Ship the `py.typed` marker and distinguish omitted optional extensions + from broken native dependencies. +- Add native API/enum drift gates to CI and release qualification. Wheel tests + include modern, legacy, top-level and integration tests. Expand Sphinx guides + with authoring/runtime units, lifecycle, persistence and callback contracts. + + +### Documentation + +- **Every implemented formulation is now documented, and every planned one is + marked as planned.** A review of the five manuals against the engine found the + `[OPTIONS]` table missing about 48 parsed keys, 27 input sections and 22 process + component sections with no grammar anywhere, no chapter for any process on the 2D + mesh, nothing on the two-layer mesh aquifer or on surface quality, an application + manual of seven empty stubs, no conceptual diagram as an image, and a roadmap + still calling five shipped features unimplemented. The programme that followed: + + - **New reference chapters.** Hydrology 8 (distributed surface processes on the + mesh) and 9 (spatially explicit groundwater); Water Quality 10 (surface quality + and transport on the mesh); planned-formulation chapters for all three reference + manuals. Hydraulics 9 absorbed the two September bolt-on sections: the three + momentum closures become §9.2.1–9.2.3, quadrilateral cells §9.3.1, the two-plane + VFR closure §9.4.2, the full shallow-water face flux §9.5.10 and the + diffusive-wave law §9.5.11. + - **The Application Manual is now a formulation-choice manual**: thirteen chapters, + six interactive decision workflows, and ten in-tree decks under + `docs/figures/decks/`. Each worked chapter runs one deck under the alternatives + and reports what changed, against laboratory measurements where they exist. + - **A scripted figure pipeline.** `docs/figures/` carries one manifest, the + generators, and the committed PNG and SVG outputs; `scripts/build_manual_figures.py` + builds, audits and runs the decks. 83 figures are generated from source — the + conceptual process-and-formulation map in five per-manual variants, the engine's + own cross-section geometry, and every simulated figure from a run the deck gate + also executes. + - **Interactive diagrams.** Mermaid workflows and the process maps pan, zoom, and + open the chapter behind whatever you click, through + `docs/custom/js/manual-interactive.js` and `@ref` targets that Doxygen and the + lint both validate. + - **Status badges.** `\status{Implemented|Experimental|Planned|Retired}` reads its + vocabulary and colours from `docs/figures/status.py`, so the text, the badges and + the diagrams cannot disagree. + - **Gates.** The manual lint runs in CI with a 38-case negative suite that proves + every check can fail; the figure check audits the manifest against the manuals; + `scripts/gen_manual_frontmatter.py` generates the figure and table lists from the + captions, which found the water-quality manual listing eleven figures against + thirty-eight and inventing their captions. +- **The three EPA reference-manual monoliths are deleted.** They were excluded from + the build and cited by nothing; their content lives in the per-chapter files. + Recover any of them with `git show ^:"docs/manuals/reference/hydrology/SWMM 5 Reference Manual I (20150917).md"`. +- **`ROADMAP.md` reconciled against the engine.** Inlet junctions, Lagrangian + transport, transport on the mesh, multi-species reactions, heat and the mesh + aquifer move to Completed; the full shallow-water equations leave the deferred + list; the TPA high-celerity filling divergence is recorded as closed on 2026-09-12 + rather than pending, and the finite-volume key count is corrected to twenty live + and five retired. + +- **The user manual is retired and replaced by an Engine Manual** at + `docs/manuals/engine/`, split from it by audience. The graphical application is + documented in its own repository, so the chapters that described the retired Delphi + interface — Quick Start Tutorial, Main Window, Working with Objects, Working with the + Map, Printing and Copying, Add-In Tools, and Appendix C's 26 property editors, about + 2,900 lines — are gone. The engine material was kept and renumbered: the conceptual + model (Ch. 1), the input file reference (Ch. 2, formerly Appendix D), files and + command-line operation (Ch. 3, formerly Ch. 11), the status report and summary tables + (Ch. 4, formerly Ch. 9), the C API, Python bindings and plugins (Ch. 5, formerly + Ch. 13), and the error and warning messages (Appendix A, formerly Appendix E). + `git mv` was used throughout, so file history follows. The parent page carries a + *Where the old chapters went* table for anyone holding a citation to the old numbering. +- **Appendix A (Useful Tables) and Appendix B (Visual Object Properties) moved to the + SWMMVis manual** — they are values typed into an editor and per-editor property + dictionaries, not engine behaviour. The engine now links to them. +- **The engine site links into the GUI site with real Doxygen cross-references.** + `docs/tags/swmmvis-manual.tag` is a committed, pages-only tagfile (42 pages, 8 KB, + filtered from the GUI's published 6 MB tag) wired through `TAGFILES`, so + `@ref manual_*` and `@ref tutorial_*` resolve to correct deep URLs under + `CREATE_SUBDIRS`. It is committed rather than fetched at build time so the docs build + never depends on the other site being reachable; `scripts/refresh_gui_tagfile.py` + refreshes it and CI reports drift without failing. Only the `manual_`/`tutorial_` + namespace is imported — both repos declare a page called `authors`, and importing it + would have silently redirected every engine `@ref authors` to the GUI site. + `EXTERNAL_PAGES` is now `NO`, so the GUI's pages do not appear in the engine's index. +- **307 orphaned images removed** from the retired manual's `images/` directory. They + were referenced by nothing and were in neither `INPUT` nor `IMAGE_PATH`, so they had + never rendered. The ~38 reusable diagrams among them (conduit cross-section shapes, + HEC-22 grate and curb types, conceptual hydrology and LID figures, pump-curve types) + are listed in `tests/output/docs_migration_2026-09-19/figure_salvage_list.md` with the + commit to recover them from, to be restored by the change that rewrites the conceptual + model chapter and actually cites them. +- **Stale references cleaned up while rewiring**: `docs/Doxyfile` listed `./Updates.md`, + which does not exist; the nav bar's GitHub tab pointed at the archived + `HydroCouple/OpenSWMMCore`. `scripts/split_user_manual.py`, the one-time generator for + the retired tree, is deleted along with its input and output. + +### Performance + +- **Explicit FV solver: five bit-identical fixed-cost removals** (Phase 1 of + `plans/FV1D_CLOSURE_KERNEL_PERF_PLAN_2026-09-11.md`). Measured on the new Phase 0 + counters against a frozen base binary: `.out` byte-identical on all 28 synthetic + baseline rows and the 25-deck parity manifest (11 FV decks); every FV unit gate and the + full unit suite green. + - `advance()` no longer begins with a full-mesh depth inversion when the state is the + solver's own (`state_clean_`): nothing outside the solver writes `cell_a` between two + routing steps, so the refresh recomputed what was already there — 27-32 % of a reach + deck's step at one substep per routing step. + - `settleAccumulators()` returns at once when no LTS macro cycle has booked flux + (`acc_dirty_`): on every baseline deck LTS never fired a cycle, yet each re-tier drained + all-zero accumulators and ran two full-mesh refreshes. The LTS block's own census is + reused as the global path's when it falls through without re-tiering — one face sweep + per substep instead of two. + - A per-cell closure cache (`cell_ah_`/`cell_t_`/`cell_i1_`, filled wherever `cell_h` is + written) serves `faceSide`, the step census and the LTS stability bounds, which + re-evaluated the same section at the same stored depth several times per substep and + once per node-solve trial. `faceSide` also stops computing a ghost side's first moment, + which `computeFaceFlux` discards, and `relaxOneNode` no longer evaluates each face's + geometry twice. + - Work removed (Phase 0 → Phase 1, same substeps): depth inversions −57 % on Example1 + and −75 % on a 500-conduit reach at the default mesh (LTS on/off rows now equal), area + calls −40…50 %, first-moment calls −50…60 %. Wall clock is recorded in + `plans/FV1D_PERF_BASELINE_2026-09-11.md` once measured on a quiet host. + +- **Explicit FV solver: the dt census bounds a pass-through junction's ghost by the cell + it actually fluxes; mass-only node residual; species-only face stores** (Phase 1e, 1c, + 1g of the same plan). `faceSide` presents the far cell's centred state as a pass-through + junction's ghost, but both the face census and the per-cell LTS bound rebuilt a ghost from + `node_head − z_face`, which after a drop offset stands above the downstream crown and + carries the slot celerity the flux never sees. Both bounds now use the far cell's cached + state: East Boston 77 goes from 28.0 to 4.55 substeps per routing step and 123.8 to 60.5 s + at one thread (continuity 0.797 % unchanged, node-head rms difference 0.0008 ft on a + 22 ft signal, overflow series identical; transitions and the lab columns identical) — + cumulative since the Phase 0 base 206 → 60.5 s, now faster than DYNWAVE's 64.4 s. The + algebraic node solve's trial fluxes are mass-only (`riemannMassFlux`, bit-identical to + `riemannFlux().mass`) with one full flux at the accepted head; the contact speed and the + full flux record are stored only when species are carried. Both bit-identical on the + 31-deck manifest. Dev switches: `OPENSWMM_FV_CENSUS_PASS=0` (old bound), `OPENSWMM_FV_CFL` + (overrides FV_CFL for the Phase 4a sweep), `OPENSWMM_FV_LTS_FIT=1` (Phase 4c: a macro + cycle cut to fit the remaining routing-step window instead of global stepping; off by + default, measured in the baseline document). Phase 4b (laterals credited into a clean + pass-through junction's end cells) was already in the tree. + +- **Explicit FV solver: section blocks shared across conduits, junction solves in + parallel** (Phase 1f and 3d of the same plan). The mesh carried one geometry block per + conduit (~4.4 kB with the closure table): TwinOaks v2 has 5086 conduits and eleven + distinct sections, East Boston 183 and forty. `mesh.geom` now holds one block per + distinct (section, barrels, open); friction, losses, slope and the culvert curve move to + `mesh.conduit_*` arrays; results are byte-identical (31-deck manifest). The algebraic + junction solves run in parallel on both the global path and the LTS tier firing — only + junctions that run the bracketed root solve are distributed, pass-through and storage + nodes stay serial, and the serial path is an explicit branch (a disabled pragma still + enters the runtime; on the 5-day East Boston deck the loop runs 13 M times and the + clause form cost 30 % at one thread). Bit-identical at any thread count. Measured on a + quiet host: TwinOaks 129.5 → 116.7 s at 1 thread and 103.0 s at 2 (183 → 125 s at 8); + East Boston 120.0 → 112.3 s at 1 thread and 104.7 s at 2. On this P+E-core host 8 + threads is slower than 1 on both decks (East Boston is authored `THREADS 8`); the + OpenMP loop gates take `OPENSWMM_FV_OMP_MIN_{CELLS,FACES,NODES}` overrides for sweeps. + Cumulative since the Phase 0 base: East Boston 206 → 105 s, TwinOaks 153 → 103 s. + +- **Explicit FV solver: cross sections from exact geometry, one POD closure kernel** + (Phase 2 of the same plan). FV no longer reads the legacy 51-row section tables + through the 25-way shape switch: `SectionGeometry.hpp` evaluates each section in + closed form at build time (circle segment, rectangle, trapezoid, triangle, parabola, + power; the arch/egg/horseshoe/… families and SWMM's elliptical pipes stay defined by + their tables), and `FvClosureKernels.hpp` samples it into a fixed-size, pointer-free + `FvClosure` — a monotone cubic-Hermite table (128 panels, Fritsch–Carlson limited) on + which the top width IS the derivative of the area and the first moment IS its + integral; RECT_OPEN / TRAPEZOIDAL / TRIANGULAR take an exact polynomial class with a + closed-form inverse. `depthOfArea` is a Newton iteration on the panel cubic instead + of Brent on tables that were never a derivative pair. Every hot-path function is an + `OPENSWMM_KERNEL_FN` over the POD struct (device-capturable, asserted by test). + Measured legacy → exact on one binary (`plans/FV1D_PERF_BASELINE_2026-09-11.md` + §Phase 2): circular inversion 3.3×, rect_open 7.9×, trapezoid 14.7×; same-moment wall + clocks base → Phase 1 → Phase 2 on reach_uniform_500 27.6 → 11.6 → 6.0 s, Example1 + 10.7 → 6.5 → 3.2 s. SWASHES `1d-fv` (33 cells) and the transitions suite are + IDENTICAL legacy vs exact; the explicit-slot lab column (e2_2006 C1) moves by 5e-4 + NSE. The legacy table/Brent path (the `XsectEval` pointer, the 129-row I₁/A table, + the area-uniform depth table, `depthOfAreaBracketed`, Brent) is deleted in the + follow-up commit once every `.out` of the exact path hashed identically without it; + `FvGeometry` is a pointer-free POD block apart from the build-time `XSectParams` it + keeps for the table-defined shapes. DYNWAVE is untouched (still bit-exact on the + legacy tables). + - **Implicit pressurized head update: secant storage correction.** With a closure + whose crown is consistent, the tangent linearization of the head row overshot + rising cells into the Preissmann slot (head spikes on the e2 rapid-fill implicit + column, divergence on e3 at c = 300 m/s). `PressurizedHeadSolver::solve` now + re-solves with the secant compliance of each cell's own closure (three Picard + passes, dev knob `OPENSWMM_FV_PRESS_SECANT_PASSES`). This changes results only under + `FV_PRESSURIZED_IMPLICIT YES`; default decks are byte-identical before/after. Known + residual (follow-up item): the map is not contracting on very stiff slots, so + `FvUnsteadyFriction.ValveClosureDampsOnImplicitPath` (c = 1000 ft/s) stays red + until a converged Newton on the nonlinear storage lands. + - **Integration gates re-based** (`test_fv_engine_integration.cpp`): the culvert + inlet-control and SURCHARGE_DEPTH fixtures run under the implicit head update — + measured on the explicit slot path the 3 ft culvert entrance locks pressurized at + 105/110/115/130/150 cfs and passed the fixture's 120 cfs only by luck of the table + closure (both closures pass every inflow with the implicit update, matching + DYNWAVE) — and the surcharge gate asserts the level the sealed node reaches; + mesh-refinement consistency is measured against FV's own dx = 10 answer (0.014 → + 0.012 → 0.004) with DYNWAVE agreement kept as a floor; the storage-loss continuity + gate is 0.2 % (the deck's own ledger error is 0.04 % under either closure). + +### Added + +- **Two-zone groundwater results in the 2D results file (G-O).** A model + with a resolved `[2D_AQUIFER]` now writes its aquifer to the `.h5` under + the new `REPORT_2D_VARIABLES` group `GROUNDWATER` (in DEFAULT): ten + per-cell `[time, face]` fields — `Mesh2_face_gw_table_elev`, `_hg` + (saturated thickness), `_hu` (closure A unsaturated storage), `_recharge`, + `_lateral`, `_node_exchange`, `_deep`, `_et`, `_dunne`, `_infil_in` (the + rates are each cell's last firing, held, as the C API reads them) — the + static `Mesh2_face_gw_bed_elev` and `Mesh2_face_gw_closure` (0 closed + form, 1 enslaved, 2 sigma — the mask for the σ block), the domain ledger + series `groundwater_ledger [time, 11]` (the `SWMM_GW2D_LED_*` order plus + the continuity residual), and `groundwater_node_exchange_cum [time, bed]` + with a `node_names` attribute (cumulative aquifer ↔ node exchange per + `[2D_AQUIFER_NODE]` bed, + out of the aquifer — the series behind the + GUI's GwExchange plot). `GW_DETAILED` (not in DEFAULT) adds the σ columns + `Mesh2_face_gw_theta_sigma [time, layer, face]`, the one m × nFace + variable. At finalize the file gains a `/groundwater_2d` group of ledger + scalars with a `continuity_error` attribute beside `/mass_balance_2d`, + and the `.rpt` gains a "2D Aquifer Continuity" block after the 2D surface + block. The storage views the C API used (`liveStorage`, `ledgeredStorage`, + the residual) moved onto `SubsurfaceState` so the file, the report and + `swmm_gw2d_get_continuity_error` share one definition. Gate: + `test_engine_2d_output_options` `DeckGroundwaterFieldsEndToEnd` — every + dataset present with the right shape on a two-triangle and a mixed + tri + quad deck, water table ≡ bed + hg per record, the ledger's last row + closes to 1e-6 of storage, a deck without an aquifer writes none of it. + Program plan row G-O; unblocks stream U (GG5) and the σ-column inspector. +- **2D cell coverages, buildup, washoff and street sweeping (S7).** The + subcatchment land-use convention on the mesh: `[2D_COVERAGES]` (`*` | `TAG + name` | `CELL n` → a land-use percent set; a row replaces its scope's set, + GLOBAL < TAG < CELL), `[2D_LOADINGS]` (initial buildup per acre | hectare, + a pollutant or a reactions-component species) and `[2D_CURB_LENGTH]` + (needed by PER_CURB land uses). Every cell then runs the subcatchment + surface-quality step's own arithmetic — POW / EXP / SAT / EXT buildup + through the inverse-days form, EXP / RC / EMC washoff with legacy's unit + pre-multiplies, the available-buildup cap, BMP removal, per-(cell, land + use) sweeping on the shared calendar — at the runoff-step cadence + (`2d/quality/SurfaceQuality2D`). The runoff rate a cell feeds the washoff + laws is its **net** outflow per unit area over the step (what left across + faces, boundaries and drains minus what arrived across faces, spills, + discharge and inflow boundaries; rain and groundwater return are + generation), accumulated by the marcher at the sites the species ledgers + already book — the cell analogue of the subcatchment's `outflow / area`, + and independent of how finely a plane is meshed. Washoff mass enters the + cell's species row (ledger `gained_washoff`) and reaches the 1D node + through the existing coupling tuple; no new coupling row. Outputs: + `REPORT_2D_VARIABLES` token `BUILDUP` (in DEFAULT) → `Mesh2_face_buildup + [time, species, face]` (lbs/acre | kg/ha, `species_names`), a `.rpt` + "2D Surface Washoff Summary" block, the `.inp` writer echoes the rows. + C API (`openswmm_sq2d.h`): `swmm_2d_coverage_*`, `swmm_2d_loading_*`, + `swmm_2d_curb_length_*` (row edits share the file's parsers; names are + validated at once; refused once initialized) and + `swmm_2d_get_buildup_bulk`. Ownership: rows are inert with one warning + under `RAINFALL_MODE NONE`; covered cells inside a subcatchment that has + `[COVERAGES]` raise one double-counting notice and the run proceeds. + Gates: `test_engine_2d_surface_quality` (buildup parity with a subcatchment + **bit-for-bit**, for a pollutant and an MSX species; EMC washoff = C × + runoff and Σ net cell outflow = drained volume to 1e-10 on a two-triangle + and a one-quad pan; ledger closure with sweeping and BMP; ownership + warnings; round trip and grammar refusals; inert rows bit-identical; C-API + grammar) and the `.h5` / C-API end-to-end gate in + `test_engine_2d_output_options`. Not in v1: co-pollutant fractions on + cells, `Mesh2_face_washoff`, hotstart carry of the store, Python / MCP + mirrors, GeoPackage rows. Design: `OVERLAND_TRANSPORT_HEAT_MSX_PLAN` §8 + (D-A22 revised 2026-09-19 to the net-outflow definition). +- **MSX species build up and wash off (BW-MSX).** `[BUILDUP]`, `[WASHOFF]` and + `[LOADINGS]` rows may name a reactions-component species (declared in the + component's `.rxn`), not only a `[POLLUTANTS]` entry. Such rows are parked by + name at parse and bound after the component is applied (the `[INITIAL_QUALITY]` + / `[INFLOWS]` deferral); they run the pollutant surface-quality step's own + arithmetic — POW / EXP / SAT / EXT buildup through the inverse-days form, + EXP / RC / EMC washoff with legacy's unit pre-multiplies, the available- + buildup cap, BMP removal and street sweeping on the shared (subcatchment, + land use) schedule — over a separate MSX-keyed store (`ReactionData::surface`, + `quality/MsxSurfaceQuality.cpp`), and deliver the washoff into + `msx_ext_mass_in`, the rate the ARD engine, the legacy MSX dispatch and LARD + already consume for species loads. Mass units per species follow the species' + declared units (`MG` → lbs/kg via UCF(MASS), `UG` → ÷1000, other → 1). The + `.inp` writer emits the rows; `swmm_buildup_get/set`, `swmm_washoff_get/set` + and `swmm_subcatch_get/set_initial_loading` address species at index + `n_pollutants + m` (Python: `set/get_buildup`, `set/get_washoff` and the + subcatchment `LoadingsView` accept a species name or that index); the `.rpt` + gains a "Subcatchment MSX Washoff Summary" + block with the surface ledger; deleting a land use re-packs the species + matrices. A row naming neither a pollutant nor a species warns (one line per + row) and is ignored. Not in v1: co-pollutant fractions for species and + kinetics inside the dry store (D-A28 / D-A29). Gate: + `test_engine_msx_buildup_washoff` — an MSX species with a pollutant's + parameters produces the pollutant's surface loads and final buildup + **bit-for-bit**, the ledger closes to 1e-10, pollutant trajectories are + bit-identical with the species rows present, round trip through writer and + C API, MSX-only decks. Design: `OVERLAND_TRANSPORT_HEAT_MSX_PLAN` §8.9. +- **`test_engine_fv_section_geometry`**: the exact section geometry against an + independent integration of the outline, the closure against the geometry (area, top + width, hydraulic radius), T ≡ dA/dh, I₁ ≡ ∫A, monotonicity, a 60 000-point + depth↔area round trip at 1e-12·y_full, the polynomial class in closed form, barrel + scaling, POD capture at a foreign address, and the recorded legacy-table error per + shape (circular: area 7.3e-4 of full, width 7.1 % of max, hydraulic radius 2.3 %). + `fv_perf_phase2_gates.sh` runs the Phase 2 gates as one job (bench, SWASHES, + transitions, lab cells, synthetic sweep; `FV_GATE_STEPS` selects steps) and + `fv_perf_compare_scores.py` diffs suite score files by verdict and metric. +- **FV solver instrumentation for the closure-kernel program.** `[PERF-FV]` gains + closure-call counters (`n.area n.width n.i1 n.hydrad`) and LTS macro-cycle counters + (`n.macro n.macrorej`); the report's "FV Solver Statistics" block gains + `LTS Macro Cycles Fired / Rejected`, which is what distinguishes "tiering never engaged" + from "tiering did not help" (the tier histogram alone cannot: it is filled whether or + not a cycle fits the routing step). `bench_fv_closure` times every closure kernel per + shape on the geometries the mesh builder builds (built in the tests tree too, no Google + Benchmark needed); `fv_perf_baseline.py` runs real decks as authored at THREADS 1/8 + (`--real-deck`), parses the new rows, and reports closure calls per substep; + `fv_perf_phase0.sh` runs the whole Phase 0 as one detached job and + `fv_perf_compare.py` is the mechanical Tier A hash gate between two runs. + ### Added +- **The 1D ⇄ aquifer quality seam carries mass (T7.4).** T7.1 left both 1D + seams moving water with no species: a recharging node and a leaking + conduit filled their cells with clean water, and a draining aquifer handed + its node nothing. All three now carry the tuple. A **leaking conduit** + delivers its own concentration into the cells it crosses, by the same + length weights the volume takes — the 1D quality solver already debited + that mass as its exfiltration loss, so this completes a transfer that used + to be a disappearance, and nothing is removed twice. A **recharging node** + sends its quality down the bed, sampled from the same published node row + the 2D surface spill reads, so the two 2D seams cannot disagree about what + a node is carrying; the 1D books the loss in `qual_routing_seep` — a row + it never had, which is why its quality continuity error previously grew by + exactly this. A **draining aquifer** hands its mass to the node's coupling + queues, the same channel the surface's drain uses, booked to + `qual_routing_gw_in`. MSX rows stay where they are: the 1D node has no MSX + inflow accumulator for any loader, the same recorded gap the surface seam + has. The gaining-conduit direction is still open — a conduit has no + per-link quality inlet to receive into. Gates: a leaking conduit's mass + arrives and the aquifer still conserves; a recharging node's mass arrives + and the 1D books the loss; **what the aquifer gives up is what the node + receives, to 1e-9, once the aquifer's conc·m³ is expressed in the 1D's + conc·ft³**. GW transport plan §3.5, T7.4. +- **The aquifer's transported tuple is visible and restartable (T7.5).** + T7.1 and T7.2 gave the two-zone kernel species, age and temperature; this + round gives them to the modeller. **Results file:** + `Mesh2_face_gw_sat_conc` and `Mesh2_face_gw_unsat_conc` + (`[time, species, face]` concentrations, one per zone, with a + `species_names` attribute) and `groundwater_species_ledger` + (`[time, species, 13]`, the per-species twin of `groundwater_ledger`, with + its `terms` attribute and a derived residual column), all riding the + existing `REPORT_2D_VARIABLES GROUNDWATER` mask and absent on a deck that + transports nothing. **Report:** a `2D Aquifer Quality Continuity` block + per species beside the water one — initial and final stored mass, every + inflow and outflow channel, and the continuity error — printed only when + the kernel carried a tuple, so a water-only deck's `.rpt` is unchanged + line-for-line. **Hot start:** a new **V6** block carries both stores and + the species ledgers, so a restart resumes a plume instead of re-seeding + it from `[GW_INITIAL_QUALITY]`; species are matched by NAME, because a row + layout depends on `[POLLUTANTS]`, the MSX list and the AGE / TEMPERATURE + switches, any of which can differ between the run that wrote the file and + the run that reads it — a species the reader lacks is dropped with a + warning, one the file lacks keeps its seed. **C API:** + `swmm_gw2d_species_count`, `swmm_gw2d_species_name`, + `swmm_gw2d_get_cell_conc` (per zone) and `swmm_gw2d_get_species_ledger`. + Gates: the C API agrees with the kernel cell for cell and term for term; + the `.h5`'s last record is the state the run ended in (at FLOAT64, so the + gate tests the plumbing rather than float32's last digit) and its residual + column closes; the `.rpt` block appears with the water block; a hotstart + round trip resumes both stores and continues the ledgers. GW transport + plan §3.4, gate 9, T7.5. +- **Dispersion, retardation and decay in the aquifer (T7.2).** The + transported tuple of T7.1 now spreads, sorbs and reacts. + **Dispersion** rides the lateral Darcy faces — `D = α_L·v_pore + D_m` from + `[GW_TRANSPORT_PARAMS]`, booked into the same side accumulators as + advection at the same cadence, so it inherits the marcher's cross-tier + consistency rather than needing its own argument. Its exchange is limited + exactly as the surface's is: at most the pairwise equalisation divided by + the receiver's face count (pairwise bounds do not compose — a cell fed by + three faces each closing its whole gap can end richer than every donor), + and at most a half-share of the giver's mass, with the signed temperature + row exempt from the mass share because it has no positivity to guard. + Binds are counted, not silenced. **Retardation** is linear-equilibrium + partitioning: a store holds the cell's TOTAL mass and every flux + multiplies by the dissolved fraction `1/R`, `R = 1 + ρ_b·K_d/θ` from + `[GW_SORPTION] Kd` and the grain density, so sorbed mass simply stays + where it is and no channel carries a retardation factor of its own. The + moving water table is the one exception — when it passes a grain, the + grain changes zone with it. **Decay** is first-order on the total mass of + both zones, per species, from `[GW_SORPTION] Decay` (1/day) falling back + to the `[POLLUTANTS]` Kdecay column, ledgered as `lost_reaction` so the + conservation statement still closes. `[GW_TRANSPORT_PARAMS]` and + `[GW_SORPTION]` resolve onto cells with the `* < TAG < CELL` precedence + the `[2D_AQUIFER]` rows use, and `[GW_TRANSPORT_OPTIONS] DISPERSION NO` + turns the first term off. Gates: dispersion narrows a seeded 25-against-5 + gradient and neither cell leaves the range its sources set; sorption cuts + what leaves the aquifer and keeps the rest; decay removes the analytic + `1 − e^{−kt}` fraction and the residual still closes to 1e-10. The + `SUBSURFACE` reactions-component adapter (`reactArdStage`, the hydvar + bindings and the EULERIAN_ARD element-state question) is **not** in this + round. GW transport plan §3.5, gates 1/2/5, T7.2. +- **The two-zone aquifer transports (T7.1).** With a `[2D_AQUIFER]` + resolved, the `[GW_*]` sections stop being authoring-only: the kernel now + carries a species / age / temperature tuple in two bulk stores + (`sat_mass`, `unsat_mass`), and every channel the water moves through + moves the tuple with it — lateral Darcy at the upwind donor's + concentration, recharge and capillary rise across the table, the moving + table's own handover between the zones, deep percolation, the node bed, + conduit seepage, saturation excess and column rejection back to the + surface, and ET, which carries water, age and temperature but no solute + (the column up-concentrates, program plan D-A20). Rows come from + `TransportPolicy` — the same authority the 1D and the 2D surface use, so a + species is the same row index in every domain — and the transport matrix's + groundwater row is real: `.rpt` and `swmm_get_transport_matrix` report + on(n) / off:KEY per class instead of a blanket "no transported quality". + `[GW_INITIAL_QUALITY]` seeds both zones (`ZONE SAT | UNSAT`; `LAYER` rows + are named as not applied while the unsaturated store is bulk). **The + surface seam is now a transfer, not a loss**: the mass infiltration takes + off a 2D cell arrives in the aquifer under it, and what saturation excess + pushes up arrives back on the surface (`gained_exfiltration`), each side + booking exactly what the other did. The "authored but INERT" warning now + fires only on a deck with no `[2D_AQUIFER]` for the rows to configure. + Gates: the GW plan's gate-6 conservation stub closes to 1e-10 on + all-triangle, all-quad and mixed meshes; the seam balances in both + directions net of what is in flight; ET leaves the solutes behind; the + matrix and the warning agree with each other. Heat still rides as a water + temperature-volume (the soil matrix's own capacity, conduction and the + thermal boundaries are T7.3), and dispersion, retardation and per-layer + resolution are T7.2. GW transport plan §3.4/§3.5, T7.1. +- **Conduits under the water table GAIN: the signed conduit ⇄ aquifer + conductance (G-X4).** `[2D_AQUIFER_OPTIONS] LINK_SEEPAGE TWO_WAY` replaces + a conduit's fixed `[LOSSES]` seepage loss with a MODFLOW-River exchange + about its own invert — `K·W·L·min((h_link − max(h_gw, z_inv))/d_c, 1)`, + `+` out of the pipe — so the loss is throttled as the table rises and + reverses once the table passes the water surface. This is the + groundwater-inflow term SWMM has never had for a conduit (only + subcatchment `[GROUNDWATER]` rows could feed a node). Three properties + hold by construction: a FULL pipe over a table at or below its invert + seeps exactly the legacy rate (`d_c` defaults to the conduit's full + depth), the exchange is continuous through the water table, and the gain + is bounded by the aquifer's own per-firing share of the cells the conduit + crosses — never by the pipe — so the 1D can never take water the 2D does + not have. `[2D_AQUIFER_LINKS] [KC k] [DC d] [EXCHANGE NO]` + overrides the conductivity and path length per conduit, gives a conduit + with no `[LOSSES]` rate an exchange, or keeps one out; rows round-trip + and a row naming an unknown link or a non-conduit is warned about, not + ignored silently. `LINK_SEEPAGE AUTO` (still the default) and `NONE` are + unchanged, and a deck without `TWO_WAY` takes the legacy branch at every + loss site. Ledger: the gaining half of the signed rate is booked to the + new `routing_link_gw_inflow` (`.rpt` "Conduit GW Inflow", printed only + when non-zero; `SWMM_ROUTING_LINK_GW_INFLOW`), never netted against + "Exfiltration Loss", and the aquifer is debited the same volume through + `led_link`. New `swmm_forcing_link_seepage` lets a host (HydroCouple's + `link_exchange_flow`) drive the exchange directly, OVERRIDE or ADD. + Gates: a drowned reach gains, the aquifer pays to 1e-9 and its table + draws down; a full pipe over a disconnected table reproduces the legacy + rate to 1e-12 while `AUTO` on the same deck still loses; `EXCHANGE NO` + keeps a conduit legacy; `KC` scales the gain linearly (damped by + drawdown); a forced rate is routed and booked on both sides. Program + plan §B.4b, D-A18 v2, G-X4. +- **Conduit seepage reaches the aquifer cells the conduit crosses (G-X3).** + A conduit with a `[LOSSES]` seepage rate used to lose that water to + nowhere; with a two-zone `[2D_AQUIFER]` under it, every routing step's + seepage volume — the same `rate × barrels × dt` the 1D books as + "Exfiltration Loss" — is now delivered into the saturated zone of the + cells its polyline (`[COORDINATES]` + `[VERTICES]`) crosses, split by the + exact length in each cell (a Cyrus–Beck clip against the convex cells; a + conduit half off the mesh delivers half). One-way and conservative: the + 1D still books the loss, the aquifer's new `led_link` term is where it + lands (continuity `in` gains it), and the table rises through the same + storage coefficient as the node and lateral volumes, Dunne excess if the + column is full. `[2D_AQUIFER_OPTIONS] LINK_SEEPAGE AUTO` (default) | + `NONE` (the legacy loss); the run reports how many conduits enrolled. + Surfaces: `.rpt` "Conduit Seepage Inflow" in the 2D Aquifer Continuity + block; `.h5` `Mesh2_face_gw_link_seepage` (m³/s per cell, held) and a + 12-column `groundwater_ledger` (`link` at index 10, the residual last — + read the `terms` attribute) plus `link_in` under `/groundwater_2d`; C API + `SWMM_GW2D_LED_LINK`, `SWMM_GW2D_VAR_QLINK`, option key `LINK_SEEPAGE`; + hotstart carries `led_link` as a 10th (length-prefixed) ledger term. + Gates: an evenly split conduit's seepage equals the aquifer's `led_link` + plus what is in flight to 1e-9 and raises the two columns identically to + 1e-12; `NONE` leaves the 1D bit-identical; a conduit half over the mesh + delivers half, all into the cell under it; one entirely off the mesh + enrols nothing. Program plan §B.4b, G-X3. +- **Every node inside the mesh exchanges with the two-zone aquifer (G-X2).** + `[2D_AQUIFER_OPTIONS] NODE_ENROLMENT AUTO` (the default) gives every node + whose `[COORDINATES]` fall in a mesh cell a direct-Darcy bed over the + cell's area — junctions, storage units and outfalls alike — so a + `[2D_AQUIFER_NODE]` row is only needed to override a bed's parameters or + to opt a node out: the row grammar gains `CELL AUTO` (locate the cell from + the coordinates; `swmm_gw2d_node_add` with cell −1) and `EXCHANGE NO` + (the node exchanges with nothing). `NODE_ENROLMENT ROWS` restores the + row-only behaviour. Auto-enrolled beds are not written back (the option + recreates them), authored rows round-trip with their new keywords, and + the run reports how many nodes enrolled. A node without authored + coordinates is never located (new `spatial.node_has_xy`). **One-owner + rule:** a storage unit with a bed exchanges through the conductance + channel and its own Green-Ampt exfiltration is switched off, named once + in the warnings; `EXCHANGE NO` keeps the legacy exfiltration. C API: + `swmm_gw2d_node_get_flags`, `swmm_gw2d_node_set_exchange`, option key + `NODE_ENROLMENT`. Gates: a junction with coordinates in a cell enrols, + exchanges and is not echoed; ROWS enrols nothing; `EXCHANGE NO` keeps a + node out and round-trips; an enrolled pond exfiltrates 0 on its own and + recharges through the bed, an opted-out pond exfiltrates as before. + Program plan §B.4b, G-X2. +- **Two-way node ⇄ aquifer exchange, the recharge direction capped (G-X1).** + `[2D_AQUIFER_NODE]` beds already drained the aquifer into a node at the + MODFLOW-River conductance rate with the drain capped at the cell's + drainable water; the reverse direction — a node standing above the water + table recharging the column — was uncapped. It is now limited by (i) a + saturation guard: a column whose table is at the ground takes nothing + (water it accepted would come back as saturation excess in the same + firing and ping-pong through the one-batch delivery lag); (ii) the + saturated zone's headroom, in the same per-step share as the drain side; + (iii) what the node can give — its stored volume as a per-batch budget + plus its through-flow (a junction stores nothing below its rim but can + lose water at the rate it is fed). Two things kept beds from exchanging + at all and are fixed with it: a bed under a node with no + `[2D_VERTEX_NODE_MAP]` entry never saw the 1D heads (the orifice coupling + alone published them), and the marcher's tail path — every routing step + that does not fit a full macro cycle, i.e. any variable 1D step — fired + the surface lists only, so the aquifer's lateral flow and node exchange + skipped those steps entirely. Gates: `FloodedManholeRechargesUntilThe + ColumnIsFullThenStops` (a surcharged junction recharges, the column + fills, the exchange goes to zero without ever reversing sign, no Dunne, + the recharge equals the storage gain to 1e-9, the 1D side books the same + water leaving) and `SpringOnDryGroundReachesTheNetworkThroughTheOrifice + Coupling` (exfiltration onto a dry cell drains into a junction; aquifer, + surface and 1D ledgers agree on the volume). Program plan §B.4b, gates + (b) and (c). + +### Fixed + +- **A CUMULATIVE rain gage rained for one step per table entry instead of for + its recording interval (issue #158).** A `CUMULATIVE` gage records a running + depth total: the intensity for a record is the rise since the previous + record spread over the gage's RECORDING INTERVAL, and legacy applies it for + the whole of that interval. Through `6.0.0-alpha.3` the gage was read as a + step-function lookup on its series rather than legacy's state machine, so + the intensity was applied for a single runoff step at each table entry. + Both reported symptoms follow from that one cause. Subcatchment runoff + showed a separate rise-and-recess at every entry instead of one continuous + hydrograph; and Total Precipitation came in short by the interval/step + ratio — 0.750 in against legacy's 9.000 in on the reporter's deck (an + hourly counter rising 1 in/hr against a `1:00` interval and a `00:05:00` + wet step), which is the reported "factor of 12", 3600 s / 300 s. The ratio + is the interval over the step, not a constant: a 15-minute cumulative + series on a `0:15` gage was short by 3. Fixed by the legacy state machine + (704ca917, 2026-09-13); ten cumulative decks — ramp, counter reset, zero + rise, interval shorter than the series spacing, non-zero first record, SI, + scale factor, co-gage, sub-hourly and late start — are now byte-identical + to legacy. Pinned by `test_engine_gage_cumulative`, which fails 8/8 against + an `alpha.3` build and passes 8/8 on this one. + +- **The aquifer's reported quality error was divided by the wrong total + (T7.4).** `.rpt`'s "2D Aquifer Quality Continuity" scaled its residual by + its own inline copy of "what came in" — `|init_mass| + |gained_infil| + + |net_lateral|` — written before T7.4 gave the aquifer two more inflow + routes, neither of which was added to it. On a network deck fed only + through its node beds that divides a machine-precision residual by a + machine-precision total: the block printed **6 198 889.840 %** beside a + balance exact to the last digit. The sum is now + `SubsurfaceTransportState::continuityDenominator`, which lives beside the + residual it scales, so a sixth channel cannot be added to one and + forgotten in the other. The balance rows themselves were always correct; + only the percentage was wrong. +- **ET's effect on water age in the 2D aquifer was implemented but ungated + (D-A20).** The kernel has always followed the program plan's D-A20 + convention — evaporation and ET take age-volume with the water at the + supplying layer's age, so the mean age of what remains is unchanged, while + solutes stay and the column up-concentrates. Nothing tested it: the only ET + gate asserted that the *solute* row lost nothing. Reverting the kernel to + the GW transport plan's superseded wording ("ET removes water not + age-volume") would therefore have left every gate green. Now gated, and the + plan's three stale statements — §2.4, the §3.5 ET row and gate 6's own + specification, which contradicted D-A20 — are amended to match the decision + and the code. +- **The node bed delivered 9.4× the water the aquifer released (G-W1).** The + cap on an aquifer→node drain measured the cell's drainable water with the + textbook specific yield θ_s − θ_r, while the column that then spent it used + the closure-consistent θ_s − θ_bot floored at 1e-3. For a table a few + centimetres under the ground those are 0.35 and 0.001, so the cap sat 350× + above anything reachable and never bound: the column went short on + essentially every firing, and the shortfall was refunded into the + **aquifer's** books alone — the router had already handed the unrefunded + volume to the node. Both continuity blocks closed and the seam still + created water: 0.625 m³ delivered against 0.067 m³ released on the gate + deck, 89 % of it refunded. Specific yield now has one owner, + `SubsurfaceSolver::specificYield`, which the cap and the column both call; + the refund no longer fires for the node at all, and the two sides agree to + the last digit. The defect needs a thin, near-saturated unsaturated zone + to bite — on a deck whose table sits 7.6 m up an 8 m column the cap never + binds and the numbers are unchanged — so the gate proves its own deck can + see it by counting the firings on which the cap actually clamped, and + asserts that the defect's signature (a column going short on the node's + account and being refunded) never occurs. The node→aquifer headroom cap has the same inconsistency + and is deliberately NOT changed here: making it consistent was attempted + twice and reverted twice — the floored yield leaves a saturated column + claiming 1e-3 of headroom it does not have (a surcharged node kept + trickling into a full column, which G-X1's gate caught), and the unfloored + yield takes the gate deck's 1D continuity from −0.66 % to −1.99 %. The + measured leak was on the drain side; the fill side needs its own round. +- **An aquifer species gate was passing on luck (G-W1).** T7.4's three seam + gates asserted `|residual| < 1e-10·|init|`, but `nacc_mass` — mass sampled + from a 1D node and not yet gathered by the cell — is counted in + `ledgeredStorage` while only being booked into `gained_node` at the + gather, so the balance is legitimately short by whatever is in transit. A + run that ends in that window carries the tail. Tightening the bed cap + above moved where the final sample landed and turned a tail that had been + ~0 into 0.0705 against an initial 614.9, which read exactly like an 11 % + mass leak; `residual` equalled `inFlight1D` to the last digit, which is + what identified it. The gates now assert `residual == inFlight1D` — the + identity completed, with the bound on it unchanged. +- **The aquifer quality block reported a perfect balance where it meant an + unmeasurable one (T7.4b).** When nothing had entered the aquifer on any + route the block had no scale to divide by, and printed `0.000` — which + reads as a balance closing exactly. It now prints `n/a`. This matters + beyond tidiness: with the denominator above naming every inflow route, a + zero scale beside a **non-zero** residual is precisely the signature of + mass leaving by a route nobody has booked the arrival of, which is the bug + class this stack has produced five times. `0 %` was the one answer that + would have hidden the sixth. +- **The aquifer→node transfer was counted in two inflow rows (T7.4b).** The + router queued the mass into `coupling_qual_queue` — which the quality + solver drains and books as `qual_routing_ex_in` — *and* booked + `qual_routing_gw_in` itself. One transfer, two inflow rows. On a deck whose + whole quality budget is aquifer → node → outfall it was a **49.7 % + continuity error**; the queue is now the only booking, which takes that + deck to ≈ −0.5 %. It was invisible until the fix below landed, because + while three quarters of the mass was being dropped the duplicated quantity + was ~0.001 lbs. The attribution cost is recorded rather than hidden: + **the `.rpt` quality continuity table's "Groundwater Inflow" row now reads + 0.000 on a deck whose only quality inflow is its aquifer, because that mass + moved to the 2D coupling inflow row — it did not vanish, and the table's + total is unchanged.** Restoring the dedicated row needs the queued mass + tagged by origin, since the queue is shared with the 2D surface's own + drain. +- **The aquifer delivered only a quarter of its drainage mass to the node + (T7.4, found by its own gate).** The per-bed weights that split a cell's + outgoing mass across the nodes it serves were accumulated over a routing + step and cleared at its flush — but a cell fires on the LTS ladder, not the + routing step, so any firing whose weights had already been cleared found no + weight to split by and dropped its mass. The aquifer still booked the + departure in `lost_node`, so the mass left one set of books without + arriving in the other: **75 % of it on the gate deck**. The weights are now + the volumes the gather itself takes, which makes them exact by + construction — they ARE what produced that firing's outgoing volume. The + seam delivers 100 %, and the two sides agree to machine precision. + +- **Infiltration from an inactive 2D cell never reached the aquifer.** The + marcher's cheap between-rebuild pass (`lazySourcesOnly`, taken on every + cycle that does not rebuild) sank infiltration off the surface and booked + it as a loss, but — unlike `syncAndRebuild`'s lazy pass, which G1-c item 1 + fixed — never handed the volume to the two-zone aquifer under it. Deck + continuity still closed, because a loss is a loss; the aquifer simply + never received that share of its recharge (1.4 % on T7.1's gate deck, and + more on a rain-on-grid deck where most cells stay inactive). Found by + T7.1's seam gate: the species could not balance while the water did not. + +- **A near-saturated closure-A column created water when anything withdrew + from it (found by G-X4's gate).** The two-zone kernel's closure A pairs the + saturated update's specific yield `Sy = θ_s − θ_bot` with a handover slab + of content `θ_bot`, so a moving table carries exactly `Sy·|Δh|`. When the + column is already at `θ_s` just under the table the difference collapses + and `Sy` hits its `1e-3` floor — but the handover kept using the raw + `θ_bot`, so the two no longer summed to `θ_s` and the cell gained + `(θ_bot − (θ_s − Sy_floor))·|Δh|·A` every firing. Invisible until a sink + pulled hard on a nearly-saturated column, which a conduit under a high + water table does: 0.08 m³ over half an hour on the G-X4 gate deck, an + 0.7 % continuity error. The slab content is now derived from the `Sy` + actually used, which makes the identity hold unconditionally and is a + no-op on every deck where the floor does not bind. + +- **ENSLAVED closure: the bracketed solve now converges on the volume.** The + G1-c bracket used a floored derivative, which across the capillary fringe + (θ(L) = θ_s, W flat in h) made Newton creep and leave the balance unmet by + up to 1e-5 m³ per firing — a −1.9e-4 m³ drift over a half-hour of two + full cells exchanging laterally. Newton now uses the true derivative and + drops to bisection where it vanishes, converging on the residual volume + (kernel gate 4 ENSLAVED: 2.3e-12 → 5e-14). +- **Two-zone aquifer: the ENSLAVED closure could zero the water table in one + firing (G1-c).** Its saturated-zone balance was solved by an unguarded + Newton from a linearised guess; with the table within centimetres of the + ground the surface water content is ≈ θ_s, the storage coefficient sits on + its floor, the guess lands above the ground and the clamped column makes + the derivative the floor too — one step of O(−1e5 m), the table zeroed, the + column re-evaluated at full length. Measured: the Dunne gate deck lost 10 + of the 10 m³ delivered (aquifer residual −32 %) and returned 0.001 m³ of + the ~8 m³ owed; a two-cell exfiltration deck created 23.5 m³. The balance + `W(h) = θ_s·h + hᵤ*(z_s − h)` is monotone, so it is now solved on + `[0, z_s]` by a safeguarded Newton with bisection fallback, and what the + interval cannot hold is booked exactly — above the ground as Dunne excess, + below the bottom as the sinks' refund. The kernel gates (`gw2d_gates`, all + closures) are unchanged to 1e-12; the Dunne deck now returns 9.95 m³ with a + zero residual. Every deck whose `[2D_AQUIFER]` resolves to ENSLAVED (AUTO + picks it for thin unsaturated zones) changes deliberately. +- **A dry surface froze the aquifer.** The marcher strode the whole window + when no surface cell was active, skipping the groundwater's lateral Darcy, + deep loss, ET and node exchange with it — a dry summer left the water + table exactly where the storm had put it. The macro cycle now runs while + an aquifer is live even when the surface is quiescent, and a dry cell the + aquifer exfiltrates into is pinned active by the rebuild's pending-cell + seed (the seed existed; nothing reached it). Gate: + `Aquifer2D.ExfiltrationReachesADryCellThroughThePendingSeed` — no rain, no + ponded water, lateral flow lifts a dry cell's table to the ground, the cell + receives exactly the Dunne volume, nothing stays parked, both continuity + errors zero. +- **The aquifer's own stability step now bounds the ladder's base step.** + `dt0` came from the active surface cells alone, so a groundwater cell with + a finer bound than every wet cell (or the whole domain with a dry surface, + where `dt0` fell back to `MAX_TIMESTEP`) fired beyond it on rung 0. The + rebuild folds `min(Δt_g, Δt_u)` over the aquifer into `dt0` (the fine side + of G1-c item 3; the coarse side — D-N1's runtime tier count — is still + open). +- **2D→aquifer recharge was booked one runoff step early (G1-c item 1).** + The 2D ledger's "to aquifer" transfer was booked when the marcher applied + the infiltration, the 1D groundwater ledger when the runoff step delivered + it, so the two disagreed by the routing steps between one drain and the + next — 3.6 % on the 30-minute U3 gate. The transfer is now booked at + delivery in the same loop as `gw_infil_2d_recharge`; the volume applied + but not yet delivered is `mass_balance_2d.infil_aquifer_pending`, and the + 2D continuity block prints both ("to Aquifer (delivered) / (in flight)"). + The U3 gate asserts the two ledgers equal to 1e-9 and delivered + pending + equals the applied loss. +- **A rain-on-grid deck with no subcatchment rained at its first record + forever.** The legacy rain-gage state machine (2026-09-13) keeps a gage no + subcatchment or unit hydrograph reads at its seeded first record — legacy's + "unused gage" rule — but the 2D mesh reads every gage under + `RAINFALL_MODE SYSTEM` / `NATURAL_NEIGHBOUR`, so a mesh-only deck saw its + first non-zero intensity for the whole run. `gageIsUsed` now counts the + mesh as a reader (S7 found it: the sweeping gate's dry tail never came). + Decks with a subcatchment on the gage are unaffected; the 25-deck parity + corpus is byte-identical; **47 rain-on-grid benchmark decks in the 2D + census change deliberately** (e.g. `2d_matrix/bench79202_t1.inp` rainfall + inflow 39601 → 6606 — it rained ~6× too long) — timing / bit-identity + baselines resting on them need regenerating. +- **2D transport rows on a mesh with no interior edge were silently inert.** + The marcher gated every species sink and source on its face accumulators + being non-empty, which is also true of a one-cell mesh with live rows + (`species_on_` now carries the intent; meshes with an interior edge are + bit-identical). +- **Street sweeping never fired on an `.inp` deck.** The per-(subcatchment, + land use) last-swept counters (`subcatches.sweep_last_swept`) were sized only + by the C API (`resize_coverage`) and the GeoPackage reader — never by the + `[COVERAGES]` handler — so the A7 sweeping loop skipped every index past the + empty array and "Sweeping Removal" read 0.000 on every deck read from a file. + `handle_coverages` now sizes the counters with the coverage matrix. Found by + the BW-MSX ledger gate; a deck with a nonzero `[LANDUSES]` sweep interval + now sweeps (legacy parity). The bit-identity corpus has no such deck and is + 25/25 byte-identical before and after. Two related parity gaps + remain and are recorded, not fixed: the `[LANDUSES]` LastSweep column is + parsed but never seeds the counter (legacy `lastSwept = start − sweepDays0`), + and a `[LOADINGS]` initial buildup is parked in `subcatches.conc` and never + applied to the pollutant buildup store (the new MSX store does apply its + loadings). +- **2D surface water age (S4b closeout).** (1) Evaporation now leaves at the + water's own mean age as well as its own temperature: the `__WATER_AGE__` row + is sunk proportionally with the evaporated volume (`sinkIntensiveRowsWithEvap`), + so a still pond no longer *ages* by evaporating — the 1D convention + ("evaporation leaves the mean age unchanged", `WaterAgeLegacy`) now holds on + the mesh. (2) `__WATER_AGE__` is authored in **hours** in `[2D_INITIAL_QUALITY]` + and `[2D_BOUNDARY_QUALITY]`, as in the 1D `[INITIAL_QUALITY]`, and the + `.h5` `Mesh2_face_species_conc` age row is reported in **hours** like every + 1D age column (it was seconds with `units = "1"`). (3) The dataset carries a + new `species_units` attribute parallel to `species_names` (`MG/L` | `UG/L` | + `#/L`, the MSX species' declared units, `hours`, `degC`); the engine + snapshot gains `surface_species_units`. (4) A deck with any transported row + (`TRANSPORT_*`, age, temperature) is always served by the CPU marcher and the + solver notice says why — the GPU/Kokkos plugin carries no transport rows and + used to be selectable with them silently absent. Gates: + `test_engine_2d_transport_s4` (evaporation keeps mean age; plugin refusal), + `test_engine_2d_output_options` (`species_units`; end-to-end hours). + Program plan D-A20 / D-A21. +- **Explicit FV: a lateral inflow into a dry pass-through junction is no longer lost.** A + clean degree-2 junction's lateral is credited straight into its two adjacent cells, and + the node marks it delivered — but under `FV_COMPACTION` (the default) dry cells are + inactive and the cell update, the only place the credit lands, skips them. The ledger + booked the inflow, no cell ever took it: on a 7 km tunnel filling from dry + (Klaver Example_02, 4.877 m circular in 5 m cells, seven lateral junctions) 100 % of the + first 70 minutes of lateral inflow at a junction vanished, 4.7 % of the run's total, until + the wetting front from upstream activated the cells; on East Boston the same mechanism + was the residual 0.8 % continuity error. A cell that receives a diverted lateral is now + active regardless of depth, and a credit into an inactive cell invalidates the active + lists. Klaver 4.691 % → −0.000 %, East Boston 0.797 % → −0.000 %, TwinOaks 4 h 0.000 %. + Gate `LateralIntoADryPassThroughJunctionIsNotLost` (a junction flanked by virtual + junctions, dry pipes: 100 % continuity error and zero outflow before the fix). +- **Explicit FV: a junction's own-head ghost no longer takes part in the slot/free bore + bound, and the vented-ghost crown-celerity cap is removed.** Marked free, the ghost of a + junction whose cell side stands in the slot too (a whole tunnel pressurized 50 m above + its junction crowns) made a "slot/free" pair of an acoustic face, the bound cut the + acoustic waves at every lateral junction, the algebraic solve found no head below its + ceiling, clamped at the rim and booked 63 100 m³ of "flooding" at junctions whose + published depth never exceeded half their rim (Klaver Example_02); marked by state, the + bound on a momentarily free cell beside it drove a 2× over-conveying limit cycle at a 20× + overloaded 0.5 ft entrance. The entrance lock the bound exists for lives at the INTERIOR + slot/free faces (measured: the bound alone releases it; the ghost celerity cap alone does + not, and with the ghost in the slot beside a momentarily free cell the cap fed the same + limit cycle), so the ghost is excluded (`FaceState::press == 2`) and the cap and + `FvGeometry::c_crown` are gone. Klaver: flooding 6.31 hectare-m → 0, the downstream weir + overflows 6.67 hectare-m as it should; the culvert fixture is unchanged; transitions + identical; lab e2_2006 C1/C2 within 0.003 NSE. `SurchargeDepthDelaysFlooding` asserts the + column on the AVERAGE depth: both runs open with a one-record startup overshoot (23–47 ft + on the first report line of a 10 cfs pour into a dry 0.5 ft pipe through a node with no + storage), a separate open item. +- **FV report: the per-node "Highest Continuity Errors" no longer reads a diverted lateral + as retained.** A lateral credited straight into the incident cells never crossed a node + face, so the face ledger held no outflow for it and the node's own continuity showed + 80–100 % at every lateral-fed pass-through junction (East Boston, TwinOaks, Klaver) while + the system balanced to 0.000 %. The solver books it as node outflow per substep on both + stepping paths (`bookDivertedLateral`), and a virtual junction's lateral likewise in the + publish. Report only: every `.out` is byte-identical. Nodes that see a few cubic metres + over a run still show large relative numbers, as in legacy. +- **Explicit FV: the wave entering a free surface at a Preissmann-slot interface is bounded + by the bore speed, not the slot's acoustic celerity.** The explicit slot path locked + pressurized at an entrance and flooded the junction however high its head stood (a 3 ft + culvert on a 5 % slope conveyed 96–136 cfs for every inflow from 100 cfs up; the + inlet-control fixture's 120 cfs passed only by luck of the legacy table closure). Face + traces showed Davis's symmetric estimate carrying the slot celerity into BOTH HLL waves + at a slot/free face, so the flux carried a diffusive mass term of order c_slot·ΔA (+52 cfs + at the node face) and a −245 ft⁴/s² momentum sink that pinned the entrance flux to the + cell's own momentum. Now: when exactly one side stands in the slot (`FaceState::press`), + the wave entering the free side is bounded by the Rankine–Hugoniot jump ΔQ/ΔA between + that side's own u ± c and the Davis bound (a junction's own-head ghost never takes part + in the bound — see the 2026-09-13 entry below); and the degree-1 prescribed-discharge + fallback skips an interior cell already in the slot (forcing the inflow into a full pipe + lifted its slot head by q·Δt/(t_slot·Δx) per substep — a 0.5 ft pipe fed 10 cfs chattered + its junction between dry and 40–80 ft; the fallback was also the culvert's + pressurized-reach deficit: 150 and 200 cfs now convey 150 / 200 at 2.85 / 10.90 ft against + DYNWAVE's 3.12 / 10.98). Measured (`plans/FV1D_PERF_BASELINE_2026-09-11.md`, Item 2): + the culvert passes 60–200 cfs like DYNWAVE at 53 instead of 173 substeps per step; + transitions `fv`/`fv-lts` identical; lab e2_2006 C1 within 0.0024 NSE, C2 st 0.56 → 0.84, + e4_aureli C1 p300 −9.7 → −2.3; the TPA high-celerity filling deck completes at + a = 150…3000 m/s where it diverged at every one (the P5b pin + `KnownIssueHighCelerityFillingDiverges` becomes the positive gate + `HighCelerityFillingCompletes`; `DivergenceGuardFailsLoudNotSilent` moves to a = 10 000, + which still diverges); `FvUnsteadyFriction.ValveClosureDampsOnImplicitPath`, the + implicit-path residual left red by the Phase 2 secant correction, passes again. Decks + that never pressurize are byte-identical. Dev switches `OPENSWMM_FV_MIXED_WAVE=0` and + `OPENSWMM_FV_DEG1=1` restore the previous estimates for one A/B cycle. +- **FILLED_CIRCULAR conduit offsets cross the C API as authored values.** Once + `resolve_cross_references` raised a partly filled circular conduit's stored offsets by the sediment + depth (legacy `link.c:1072-1077`), `swmm_link_get_offset_up/dn` reported the raised value, the setters + stored whatever they were handed, and `swmm_link_set_xsect` left `y_bot` stale on a live edit — while + the `.inp` and GeoPackage writers subtract the sediment depth unconditionally, so a GUI edit of such a + conduit saved offsets shifted by `yBot`. The getters and setters now remove / add the bump + (`link::filledCircularOffsetBump`; zero in the BUILDING state so `swmm_finalize_model` bumps exactly + once), `swmm_link_set_xsect` re-bases the stored offsets on every exit and derives a filled section + through `xsect::setParams` like the resolver does, and `swmm_conduit_split` stores an authored 0 at + the new junction. `test_engine_filled_circular_offset_api`. +- **The control-action log and the 2D marcher's telemetry no longer grow without bound.** With + `[REPORT] CONTROLS YES` every numeric control action pushed an entry carrying a heap string for the + whole run; entries are POD now (rule names interned by index), capped at 1 000 000 with the overflow + counted at the end of the report's *Control Actions Taken* block. `ExplicitInertialSolver` kept a + `(t, active cells)` sample per rebuild only to fold min / mean / max at `run_stats()`; it folds as it + samples and keeps the vector only when `OPENSWMM_2D_MARCHER_TELEMETRY` names a CSV. + +### Changed + +- **Host-reserved threads count toward oversubscription.** `OPENSWMM_HOST_RESERVED_THREADS` (the GUI + exports 3: its main, render and engine-IO threads) is added to the requested team before the + logical-CPU comparison, so a `THREADS 8` deck run inside the GUI on a 10-CPU machine takes the + passive-wait path instead of spinning eight workers against the render thread; the oversubscription + warning names the reserved count. `kmp_set_blocktime(INT_MAX)` is skipped when the host pre-set + `KMP_BLOCKTIME`, which the old comment promised but the runtime call ignored. `test_thread_info`. + +## [6.0.0-alpha.4] — 2026-09-08 + +### Fixed + +- **2D marcher performance regression after the tri-quad / FULL_SWE work** (`plans/2D_PERF_REGRESSION_DIAGNOSIS_2026-09-07.md`). + LOCAL_INERTIAL results are byte-identical before and after; measured on the Bellinge storm slice the + LI cell pass had grown ~17 % and start-up ~0.3 s. Fixes: `MeshData::n_quads()` (an O(n_cells) scan) was + re-run at the top of every face-tier fire — now resolved once in `initialize()`; the FULL_SWE momentum + code is compiled out of the LI/DW cell kernel (`fireCellsImpl`), and the quad VFR Newton is kept + out of line so `cellEtaDepth` inlines into the cell kernels again; species booking is skipped without + species; `buildCouplingPoints` uses a vertex→cell map instead of an O(n_couplings × n_cells) scan. + FULL_SWE / DIFFUSIVE_WAVE: the front-rebuild halo and frontier sweeps are cell-parallel instead of + serial edge passes (the tier pull-down stays serial — Gauss-Seidel propagates further per pass than a + Jacobi form, and the difference moves DIFFUSIVE_WAVE results); `FRONT_REBUILD AUTO` is on for FULL_SWE + only (a diffusive front never breaches the 1-ring halo within the 4-cycle cadence) — a DIFFUSIVE_WAVE + deck that wants the old cadence sets `FRONT_REBUILD YES` and is byte-identical again; dry-dry faces + leave the FULL_SWE face pass + early; WALL mirror fluxes come from a per-cell CSR built once instead of a neighbour scan on every wet + cell; the RK2 step allocates nothing and averages in parallel; the diffusive law reads a precomputed + `n_face`. `THREADS 0` now applies the Apple P-core clamp to the 2D marcher as it already did to dynamic + wave. Nothing changes for the Kokkos plugin (still all-triangle LOCAL_INERTIAL only). +- **2D `FULL_SWE`: a prescribed-discharge boundary now delivers exactly what it prescribes.** The + ghost-cell Riemann boundary returned the Riemann solver's own mass flux — a wave-speed-weighted + blend of the interior and prescribed states — so a `SPECIFIED_FLOW` / `RATING_CURVE` inlet + under-delivered (measured 9 %, 7 % and 21 % short on the SWASHES subcritical, transcritical and + shock bumps) and, at a supercritical inlet, ran backwards. The prescribed discharge is now the + mass flux verbatim, as the local-inertial law has always done; the ghost state supplies only the + momentum flux, and its depth follows the outgoing Riemann invariant on a subcritical inflow + (critical depth into a dry cell, interior depth otherwise). `NORMAL_FLOW` under `FULL_SWE` applies + the same Manning outlet law as the other closures instead of a zero-gradient ghost, which was + absorbing and drained a pond through an inert (zero-slope) outlet. Boundary momentum is rescaled + when the availability clamp shrinks the mass, and `settleAccumulators` now settles the momentum + accumulators with the mass, so a cell re-tiered between firings cannot strand or replay them. + SWASHES depth error: bump-shock 0.070 → 0.032 (gate 0.05), bump-transcritical 0.042 → 0.033. + All-triangle `LOCAL_INERTIAL` results are unchanged (byte-identical on four decks). + +### Added + +- **2D: `swmm_2d_get_run_stats` C API / `Surface2D.run_stats`** — the solver backend label, momentum + closure, configured `LTS_TIERS`, cumulative substeps and face-kernel evaluations, active-cell fractions + and per-tier LTS occupancy, readable while the run is in flight. The report and the warning callback + carry a `2D solver: …` advisory naming the backend and closure, and the Kokkos-plugin refusal for quad + meshes / non-`LOCAL_INERTIAL` closures is now printed under `BACKEND AUTO` too (a `DIFFUSIVE_WAVE` deck + used to fall back silently and read as a slowdown). `bookFaceSpecies` returns early on species-free models. +- **2D: momentum closures `MOMENTUM_EQUATION FULL_SWE | DIFFUSIVE_WAVE` and mixed + triangle/quadrilateral meshes `[2D_QUADS]`** (plans/2D_FULL_SWE_SHOCK_CAPTURING_PLAN_2026-09-05.md, + plans/2D_TRI_QUAD_MESH_PLAN_2026-09-06.md; handoff plans/HANDOFF_2D_TRIQUAD_FULLSWE_2026-09-06.md). + - `FULL_SWE`: conservative shallow-water equations with the convective term on the same explicit + marcher — cell (h, hu, hv), hydrostatic reconstruction, rotated HLLC Riemann flux with the + Audusse bed-slope correction (lake at rest exact, walls included), ghost-cell Riemann boundaries, + semi-implicit friction; `RECONSTRUCTION_ORDER 2` = MUSCL (Barth–Jespersen) + SSP-RK2 in global-dt + mode. SWASHES strips: Stoker 6.5 % → 0.7 % (0.3 % second order), Ritter 13 % → 1.7 %, subcritical + bump 6.8 % → 0.5 %, transcritical bump 27 % → 4.4 %, bump-with-shock 12 % → 7.3 %. + - `DIFFUSIVE_WAVE`: explicit Manning quasi-steady face law (Hunter et al. 2005) with the Δx² + step bound per cell absorbed by the LTS tiers. + - `FRONT_REBUILD AUTO|YES|NO`: breach-triggered active-set rebuild with a five-ring halo and + front-cadence tiers for dry halo cells (AUTO = on for the new closures, off for local-inertial). + - `[2D_QUADS]` convex quadrilateral cells (cells numbered triangles first, then quads; unified + edge rule `edge k = (v[(k+1)%nv], v[(k+2)%nv])`), padded stride-4 `MeshData` layout, quad + geometry (shoelace area, area centroid), Begnudelli & Sanders (2007) quad VFR closure + (`mesh/QuadVfr.hpp`, verified against a brute-force integration oracle), `β/nv` positivity + share, GeoPackage `mesh_2d_quads`, UGRID mixed-topology HDF5 output (`Mesh2_face_nodes [n,4]` + + `_FillValue` + `Mesh2_face_nv`), C API `swmm_2d_cell_count / quad_count / edge_stride / + cell_vertex_count / cell_get_vertices / cell_get_neighbours`, Python mirrors. + - All-triangle LOCAL_INERTIAL models are byte-identical (verified on four decks: `.h5` datasets, + `.out`, `.rpt`); the SWASHES harness gains the `2d-swe`, `2d-swe2`, `2d-dw` columns. + - `ADVECTION YES` is deprecated (warns on load; still honoured). The Kokkos plugin serves only + all-triangle LOCAL_INERTIAL models and hands everything else to the CPU marcher with a notice. + +- **Street inlets: legacy-parity HEC-22 capture kernel, `[INLET_JUNCTIONS]`, and an inlet + design/usage C API** — `[INLETS]` / `[INLET_USAGE]` were parsed, but the street geometry never + reached the capture calculation and capture ran after routing: + - `[INLET_JUNCTIONS]` and the inlet-junction node: a virtual junction (zero storage, exactly two + STREET conduits of the same section) that is *unsealed* — it floods above the street section + or its `MaxDepth` — and captures the arriving gutter flow to a capture node, with backflow when + that node surcharges. `Name Elev MaxDepth Inlet CaptureNode (#Inlets %Clog Qmax aLocal wLocal + Placement)`, written after `[VIRTUAL_JUNCTIONS]` and excluded from `[JUNCTIONS]` / + `[VIRTUAL_JUNCTIONS]`. Errors 623 (host conduits are not streets), 625 (unknown design), + 627 (capture node missing, the host itself, or virtual), 629 (`[INLET_USAGE]` row on an + inlet-junction conduit), 631 (extra tokens), 633 (inlet junction without a usage row); + warning 635 (design incompatible with the host section — legacy WARNING 12, row ignored). + - C API: `SWMM_InletDesign` with `swmm_inlet_get_design` / `set_design` / `get_comment` / + `set_comment`; `SWMM_InletUsage` with `swmm_inlet_usage_count` / `find_link` / `find_node` / + `get` / `set` / `remove`; `swmm_node_is_inlet` / `inlet_eligible` / `set_inlet`; + `swmm_conduit_split_inlet` (atomic split of a street conduit inserting an inlet junction) and + `swmm_inlet_junction_fuse`. Python: `Inlets.get_design` / `set_design`, `InletUsages`, + `Node.is_inlet`, `inlet_rule_violation`, `split_conduit_inlet`, `fuse_inlet_junction`, enums + `InletType` / `GrateType` / `ThroatType` / `InletCurveKind` / `InletPlacement` / + `InletHostKind` (`python/tests/engine/test_inlet_api.py`). + - HEC-22 kernel ported 1:1 from legacy `inlet.c` (on-grade grate / curb / slotted / combination + sweeper, on-sag weir-orifice, custom curves, drop inlets), run at the end of lateral-inflow + assembly — before routing, as legacy does — for conduit- and node-hosted inlets alike. + Report: legacy "Street Flow Summary" plus a "Street Inlet Flow Summary" with the legacy + performance columns (node hosts as `NAME (node)`). Example + `examples/inlets/street_inlet_junction.inp`; parity set `tests/parity/inlets/` (EPA 5.2 inlet + decks through both engines: capture efficiency at peak identical on every on-grade / on-sag + deck and physical captured volumes identical; the attributed divergences are listed in its + README). (`test_engine_inlet_capture`, `test_engine_inlet_junction_io`.) + - `swmm_model_write_compat(engine, path, SWMM_INP_PROFILE_SWMM5)` — the SWMM 5.x write + profile (MULTI_ENGINE plan V2 Phase 4): v6-only sections (`[VIRTUAL_JUNCTIONS]`, + `[INLET_JUNCTIONS]`, `[2D_*]`, `[PLUGINS]`, `[PROCESS_COMPONENTS]`, `[USER_FLAGS]`, + `[USER_FLAG_VALUES]`, `[RDII_DECAY]`) and option keys are omitted, `FLOW_ROUTING FV` is + written as `DYNWAVE`, `SURCHARGE_METHOD DYNAMIC_SLOT` / `TPA` as `SLOT`, a virtual junction + becomes an ordinary junction and an inlet junction an ordinary junction plus an + `[INLET_USAGE]` row on its approach conduit; every substitution is reported as a warning + and the file opens with a comment naming the profile. GeoPackage gains `streets`, + `inlets` and `inlet_usage` tables so street models and both inlet grammars round-trip + (files written before the tables existed read as before). Inlet junctions are refused + under `FLOW_ROUTING FV` (error 619): the FV mesh splices virtual junctions out and would + drop the inlet. + +- **GUI-editor round-trip API for `[GWF]` expressions** — a subcatchment's custom groundwater + flow expressions were reachable only through the stringly-typed `swmm_options_get/set_ext` + key `"GWF::LATERAL|DEEP"`, with no validator: + - `SWMM_GwfType` (`LATERAL`, `DEEP`) with `swmm_subcatch_get_gwf_expression` / + `_set_gwf_expression` (NULL/empty clears; stored as typed, pre-start-only). + - `swmm_gwf_validate_expression` — grammar-only, never mutates the engine (peer of + `swmm_treatment_validate_expression`): unknown characters and identifiers, unbalanced + parentheses, misplaced operators and wrong `min`/`max` arity are errors, with a 0-based + column; an empty expression is invalid. + - Vocabulary for completers: `swmm_gwf_variable_count/name/description` (the 11 `GW_VAR_NAMES`) + and `swmm_gwf_function_count/name` (the 21 `MathExpr` functions, enumerator order). + - Parser leniencies matched to the editor: `[GWF]` accepts the legacy `LAT` abbreviation and a + subcatchment name in any case (the stored key uses the `[SUBCATCHMENTS]` spelling), and a + malformed expression is now **ERROR 233** at open and at `initialize()` instead of being + silently dropped (unknown identifiers previously evaluated to 0.0). + - Python: `GwfType`, `Subcatchments.get/set_gwf_expression`, `validate_gwf_expression`, + `gwf_variables()`, `gwf_functions()` with `.pyi` stubs. (`test_engine_gwf_api`.) + +- **Live `.out` reading.** `swmm_output_open_live(path)` opens a binary + output file that is still being written (or that never got its closing + records): offsets come from a forward parse of the header and the period + count from the file size, whole records only. `swmm_output_refresh` + re-counts on demand and adopts the footer when it appears; + `swmm_output_is_live` reports the state. Both writers — the 6.x + `DefaultOutputPlugin` and the legacy 5.3.0 `output_saveResults` — now + `fflush` after every report period so a tailing reader sees each period + as it completes. File format unchanged; `swmm_output_open` untouched. + (`OutputReader::openLive/refresh`, `test_engine_output_reader_live`.) + +- **Per-cell cumulative rainfall volume in the 2D sidecar.** New + `/Mesh2_face_rain_cum` `[nTime, nFace]` dataset (m³) alongside the existing + `/Mesh2_face_rainfall` intensity. Booked in + `SurfaceRouter2D::accumulateMassBalance` from the same per-cell term that + feeds `mass_balance_2d/rainfall_in`, so the sum over cells equals the + ledger by construction (`SurfaceRouter2D::rainCumulative`, + `SimulationSnapshot::surface_rain_cum`). C API bulk getters + `swmm_2d_get_rainfall_bulk` / `swmm_2d_get_rain_volume_bulk` expose the + two fields to in-process consumers (GUI live plotting). + +- **`TEMP` in reaction expressions — temperature-dependent kinetics.** + Multispecies rate/equilibrium/formula expressions can reference the + local water temperature (°C) as the hydraulic variable `TEMP`, enabling + Arrhenius-style corrections such as `theta ^ (TEMP - 20)`. Under + `HEAT_TRANSPORT` each element evaluates at its own transported + temperature (per cell on the Eulerian ARD mesh, per segment under LARD, + per link/node under the legacy router); without heat transport the new + `[REACTION_OPTIONS] TEMPERATURE` constant applies (default 20 °C, any + finite value accepted, exposed through `swmm_reaction_option_get/set` + and `solver.reactions.get_option/set_option`). The GUI expression + completer picks the variable up automatically from the shared + vocabulary API. + +- **Components write their own config files — API and GUI edits survive a + save (IO3a).** `swmm_model_write` emitted the `[PROCESS_COMPONENTS]` + `config=` path and copied the file the model was read from; nothing + rewrote its content, so every edit made through a C API or the GUI — a + `[HEAT_SOURCES]` temperature, a reaction expression — was silently lost + on save while a hand-edit persisted. A new `ComponentConfigSave` hook + (the inverse of `ComponentConfigApply`; empty return = decline → the + carry-alongside copy still runs) lets components adopt saving one at a + time with no intermediate state losing data. Heat and reactions adopt + here; water age and ARD still decline (IO3c — their configs are + unit-bearing: age stores seconds against a file in hours, so their + serializers need a fixed-point round-trip gate heat never needed). `saveHeatConfig` writes + only what the model actually set. IO3b (same release) extended the heat + renderer to all five sections — `[RADIATIVE_FLUXES]` (all three SHORTWAVE + spellings, with the TIMESERIES index mapped back to its series name), + `[SOLAR_RADIATION]` and `[CLOUD_COVER]` — so heat edits survive a save on + EVERY model, and the IO3a decline guard is deleted in favour of + structural `sizeof` pins: growing any of the three config structs now + breaks the build beside the renderer instead of silently reopening the + loss. Found en route: the renderer's first spelling of the + longwave-reflection key (`LW_REFLECTION`) is one the parser refuses + (`ATM_LW_REFLECTION`) — caught by the new field-by-field round-trip gate + before it shipped. + +- **Python bindings for the five transport-process headers.** `solver.heat`, + `solver.reactions`, `solver.water_age`, `solver.initial_quality` and + `solver.process_components` bind `openswmm_heat.h`, + `openswmm_reactions.h`, `openswmm_water_age.h`, + `openswmm_initial_quality.h` and `openswmm_process_components.h` as + keyed, iterable Python views rather than index-and-out-pointer calls: + flux-module and `[RADIATIVE_FLUXES]`/`[SOLAR_RADIATION]`/`[CLOUD_COVER]` + mappings plus the `[HEAT_SOURCES]` table with node overrides; species, + coefficients, terms, per-scope expressions and the whole-file `.rxn` + text surface; the `[WATER_AGE_SOURCES]` globals and overrides; the + `[INITIAL_QUALITY]` rows; and the `[PROCESS_COMPONENTS]` registry. + + The bindings refuse exactly what the C API refuses, so the constraints + that matter survive the language boundary rather than being re-derived + on the Python side: heat values are refused rather than clamped and + `COMPUTED` shortwave still demands an explicit latitude and longitude; + reaction mutation stays eagerly validated and transactional, so a stored + system is never uncompilable and `serialize -> apply_text -> serialize` + is byte-identical; water-age hours stay **signed**, because a negative + source age extracts age-volume by design; and `Reactions`' + hydraulic-variable and function tables are exposed as engine-less + `staticmethod`s so a completer enumerates the compiler's own vocabulary + instead of carrying a copy of it. + + Nine enums land with them — `HeatFluxModule`, `HeatShortwaveMode`, + `HeatRadiativeParam`, `HeatSolarParam`, `HeatCloudParam`, + `HeatSourceKind`, `WaterAgeSource`, `ReactionScope` and + `ReactionExprForm` — each mirroring its C counterpart's numbering, and + each accepted anywhere the corresponding `int` or token string is. + +- **Preissmann slot readers and 2-D name/bulk accessors.** `Link.slot_volume` + reports the water standing above the pipe crown, and + `LinkStatsView.peak_slot_share` / `LinkStatsView.slot_share` report the + peak instantaneous and run-level slot fraction — the latter a ratio of + time integrals, never an average of instantaneous ratios. All three are + finite-volume-routing only and read `0.0` under dynamic wave. + `Surface2D` gains `set_vertex_z_bulk` (one mesh rescan instead of one + per vertex) and the three read-back companions the boundary-condition + setters had been missing: `get_edge_bc_tseries_name`, + `get_edge_bc_flow_tseries_name` and `get_edge_bc_rating_curve_name`, + each returning `""` for a clear slot so a round trip through an editor + no longer has to remember what it wrote. + +- **C/Python/MCP parity matrix at zero gaps in all three columns.** Of + 1016 C symbols, 992 are at parity and the remaining 24 are + documented-intentional exclusions — the first time the matrix has been + clean across the C, Python and MCP columns simultaneously. + +- **Heat transport H6a — incoming shortwave forcing.** `[RADIATIVE_FLUXES] + SHORTWAVE` now takes three mutually exclusive spellings instead of one + constant: a fixed W/m², `TIMESERIES ` for a measured record, or + `COMPUTED` for a Spencer/NOAA solar position driving a Bird & Hulstrom + (1981) clear-sky model. New `[SOLAR_RADIATION]` (site coordinates, + timezone, elevation, Bird atmosphere) and `[CLOUD_COVER]` (fraction, + constant or timeseries) sections in `model.heat`, plus the new public + header `openswmm_heat.h` — the surface GUI task G4g was blocked on. + The header also carries the `[HEAT_SOURCES]` inlet-temperature table + (step 3): per-source GLOBAL read/write with a configured-vs-default + distinction, NODE-override enumeration and CRUD, and an effective-value + resolver that delegates to the engine's own precedence. The API refuses + exactly what the parser refuses — including NaN/inf, a hole the + pre-existing `[RADIATIVE_FLUXES]` fraction guard had and which is now + closed. ⚠ API edits to a component config do not yet survive + `swmm_model_write` (no per-component serialization until IO3) — pinned + by a gate rather than left to be discovered. + + Cloud fraction modulates **both** directions: it attenuates shortwave + (Kasten–Czeplak `1 − k C^n`) and raises atmospheric emissivity in the + longwave (Bolz `1 + k_lw C²`). Clear sky is bit-identical to H3, not + merely close — the cloud factor is a literal `1.0` that + `atmosphericEmissivity` short-circuits, so the RHE-gated Brunt path is + the same object code it was validated as. + + Three refusals worth knowing about, each chosen over a silent fallback: + configuring two SHORTWAVE spellings is an error rather than a precedence + ladder; `COMPUTED` without `LATITUDE`/`LONGITUDE` is an error rather than + a fallback onto the `[TEMPERATURE]` SNOWMELT latitude (which defaults to + 0 and would model equatorial noon); and out-of-range values are refused + rather than clamped, identically in the parser and the C API. + + **Solar position is Spencer/NOAA (~0.1°), not NREL SPA (±0.0003°).** The + plan originally specified SPA; implementing it faithfully needs ~260 rows + of periodic-term constants that would have been transcribed from memory + alongside the test vector meant to check them. `solarPosition()` is the + swap point for landing SPA later, against its published source. See + `HEAT_TRANSPORT_PLAN.md` §2.5 and D-H6a-4. + +### Performance + +- **FV published node stages no longer stand above the conduits they connect.** + The face-consistent stage reconstruction covered only degree-2 pass-through + junctions -- the set the solver splices -- so on a branching network most + nodes published the solver's own head. Measured on `Example1.inp` under + `FLOW_ROUTING FV` at `FV_MIN_CELLS 1`, where a conduit is a single cell and + the comparison is exact, those nodes sat up to **0.91 ft above their own + adjacent cell**, falling ~first-order under refinement -- a datum term, not + physics. It survives on nodes with no lateral inflow at all, which rules out + "head needed to drive the inflow out". + + The reconstruction now covers storage-less clean junctions at any degree. + Two guards make that safe: a face perched ABOVE the node's own water surface + is a free overfall and does not vote (without this, a conduit entering 4 ft + above the invert read its offset as node depth and drove one junction's + reported average from 0.28 ft to 4.11 ft), and a pondable junction demoted to + the bucket path above its rim keeps its volume-ledger head. + + Culvert-inlet and flap-gate faces are excluded: those node heads are genuine + headwaters. Storage units, outfalls and structure-fed nodes are unchanged. + + After: max +0.0002 ft and -0.0001 ft on the two affected classes. **Routing is + untouched** -- continuity identical and the Link Flow Summary byte-identical. + Reporting only, on purpose: solver-internal heads double as ghost boundary + states under LTS tier holds. Probe and full measurements in + `tests/manual/fv_node_stage/`. + +- **`FV_CFL_CENSUS_INTERVAL` now does what it says; it was inert for every + value.** The explicit FV solver conflated two quantities in one member: the + step the last accepted substep took, and the Courant bound the last census + returned. The substep tail wrote the taken step — clamped to the time + remaining in the routing step, so possibly a tiny fragment — into that + member and then zeroed the census countdown to undo the damage, which forced + a full face census on every substep no matter what the option was set to. + + The two are now separate. The bound survives a skipped census, which is the + entire point of the option, and only a genuine invalidation resets the + countdown: a retry (the post-step census having proved the bound + inadmissible), a rebuild of the active face lists, a re-tier's accumulator + settle, and both macro-cycle exits. A clamp to the routing-step boundary + proves nothing about the Courant bound and is no longer written back. + + At the default of 1 this is bit-identical — verified, not asserted: + `Example1.inp` under `FLOW_ROUTING FV` produces a byte-identical `.out` at + `OMP_NUM_THREADS` 1 and 8 against the same build with the change reverted. + +- **Dropped the write-only `f_scale_` face array from the explicit FV solver.** + Written at four sites (`initialize`, `computeFaceFlux`, `limitPositivity`, + `fireFaces`) and read nowhere in the repository. + +- **Model load and initialization: up to 17× faster, and the wall-clock window + before the first routing step cut on every model size.** A user reported 25 + minutes between clicking Run and the analysis starting on a large model; that + window is `open()` + `initialize()` + `start()`, and none of it was + instrumented. See `plans/MODEL_LOAD_OPTIMIZATION_RESULTS_2026-08-13.md` for + the full measured breakdown, including the four plan predictions that + measurement disproved. + + Measured in Release, medians of 5, one process per model: + + | Model | before | after | + |---|---:|---:| + | 100k-conduit FV model, `open+initialize` | 8047 ms | 466 ms (**17.3×**) | + | 500 timeseries × 10k rows, `open` | 7213 ms | 2571 ms (**2.81×**) | + | 10k rain gages, `open` | 335 ms | 49 ms (**6.90×**) | + | 50k STREET conduits, `open` | 448 ms | 159 ms (**2.83×**) | + | 100k nodes + inflows with `REPORT INPUT YES`, full | 2550 ms | 897 ms (**2.84×**) | + | 500k nodes/links with geometry, `open` | 3553 ms | 2482 ms (**1.43×**) | + + Peak RSS also falls: −35% on the STREET model, −17% on the 500k model. + + The individual fixes: a case-insensitive hash index over the timeseries/curve + store (it was a linear scan called once per data row while parsing + `[TIMESERIES]`/`[CURVES]`); memoized STREET/CUSTOM transect tabulation (one + ~1.3 KB table per street instead of one per link); a one-pass node-inflow + prepass in the `REPORT INPUT YES` summary (was O(nodes × inflow rows)); + memoized finite-volume per-conduit geometry tabulation (~5,000 closure + evaluations per conduit, for a result that depends only on the cross-section); + reserved SoA capacity and token vectors during parsing; an allocation-free + `;; UNITS:` prescan; a 1 MB buffer on the binary `.out` stream; and + file-size-derived reserves for external FILE-backed timeseries in place of a + flat 100,000-row allocation each. + + All of it is bit-identical: every change was gated on a parity check + comparing the `.rpt` (timing lines masked), the `.out` byte for byte, and the + written-back `.inp`, across six models covering STREET, IRREGULAR, FV, + storage and quality. + +### Added + +- **MSX species ride LARD segments and react — L3, the last unstarted + quality step.** A reactions component under `QUALITY_SOLVER LAGRANGIAN` + used to warn "the LARD reaction binding is not implemented (deferred + L3)" while every species sat at its initial value — and the species did + not ride the segments at all (the row layout was pollutants + age + + temperature; the LEGACY engine does not transport them either — only + ARD did). The segment store now carries the compiled species table + after the reserved rows: they mix, pass through, release, seed and + publish like every other row, and a new stage after DECAY integrates + them through the shared reaction integrator — per segment at pipe + scope with the segment's own pollutant rows as expression context, per + node store at tank scope with the node's HRT. The deferral warning is + gone; treatment remains the one LARD bypass. Cross-engine: the same + RATE-production deck reads 85.46 under LARD segments where ARD cells + read 87.64 — ratio 0.9751, two independent discretisations within + 2.5 %. A model with no reactions component is byte-identical to before + (the corpus is the witness). + +- **Water age and ARD write their own config files — IO3 save is complete + (IO3c).** The last two decliners of the `ComponentConfigSave` hook adopt + it: every registered component now renders its own config on save. + `swmm_water_age_save`'s writer became the shared + `serializeWaterAgeConfig` (one spelling for `[WATER_AGE_SOURCES]`, + upgraded from `%g` to shortest-exact — the config stores seconds against + a file in hours, and a rounding formatter drifts the model every save; + the new gate pins exact round-trips and a byte-stable fixed point by + generation two). ARD renders `[TRANSPORT_OPTIONS]`, + `[CONDUIT_DISPERSION]` and the raw boundary/source rows; + `SCALAR_SCHEME`/`LIMITER` ride new provenance flags — they alias + `[OPTIONS]` FV keys the writer only emits under FLOW_ROUTING FV, so on + a DYNWAVE deck the component file is the only carrier of that state: + the renderer re-emits the alias (at the current live value) exactly + when the file originally spelled it, and never invents one. Found en + route: the IO3a **render** path silently replaced a different + pre-existing config at the save destination where the copy path + announces it — the render path now carries the same + announce-on-replace contract. + +### Fixed + +- **Street inlets** — `[INLETS]` two-line combination inlets were not merged, the curb throat + angle and custom capture curves were dropped, on-sag capture never ran, street geometry never + reached the capture calculation, and capture was applied after the routing step (so the router + never saw it); splitting a STREET conduit lost the lower half's street reference + (`vj_split_conduit` now copies the named cross-section). An `[INLET_USAGE]` row naming a capture + node declared later in the file (legacy parsing is order-independent) is deferred to the + post-parse pass instead of failing the open with ERROR 209, and the inlet summary's "Peak Flow" + column is the peak approach flow, as in legacy, not the peak captured flow. The Node Inflow + Summary's lateral-inflow statistics keep their sign as legacy `stats.c` does (an inlet's bypass + node shows the transfer as a negative lateral); they were accumulated as absolute values. + +- **DUMMY links are routed under `FLOW_ROUTING FV` instead of failing the + model.** A DUMMY conduit carries no cross-section, so the FV mesh builder + rejected it as unmeshable (`y_full <= 0`) and the run aborted — DUMMY links + are common in real models. They are now excluded from the mesh and routed as + pass-throughs, the way `isTrueConduit` (`dynwave.c:411-414`) excludes them + from the DW momentum solve and `findNonConduitFlow` passes the upstream + node's inflow straight through. + + The discharge could not simply reuse the existing DUMMY branch of + `computeNonConduitFlowOne`: that reads `ctx.nodes.inflow`, which under FV + holds only laterals and structure scatter until `publishFv` writes the real + boundary fluxes at end of step, so a dummy fed by a conduit would have + carried a fraction of its true flow. `ExplicitFvSolver::refreshDummyFlows` + derives it instead as a drain condition on the upstream node — the net face + volume plus that node's lateral and structure forcing, evaluated per substep + against the same fluxes the node update is about to integrate. This + reproduces DW's continuity exactly: `Q = inflow + overflow` booked as + outflow leaves `dV/dt = 0`, making the node a free-drainage boundary rather + than a storing junction. Both ends stay pinned to LTS tier 0, so the + source/sink pair integrates over one span and the transfer is conservative. + (`NetworkMeshBuilder` `struct_is_dummy` / `node_dummy_drain`, + `FvStepForcing::link_q_cap` for the control setting and FLOW_LIMIT, + `Router::publishFv` for the step-mean discharge and the node ledger.) + +- **ERROR 134 — illegal DUMMY / ideal-pump connections — is enforced again.** + Legacy `checkDummyLinks` (`toposort.c:480-528`) and the degree test in + `validateGeneralLayout` (`flowrout.c:301-313`) were never ported + (`plans/FULL_GAP_ANALYSIS.md:618`). Both are what make a pass-through well + posed: its discharge is defined as everything arriving at the upstream node, + so it must be that node's only outlet — a second outlet has both links + claiming the whole inflow — and a node cannot both receive and emit one + without the two definitions becoming circular. A storage node's outlet may + not be a dummy (`link.c:1006-1014`). Enforced for DYNWAVE and FV, the two + models that implement the pass-through. No model in the regression corpus + (1,637 `.inp` files) is rejected by it. + +- **`.inp` writer emits authored form — offsets and conduit orientation.** + `resolve_cross_references` rewrites `LINK_OFFSETS ELEVATION` offsets and + weir/outlet crests as depths and reverses adverse-slope conduits (node1/ + node2, offsets, losses, InitFlow sign) for DYNWAVE/FV; `InpWriter` then + emitted both verbatim, so Open → Save wrote depths under an `ELEVATION` + header (clamped to 0 with WARNING 03 on the next open) and swapped From/To + on every adverse conduit. New `input::convert_internal_to_authored` (with + `needs_authored_conversion` / `restore_authored_orientation`) undoes both + on the writer's local copy, and `swmm_links_restore_authored_orientation` + lets an editing host hold the authored orientation in its live context. + Pinned by four new `InpWriterRoundTrip` cases on `authored_form.inp`. + +- **`[POLLUTANTS]` Kdecay was applied 86,400× too fast — and the common case + destroyed the pollutant with a 100 % continuity error, booked nowhere + (KD1).** The Kdecay column is 1/day; legacy divides by `SECperDAY` at parse + (`landuse.c`), but this engine used the raw value against dt-in-seconds at + every application site. Any deck with a plausible decay coefficient + (0.05–1/day BOD or coliform values) silently reported ~zero for that + pollutant: the linearized node factor `1 − k·dt` went negative, clamped to + zero, and the annihilated mass never reached the ledger. The parser now + stores 1/sec exactly as legacy does, and every file-unit boundary converts + back — INP writer, GeoPackage both directions (pre-KD1 `.gpkg` files stored + the deck value unconverted, the same unit, so both eras read correctly), + and the C API, whose header documented 1/day all along. Three more defects + fell out of making the new gates close: the legacy-path solver never booked + decayed mass into `qual_routing_reacted` (nodes, all three link branches, + and the reactions-active twin now book it); every node decayed where legacy + decays only storage nodes or nodes actually holding volume, so pass-through + junction flux picked up decay factors legacy never applies; and + `mixAtNodes`' evaporation factor inferred evaporation from any volume + decrease — true for every draining node — creating ~`c·v_out` of mass per + step at a draining storage whenever concentrations weren't uniform (the + `c_max` cap masked it on every k = 0 deck). It now uses the storage unit's + actual evaporation volume, `fEvap = 1 + vEvap/v1`, per legacy + `findStorageQual`. After all four: the k = 1/day differential probe reads + Mass Reacted 0.158 lbs vs legacy's 0.159, and k = 200/day books 8.455 vs + legacy's 8.523 with continuity at baseline. Known residual (both engines, + P2.4): per-step transit mass takes the decay factor outside any + volume-basis booking — legacy leaks 6.5–7 % on the same storage deck. + Triage + evidence: `plans/transport/KDECAY_UNITS_TRIAGE_2026-08-31.md`. + +- **The 2D results file now says what its coordinates mean.** `Mesh2_node_x/y` + and `Mesh2_face_x/y` have always been written in the solver's SI metres and + tagged `units = "m"`, but nothing in the file recorded the model's own CRS or + the factor applied to get there. A consumer of a model in a foot-based + projected CRS (EPSG:2249 and friends) reasonably read those metres as the + CRS's own unit and placed the results ~0.3048x toward the CRS origin, while + the `.2dm`-backed mesh — which never leaves model units — rendered correctly. + The file gains a scalar `/crs` variable carrying `model_crs` (verbatim from + `[OPTIONS] CRS`) and `metres_per_model_unit` (the factor + `SurfaceRouter2D::initialize` actually applied), referenced by a + `openswmm_crs = "crs"` attribute on `/Mesh2` and the coordinate variables. + Deliberately not spelled as CF `spatial_ref`/`crs_wkt`: a reader honouring + those would place metres in a foot CRS and reproduce the very offset the + variable exists to describe. Existing files are unaffected and stay readable; + they simply declare nothing. (Issue #155.) + +- **A malformed option value handed to `swmm_options_set` returns + `SWMM_ERR_BADPARAM` instead of killing the process.** Thirty of the + dispatch's key branches parsed with raw `std::stod`/`std::stoi`, which + throw on junk — and an exception crossing the C boundary is + `std::terminate`. The guard alone was also not sufficient: two + non-throwing families slipped through it — the lenient time parse + fabricated `0.0` from junk (`"1e999999"` read back as 3600 s), and + `std::stoi` accepted a numeric prefix (`"1.5"` read as 1). All parse + sites in the dispatch now use strict wrappers that consume the entire + token or reject, covered by an exhaustive malformed-value suite over + every settable key. Reachable from any API client, including the MCP + server passing through arbitrary text. (`d80bba34`.) + +- **The `OPENSWMM_PERF` FV phase split double-counted, and its parts could + exceed the whole.** `perf::GatedTimer` accumulated wall time, so a timed + phase that called another timed phase booked the child twice — once in the + child's accumulator and once inside its own. Three nestings exist: + `settleAccumulators` and `restoreState` both call `refreshDepths`, and the + boundary-flow callback sits inside the settle window. Two of the three are + local-time-stepping only, which is why the symptom appeared only on LTS runs: + `total` came out **larger than the routing `step` it is a breakdown of**, for + an "unattributed" share of **−16.2 %** on Example1 and **−19.0 %** on a + 500-conduit graded chain. That is not a quantity that can be negative, and a + breakdown whose parts exceed the whole cannot rank optimization targets — + the only reason the timers exist. + + Timers now book **self** time: a thread-local tally collects the wall time of + timers opening and closing inside the running one, and each timer subtracts it + before accumulating. Unattributed time is now **+3.5 % to +6.2 %** across the + same four runs. No call site changed, and with `OPENSWMM_PERF` unset the + destructor still returns before touching anything. + + The correction changes what the table recommends. `settle` read 2.255 s on the + graded chain — 18 % of the reported total — while its own work is 0.007 s; + effectively all of it was the two `refreshDepths()` calls already counted under + `refreshdepths`. `restore` was the same. The real target is `refreshDepths`, at + 33 % of total on that deck, and it was not legible before. Evidence and + reproduction: `tests/manual/fv_phase_timers/RESULTS.md`. + +### Changed + +- **Inlet `MIN_RUNOFF_FLOW` / `FUDGE` aligned with legacy** (`0.001` cfs / `0.0001`; both were + `2.5e-5`) — results change for any model with conduit inlets. Error codes 623–635 are new. + +- **Hydraulics Reference Manual §8.6 and §8.7 rewritten to describe the solver + that ships.** §8.6.1 documented junctions as `MIN_SURFAREA` linear reservoirs + integrated in time — the BUCKET model, removed when junctions became + interfaces. A plain junction has **no storage**: a clean degree-2 junction + passes its cells' states straight through, and every other storage-less + junction solves its head from an instantaneous flux balance, now written out + as equation (8-31). §8.6.5's step 3 and the option table still documented + `FV_NODE_CELL_COUPLING` and `FV_JUNCTION_MODEL`, both retired and + accept-and-ignore since the interface treatment landed. The node time-step + bound (8-32) is now stated to apply to bucket nodes only, which is what + `nodeStableDt` implements. + + Two gaps are also closed. The chapter said junction momentum is not conserved + without saying what that costs — roughly a millimetre of head per junction + from splitting one Riemann problem into two, integrating to a 0.23 m backwater + over a 199-junction subcritical chain, which is why the pass-through splice + exists. And §8.7 now documents the published node stage (8-33), reconstructed + from the incident wet cells rather than read off the solver's head; that rule + changed in the previous release and was undocumented. + + A new subsection also answers a recurring question directly: the node is not + given half of each connected link's volume, as dynamic wave does, because + under finite volume the end cells already hold that water as explicit state. + Booking it at the node too was measured twice and rejected twice, at ~0.3 % of + routing continuity and −0.005 % per junction. + +- **Relicensed from MIT to the Apache License, Version 2.0** (#123, #122). The + `LICENSE` file now carries the full Apache 2.0 text, retaining the addendum + that acknowledges the USEPA SWMM material residing in the public domain under + 17 USC § 105 — the Apache grant covers only the OpenSWMM authors' original + contributions and places no restriction on that public domain material. A new + `NOTICE` file records the required attribution and SWMM provenance. All + first-party source headers now carry the Apache 2.0 boilerplate and an + `SPDX-License-Identifier: Apache-2.0` tag in place of the previous MIT tags. + `CLA.md` (v1.1), `CONTRIBUTING.md`, `CITATION.cff`, `README.md`, the Python + bindings' `pyproject.toml` and the Sphinx license page were updated to match. + Built-in plugin metadata (`IPluginComponentInfo::license_type`) now reports + `"Apache-2.0"`. + +### Added + +- **Two-component pressure approach (TPA) — sub-atmospheric pressurized flow + in both hydraulic solvers.** (#156) The closure of Vasconcelos, Wright & Roe + (2006): a sealed full pipe may carry pressure below atmospheric instead of + spuriously reverting to free-surface geometry, governed by a physical + air-pathway regime flag with submergence-checked venting and a + column-separation floor at −30 ft of head. In the finite-volume solver, + `FV_PRESSURE_CLOSURE TPA` extends the existing slot line to both signs of + ΔA (`FV_SLOT_CELERITY` doubles as the acoustic celerity) and composes with + `FV_PRESSURIZED_IMPLICIT`; in the dynamic wave solver, `SURCHARGE_METHOD + TPA` adds a constant-width slot `w = g·A_full/a²` above the crown (new + `TPA_CELERITY` key, project units, default 100) with a per-conduit + sub-atmospheric latch below it, updated once per routing step so the Picard + iteration sees a fixed operator. Both default off and bit-inert when off. + Measured on the paper's laboratory cases: the FV closure carries 0.0398 m of + true vacuum below the crest invert of the negative-pressure siphon into the + `.out` file — still deepening when the deck reaches the 40 s validity + boundary it documents — where the slot closures floor at the invert; and + DW TPA's rapid-fill bore arrival lands within 2 % of the + FV timing where the Sjöberg slot is ~20 % early. Documented as Hydraulics + Reference Manual §8.4.5 and §3.3.11. One pinned known issue: explicit FV + TPA filling at a = 150 m/s diverges at the reflected surge (the paper's own + high-celerity post-shock frontier; gate + `FvTpa.KnownIssueHighCelerityFillingDiverges`). + +- **Unsteady friction (`UNSTEADY_FRICTION VITKOVSKY`, `UF_K3`) in both + hydraulic solvers.** (#156) The Pinto, Vasconcelos & Soares (2025) modified + Vítkovský term with regime-dependent celerity, orthogonal to the + pressurization closure: the local-acceleration half folds in + semi-implicitly (no new prognostic state — hotstart- and rollback-safe), + the convective half enters explicitly with a velocity dead-band and a + half-momentum clamp. The DW gradient uses a cross-link stencil (the + within-link difference is structurally zero on a full conduit), so DW + damping requires discretized reaches — a single-link reach gets added + inertia only. Default `NONE` is bit-identical to earlier releases. + Documented as §3.3.12 and §8.5.4 of the Hydraulics Reference Manual. + +- **`REPORT_SIGNED_HEADS` — true signed piezometric heads in the `.out` + file.** (#156) The binary output's NODE_HEAD has always been rebuilt from a + depth floored at zero, so sub-atmospheric heads were invisible. With + `REPORT_SIGNED_HEADS YES` the HEAD field carries the true signed head for + both solvers; DEPTH stays floored, and the default `NO` keeps legacy + bit-parity (measured: a never-negative DW deck differs only in + float32-rewrite ULPs, confined to HEAD). + +- **Virtual-junction initial-state seeding under dynamic wave routing.** + (#156) `[VIRTUAL_JUNCTIONS]` carries no initial-depth column, so VJs + started dry regardless of their neighbors — a deck whose real nodes define + an initial pool began with a hole at every splice. Each VJ head is now + seeded by distance-weighted interpolation between the nearest non-virtual + nodes along its spliced chain, requiring at least one wet endpoint (so a + dry deck cannot manufacture water). DYNWAVE only, and that restriction is + measured, not cautionary: seeding the node alone under FV contradicts the + uniform-depth cell seed and was measured to drive a pressurized study deck + from 0.000 % to −19 % continuity. Decks without VJs are bitwise untouched. + +- **A Lagrangian transport engine (LARD) joins the quality solvers.** + `QUALITY_SOLVER LAGRANGIAN` routes pollutants with a Lagrangian + advection–reaction–dispersion scheme in place of the legacy complete-mix + chain: plug-parcel advection on per-link segment stores, first-order + `KDECAY`, quality substepping under a `QUALITY_STEP` key, and random-walk + particle tracking for longitudinal dispersion (validated against the Elder + profile). Water age rides the same engine as a reserved species: `WATER_AGE + ON`, a `[WATER_AGE_SOURCES]` table for initial state and boundary ages, + `__WATER_AGE__` rows in `[INFLOWS]`, hotstart round-tripping of the aged + state (including dry elements), and age columns in the report and binary + output. Negative source loads now extract mass deliberately at the node + seam in all three quality engines — warned at parse, clamped to the mass + actually held, booked to the ledger, and summarised in the report — instead + of being silently dropped. The option and API surface lands with it: + transport keys in `swmm_options_set`/`_get`, and a C age-source table API + (`openswmm_water_age.h`). Configurations the LARD engine does not cover yet + (heat, MSX-style reactions, `[TREATMENT]`) announce themselves with bypass + warnings at open rather than failing silently. (`24602eb2`, `8c141a5e`, + `647a3603`, `b9852cee`, `9f155227`, `d79c8bcf`, `d7b6c079`, `948b2840`, + `4639be37`.) + +- **FV solver statistics in the report file, and `OPENSWMM_PERF` phase timers + for the FV step.** FV runs now emit an "FV Solver Statistics" block — + explicit substeps, face-flux evaluations, mean/min/last substep, active-face + occupancy and the LTS tier histogram. Setting `OPENSWMM_PERF=1` additionally + prints a `[PERF-FV]` line whose bracketed phases sum to `total`, beside the + whole-router `step`; the difference between them is unattributed time and is + meant to be read as a finding rather than smoothed away. + +- **`SWMM_FilePathRole` covers the remaining external-file slots.** Three new + roles — `SWMM_FILE_MESH_2D`, `SWMM_FILE_OUTPUT_2D` and `SWMM_FILE_LID_REPORT` — + let a host read the resolved and authored form of the 2D mesh reference, the 2D + output file, and per-unit LID report files through the same + `swmm_file_path_get` / `swmm_file_path_set` pair as every other slot. The 2D + roles return `SWMM_ERR_BADPARAM` in builds without 2D support. + +- **Model-load benchmark harness and parity gate.** + `OPENSWMM_PERF=1` now emits a `[PERF-LOAD]` line attributing the whole + open/initialize/start window across 20 phases (including a `read.scan` vs + `read.dispatch` split of parsing). + `tests/benchmarks/scripts/gen_load_bench.py` generates a deterministic + scaling corpus up to 500k elements (not committed — 500 MB — but reproducible + from the committed generator); `bench_model_load` times three cuts per model + and reports peak RSS; `parity_probe` + `tests/benchmarks/scripts/load_parity.sh` + compare `.rpt`/`.out`/write-back `.inp` between two builds. Two new ctest + gates run under `ctest -L unit`: `load_parity_selfcheck` (engine output is + deterministic) and `bench_corpus_generator_smoke`. + - **Per-triangle 2D initial conditions in the Python bindings.** `Surface2D.get_triangle_init_depth` / `set_triangle_init_depth` and `get_triangle_init_velocity` / `set_triangle_init_velocity`, wrapping @@ -209,6 +1790,84 @@ retroactive. ### Fixed +- **Groundwater was silently lost on every save.** `[AQUIFERS]`, `[GROUNDWATER]` + and `[GWF]` were parsed but had no writer, so a model that used groundwater + came back from a round trip with none of it. All three now round-trip. + `[GWF]` expressions are stored in `options.ext_options` under + `"GWF::"` and were being re-emitted through the `[OPTIONS]` + passthrough, which uppercased the key (breaking the lookup that reads them + back) and kept only the first value token (truncating any real expression); + those keys are now excluded from the passthrough and written as a real + `[GWF]` section. + +- **A `[GROUNDWATER]` receiving node declared later in the file never + resolved.** `[GROUNDWATER]` normally precedes `[JUNCTIONS]`, and + `handle_groundwater()` resolved the node eagerly with no deferred pass, so + the index stayed `-1`. This was a live runtime bug as well as a write bug: + groundwater fell back to the subcatchment's outlet node instead of the one + the model specified. The name is now captured for `PostParseResolver`. + +- **Links declared before the node sections loaded silently orphaned.** Legacy + parsing is order-independent, but `[CONDUITS]`/`[PUMPS]`/`[ORIFICES]`/ + `[WEIRS]`/`[OUTLETS]` resolved their end nodes eagerly, kept no name, and + were never retried — so an `.inp` whose link sections came first loaded with + `node1`/`node2` at `-1` and **no error at all**, and then saved with `*` in + the FromNode/ToNode columns. End-node names are now re-resolved after every + section is parsed. An end node that still will not resolve is a fatal + `ERROR 209`, matching legacy `link_readParams()`; such a model previously + loaded quietly and ran wrong. + +- **`[INLET_USAGE]` was parsed but never written.** Every inlet-to-link + assignment was lost on save, leaving the surviving `[STREETS]` and + `[INLETS]` definitions inert. + +- **Five writers emitted `*` where the name was known, three of them fatally.** + A subcatchment that drains to another subcatchment (or to itself) has no + outlet *node*, and the writer only ever consulted the node index — so the + Outlet column saved as `*` and the model would not reload + (`ERROR 209: undefined object *`). The RainGage column had the same defect + against a retained gage name, and a rain gage with neither a resolved series + index nor a file path wrote **no row at all**, which made the subcatchments + naming it fail to reload. Non-fatally, the Snowpack column dropped a retained + pack name, and `[PUMPS]` ignored `pump_curve_name` and wrote `*` — silently + downgrading a curved pump to IDEAL. Each of these now falls back to the + retained name before giving up. + +- **Four external-file slots were written to the `.inp` verbatim.** The writer + rebases every external reference against the destination directory, but the + external `.2dm` mesh, the 2D `OUTPUT_FILE` and per-unit LID report files never + went through that path. `SolverOptions2D::mesh_file`, `::output_file` and + `LidUsageStore::rpt_file` are now `FilePathPair`s registered with + `resolve_external_file_slots`, and all three emit through `emit_path_token` + like every other slot. `[PLUGINS]` paths remain absolute by design — they name + installed shared libraries, not model data. + +- **A `[2D_MESH_FILE]` reference dangled after Save As when the mesh had not + loaded.** External mode normally writes the in-memory mesh to a `.2dm` beside + the destination `.inp`, so the reference stays local and the mesh travels with + the model. When there was no mesh in memory — a missing or unreadable `.2dm`, + or a lenient (editor) open — nothing was written, yet the token was still + copied through unchanged, so it re-resolved against the *new* directory and the + saved model opened 1D-only with no diagnostic. That case now re-anchors against + the resolved source path. The loaded case is unchanged, and a new test pins + both halves, including that Save As never writes back over the source `.2dm`. + +- **`[TEMPERATURE]` climate file was opened against the process working + directory.** `SWMMEngine::initialize` passed `temp_file.original` — the token + as authored — to `ClimateFileReader::open`, so a relative reference resolved + against the CWD rather than the `.inp` directory. `open()` returns `false` on + failure and the return value was discarded, so the run continued with no + climate data: a clean-looking result with silently missing temperature and + evaporation. It now opens the resolved `.absolute` path and pushes a warning + when the file cannot be read or its format is not recognised. + +- **`swmm_options_set_ext(engine, "WRITE_ABSOLUTE_PATHS", …)` did not set the + option.** The key fell through to `ext_options`, where the writer never looks, + so the save still emitted relative paths — while re-emitting + `WRITE_ABSOLUTE_PATHS YES` into `[OPTIONS]`, meaning the deck reopened with the + opt-out armed and behaved differently from the save that produced it. The key + now writes `SimulationOptions::write_absolute_paths` directly. + - **`MIN_SURFAREA` was a project option the junction storage convention never read.** Legacy keeps no junction storage at all (`node_getVolume` returns `fullVolume*(d/fd)`, and `fullVolume` is 0 for a plain junction); this engine diff --git a/CITATION.cff b/CITATION.cff index de32a1fa3..393dbff4a 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,7 +5,8 @@ authors: 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-alpha.3 +version: 6.0.0-alpha.4 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 +url: "https://github.com/hydrocouple/openswmm.engine" +license: Apache-2.0 \ No newline at end of file diff --git a/CLA.md b/CLA.md index 387f1ed97..8338e4949 100644 --- a/CLA.md +++ b/CLA.md @@ -1,11 +1,13 @@ # Contributor License Agreement — openswmm.engine -**Version 1.0 — May 2026** +**Version 1.1 — August 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 Project is currently distributed to the public under the **Apache License, Version 2.0** (see [LICENSE](./LICENSE)). Contributions accepted into the Project are made available under those terms, together with the attribution notices recorded in [NOTICE](./NOTICE). The relicensing right granted in Section 2 below operates in addition to, and does not revoke, that outbound license: anything already released under the Apache License, Version 2.0 stays available under it. + 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. --- @@ -24,7 +26,9 @@ The Technical Manager role may transfer over time per the succession process des 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. +- **relicense** Your Contributions under any license, including but not limited to the Apache License 2.0, GPLv3, LGPL, AGPL, MIT, or a commercial license, at the Technical Manager's sole discretion and without further notice to You. + +This grant is at least as broad as the copyright license in Section 2 of the Apache License, Version 2.0, so the Technical Manager may distribute Your Contributions under the Project's current outbound license without further action by 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. @@ -34,7 +38,7 @@ The relicensing right is granted specifically to preserve the Technical Manager' 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. +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. This Section mirrors the patent grant and defensive termination provisions of Section 3 of the Apache License, Version 2.0. --- @@ -50,7 +54,7 @@ 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. +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 Apache License, Version 2.0, and added any attribution those licenses require to [NOTICE](./NOTICE). 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. --- @@ -123,4 +127,10 @@ Questions about this CLA should be directed to the Technical Manager via GitHub: --- -*This CLA is maintained by the Technical Manager of openswmm.engine (currently [@cbuahin](https://github.com/cbuahin)). Last updated: May 2026.* +## 13. Public Domain Material + +Portions of the Project are derived from USEPA SWMM software that resides in the public domain and for which domestic copyright protection is not available under 17 USC § 105. Nothing in this CLA asserts, or asks You to assert, copyright over that material. If Your Contribution modifies or extends public domain material, the licenses granted above apply only to Your own original changes; the underlying public domain material remains free of any such restriction. See [NOTICE](./NOTICE) for the full provenance statement. + +--- + +*This CLA is maintained by the Technical Manager of openswmm.engine (currently [@cbuahin](https://github.com/cbuahin)). Last updated: August 2026.* diff --git a/CMakeLists.txt b/CMakeLists.txt index ceaa831e4..8a7cbf83f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ # # Project: openswmm.engine # Description: Open Storm Water Management Model Engine -# Version: 6.0.0-alpha.3 (refactored engine) / 5.3.0-beta.3 (legacy engine wrapper) +# Version: 6.0.0-alpha.4 (refactored engine) / 5.3.0-beta.3 (legacy engine wrapper) # # 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) @@ -34,7 +34,7 @@ project( ) # 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.3" CACHE STRING "Pre-release tag for the refactored engine (e.g. alpha.1, beta.1, rc.1). Empty for a final release.") +set(OPENSWMM_PRERELEASE "alpha.4" CACHE STRING "Pre-release tag for the refactored engine (e.g. alpha.1, beta.1, rc.1). Empty for a final release.") # Legacy SWMM engine version (EPA SWMM 5.x lineage, independent of the OpenSWMM # Engine version). Single source of truth: src/legacy/engine/CMakeLists.txt @@ -95,9 +95,18 @@ 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_WITH_HDF5 "Build HDF5 model I/O (requires hdf5)" ON) option(OPENSWMM_BUILD_2D "Build optional 2D surface routing module (explicit local-inertial marcher; HDF5 output)" 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). 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) +# HC-1 (2026-09-27): the HydroCouple component layer. OFF by default — the +# engine has shipped without it and a from-source build should not suddenly +# acquire a git fetch. Program plan D-C1 puts ONE `integrated2d` component +# behind this (surface + two-zone groundwater + all transport, GW as capability +# toggles), D-C2 puts the wrappers in `src/couplers/`, and D-C6 restricts the +# dependency to the header-only interfaces — never `HydroCoupleSDK`. +option(OPENSWMM_WITH_HYDROCOUPLE "Build the HydroCouple component wrappers (src/couplers) — header-only interfaces only, no HydroCoupleSDK (D-C6)" OFF) + # 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) @@ -227,10 +236,27 @@ function(openswmm_bundle_runtime_deps TARGET_NAME SUBDIR) # 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. + # + # vcpkg's DEBUG binaries live in /debug/bin, not /bin, so + # both trees are captured and the install script picks the order by config + # (issue #9). Searching only the release tree left Debug installs broken in + # two different ways: + # + # - HDF5's debug DLLs carry a _D suffix (hdf5_D.dll, hdf5_hl_D.dll). The + # name in the import table has no match under bin/, so it surfaced as + # "unresolved runtime dep: hdf5_D.dll" and the installed exe died at + # startup with STATUS_DLL_NOT_FOUND (0xC0000135). + # - sqlite3's debug DLL keeps the release NAME, so it resolved happily — + # against the RELEASE copy, quietly mixing a release sqlite3 into a + # Debug package. That one never warned, which is why it looked fine. + # set(OPENSWMM_BUNDLE_EXTRA_DIRS "") + set(OPENSWMM_BUNDLE_EXTRA_DIRS_DEBUG "") if(DEFINED VCPKG_INSTALLED_DIR AND DEFINED VCPKG_TARGET_TRIPLET) list(APPEND OPENSWMM_BUNDLE_EXTRA_DIRS "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin") + list(APPEND OPENSWMM_BUNDLE_EXTRA_DIRS_DEBUG + "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/debug/bin") elseif(DEFINED ENV{VCPKG_ROOT}) # Fallback: use the env-var-rooted classic-mode layout. set(_triplet "${VCPKG_TARGET_TRIPLET}") @@ -240,6 +266,8 @@ function(openswmm_bundle_runtime_deps TARGET_NAME SUBDIR) if(_triplet) list(APPEND OPENSWMM_BUNDLE_EXTRA_DIRS "$ENV{VCPKG_ROOT}/installed/${_triplet}/bin") + list(APPEND OPENSWMM_BUNDLE_EXTRA_DIRS_DEBUG + "$ENV{VCPKG_ROOT}/installed/${_triplet}/debug/bin") endif() endif() @@ -382,6 +410,14 @@ if(OPENSWMM_INSTALL OR OPENSWMMCORE_INSTALL) ) endif() +# HC-1: resolve the HydroCouple interface headers before src/, so +# src/couplers/ can link HydroCouple::HydroCouple. Guarded, so a default build +# neither fetches nor requires anything. See cmake/FetchHydroCouple.cmake for +# why the pin is a SHA. +if(OPENSWMM_WITH_HYDROCOUPLE) + include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchHydroCouple.cmake) +endif() + # Add subdirectories add_subdirectory(src) diff --git a/CMakePresets.json b/CMakePresets.json index b78448d5a..86f7b2ee0 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -3,25 +3,31 @@ "configurePresets": [ { "name": "default-debug", + "description": "Base Debug preset. Fast geometry/Manning kernels are OFF so the engine stays bit-identical to the legacy 5.x engine — see the `default` preset for why.", "generator": "Ninja", "binaryDir": "${sourceDir}/build/debug", "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" + "CMAKE_BUILD_TYPE": "Debug", + "OPENSWMM_FAST_XSECT_LOOKUP": "OFF", + "OPENSWMM_FAST_MANNING_POW": "OFF" } }, { "name": "default", + "description": "Base Release preset — the shipping configuration. OPENSWMM_FAST_XSECT_LOOKUP and OPENSWMM_FAST_MANNING_POW are pinned OFF: they are ~10% faster on DW routing but NOT bit-exact vs legacy, and the engine the GUI ships must be bit-identical to the legacy 5.x engine so that a v6-vs-5.x run in the GUI is a real parity comparison. Use the `-fast` presets when you want the speed and do not need parity. Set BOTH or NEITHER — never mix.", "generator": "Ninja", "binaryDir": "${sourceDir}/build", "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", "cacheVariables": { + "OPENSWMM_FAST_XSECT_LOOKUP": "OFF", + "OPENSWMM_FAST_MANNING_POW": "OFF" } }, { "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.", + "description": "Windows Release (Ninja Multi-Config, MSVC). FP policy: /fp:precise (IEEE-754, no FMA contraction — VS2022 17.0+ generates no contractions under /fp:precise; /fp:contract would opt back IN, and there is no /fp:contract- spelling). C extensions /we4013 promote implicit-decl to error.", "generator": "Ninja Multi-Config", "binaryDir": "${sourceDir}/build/windows", "cacheVariables": { @@ -279,6 +285,36 @@ "OPENSWMM_BUILD_REGRESSION_TESTS": "ON", "OPENSWMM_WITH_GEOPACKAGE": "ON" } + }, + { + "name": "Windows-fast", + "inherits": "Windows", + "description": "Windows Release with the fast geometry/Manning kernels ON: ~10% faster DW routing, NOT bit-exact vs legacy. For benchmarking and perf work only — DO NOT SHIP this build, and do not grade legacy parity with it.", + "binaryDir": "${sourceDir}/build/windows-fast", + "cacheVariables": { + "OPENSWMM_FAST_XSECT_LOOKUP": "ON", + "OPENSWMM_FAST_MANNING_POW": "ON" + } + }, + { + "name": "Linux-fast", + "inherits": "Linux", + "description": "Linux Release with the fast geometry/Manning kernels ON: ~10% faster DW routing, NOT bit-exact vs legacy. For benchmarking and perf work only — DO NOT SHIP this build, and do not grade legacy parity with it.", + "binaryDir": "${sourceDir}/build/linux-fast", + "cacheVariables": { + "OPENSWMM_FAST_XSECT_LOOKUP": "ON", + "OPENSWMM_FAST_MANNING_POW": "ON" + } + }, + { + "name": "Darwin-fast", + "inherits": "Darwin", + "description": "macOS Release with the fast geometry/Manning kernels ON: ~10% faster DW routing, NOT bit-exact vs legacy. For benchmarking and perf work only — DO NOT SHIP this build, and do not grade legacy parity with it.", + "binaryDir": "${sourceDir}/build/darwin-fast", + "cacheVariables": { + "OPENSWMM_FAST_XSECT_LOOKUP": "ON", + "OPENSWMM_FAST_MANNING_POW": "ON" + } } ], "buildPresets": [ @@ -368,6 +404,21 @@ "name": "Darwin-tests-release", "description": "Build unit and regression tests in Release on macOS", "configurePreset": "Darwin-tests-release" + }, + { + "name": "Windows-fast", + "description": "Build the fast (non-bit-exact) engine on Windows — benchmarking only", + "configurePreset": "Windows-fast" + }, + { + "name": "Linux-fast", + "description": "Build the fast (non-bit-exact) engine on Linux — benchmarking only", + "configurePreset": "Linux-fast" + }, + { + "name": "Darwin-fast", + "description": "Build the fast (non-bit-exact) engine on macOS — benchmarking only", + "configurePreset": "Darwin-fast" } ] } \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf175ab8d..282b4bf85 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,7 +43,7 @@ openswmm.engine is a community-driven open source project. All contributors are The Technical Manager is responsible for: -- Maintaining the integrity and health of the `main`, `dev`, and `experimental` branches. +- Maintaining the integrity and health of the `main`, `develop`, 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. @@ -65,7 +65,7 @@ The Technical Manager role is a single point of authority, which must be protect ## 4. Licensing, Intellectual Property & CLA -openswmm.engine is released under the **MIT License**. +openswmm.engine is released under the **Apache License, Version 2.0**. Attribution requirements are recorded in [NOTICE](./NOTICE), which also documents the USEPA SWMM material that resides in the public domain under 17 USC § 105. 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: @@ -83,9 +83,9 @@ The project uses [CLA Assistant](https://cla-assistant.io). When you open your f 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. +If your contribution includes third-party code or data, you are responsible for ensuring that the third-party license is compatible with the Apache License, Version 2.0 and that proper attribution is added to [NOTICE](./NOTICE). -The full license text is available in [LICENSE](./LICENSE). +The full license text is available in [LICENSE](./LICENSE), and required attribution notices in [NOTICE](./NOTICE). --- @@ -179,11 +179,11 @@ Experimental APIs (clearly marked as such) carry no backward compatibility guara | 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. | +| `develop` | 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`. | +| `bugfix/-desc` | Short-lived branches forked from `develop` to address specific bug reports. | +| `feature/` | Short-lived branches for minor feature additions, forked from `develop`. | +| `release/` | Release preparation branches (alpha → beta → rc) cut from `develop`. | --- @@ -192,13 +192,13 @@ Experimental APIs (clearly marked as such) carry no backward compatibility guara 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`). +2. **Fork the `develop` 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. +6. **Submit a pull request** against `develop` following the process described in Section 11. --- @@ -222,7 +222,7 @@ Self-approvals are not permitted. The PR author may not count toward the communi Before requesting review, confirm that your PR: -- [ ] Targets the correct branch (`dev` for bug fixes and features; see Section 15 for experimental work) +- [ ] Targets the correct branch (`develop` 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`) @@ -235,8 +235,8 @@ Before requesting review, confirm that your PR: ### 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. +- Squash merging is preferred for `bugfix` and `feature` branches to keep the `develop` history clean. +- Merge commits are used when integrating `develop` into a `release` branch to preserve the full history. --- @@ -325,7 +325,7 @@ Accepted proposals are implemented in a dedicated `experimental/` branch. - 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. +- Code must compile cleanly and not break the existing test suite on the `develop` branch. - Implementation notes and algorithm descriptions must be maintained in Doxygen-compatible documentation. ### Step 4 — Peer Review (Strongly Encouraged) @@ -342,7 +342,7 @@ See Section 17 for full details. At minimum, the experimental implementation mus ### Step 6 — Documentation & References -Before a major formulation change can be merged into `dev`, the following documentation must be complete: +Before a major formulation change can be merged into `develop`, 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. @@ -417,7 +417,7 @@ Adding a third-party dependency increases the maintenance burden and risk surfac | 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. | +| **License compatibility**| The dependency's license must be compatible with the Apache License, Version 2.0 (e.g., Apache-2.0, MIT, BSD). Copyleft licenses such as GPL, and licenses incompatible with Apache-2.0 such as GPLv2-only, 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. | diff --git a/LICENSE b/LICENSE index be942bae4..7810e5612 100644 --- a/LICENSE +++ b/LICENSE @@ -1,14 +1,219 @@ -MIT License +Copyright 2026 HydroCouple Developers -Copyright 2026 Caleb Buahin + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -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: + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + 1. Definitions. -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. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -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. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You 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 the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) 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. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +-------------------------------------------------------------------------------- + +ADDENDUM: PUBLIC DOMAIN MATERIAL + +This project contains original material that has been released as part of +various USEPA SWMM software distributions over the years. That material +resides in the public domain and no copyright is claimed over it. This +project also contains new material prepared by the United States Government +for which domestic copyright protection is not available under 17 USC § 105. + +The Apache License, Version 2.0 above applies to the original contributions +of the OpenSWMM authors. It does not, and cannot, extend copyright to the +public domain material described in this addendum, nor does it restrict any +rights the public already holds in that material. See the NOTICE file for +further attribution details. diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..60997a89e --- /dev/null +++ b/NOTICE @@ -0,0 +1,40 @@ +OpenSWMM Engine +Copyright 2026 HydroCouple Developers + +This product includes software developed by the OpenSWMM authors +(https://github.com/HydroCouple/openswmm.engine). + +Licensed under the Apache License, Version 2.0. See the LICENSE file for the +full license text. + +-------------------------------------------------------------------------------- +PUBLIC DOMAIN MATERIAL +-------------------------------------------------------------------------------- + +Portions of this product are derived from the Storm Water Management Model +(SWMM) developed and released by the United States Environmental Protection +Agency (USEPA) over the course of several decades. That material was prepared +by or for the United States Government and resides in the public domain: +domestic copyright protection is not available for it under 17 USC § 105, and +no copyright is claimed over it by the OpenSWMM authors. + +The Apache License, Version 2.0 applies to the original contributions of the +OpenSWMM authors. It does not extend copyright to, and places no restriction +on, the public domain material described above. Recipients retain all rights +they already hold in that material independently of this License. + +Files derived in whole or in part from USEPA SWMM sources are, where +practical, marked as such in their file headers. The EPA SWMM 5.x sources are +available from https://www.epa.gov/water-research/storm-water-management-model-swmm. + +The USEPA does not endorse this product, and the names "EPA", "USEPA", and +"SWMM" are used solely to describe the origin of the public domain material. + +-------------------------------------------------------------------------------- +THIRD-PARTY COMPONENTS +-------------------------------------------------------------------------------- + +This product depends on and, in some distribution formats, bundles third-party +libraries that are licensed separately by their respective copyright holders. +Dependencies are declared in vcpkg.json. Each such component remains subject to +its own license terms, which accompany the component in its own distribution. diff --git a/README.md b/README.md index c4a49c546..76f79bf03 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![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) +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-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) @@ -22,10 +22,11 @@ | | 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. | +| **C / C++ Engine** | **[hydrocouple.org/openswmm.engine](https://hydrocouple.org/openswmm.engine)** | Full C API reference, hydrology / hydraulics / water-quality reference manuals, engine manual (input format, files, reports, C API), 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. | +| **SWMMVis (GUI)** | **[hydrocouple.org/openswmm.gui](https://www.hydrocouple.org/openswmm.gui/)** | The graphical application: map canvas, layers and CRS, every editor, running simulations, plots and animation, 2D meshing, tutorials, reference tables and error codes. | -Both sites cross-link from their top navigation. +The sites cross-link from their top navigation. --- @@ -273,7 +274,7 @@ Signing is automated through [CLA Assistant](https://cla-assistant.io) — when ## License -MIT — see [LICENSE](LICENSE). Original EPA SWMM material is in the public domain under 17 USC § 105. +Apache License, Version 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE). Original EPA SWMM material is in the public domain under 17 USC § 105 and is not subject to the Apache license grant. ## Acknowledgements diff --git a/ROADMAP.md b/ROADMAP.md index e613f145e..881312bca 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -37,7 +37,7 @@ test suites, but ship in pre-release builds until 6.0.0 is final. - Second-order MUSCL reconstruction (`FV_ORDER 2`) and SSP-RK2 time integration (`FV_TIME_INTEGRATION RK2`), both preserving the still-water property to machine precision. - Local time stepping (`FV_LTS`, on by default): each control volume takes a power-of-two tier from its own Courant limit, tiers are graded so no face spans more than one level, and conservation across a tier interface is exact by construction. Where tiering finds nothing to separate, the solver falls through to global stepping bit-for-bit. - Semi-implicit node coupling (`FV_NODE_COUPLING`, default `SEMI_IMPLICIT`), which linearizes each coupling face's flux in the node head and so removes the junction storage floor — not the pipe — from the explicit stability limit. -- Backward compatible: seventeen `FV_*` `[OPTIONS]` keys, readable and writable through `swmm_options_get`/`set`, the Python bindings (`RouteModel.FV`) and the MCP server, and inert rather than rejected under the other routing models, so switching `FLOW_ROUTING` never invalidates a file. Virtual junctions become ordinary interior faces and reproduce the unsplit conduit cell for cell. +- Backward compatible: twenty live `FV_*` `[OPTIONS]` keys and five retired ones (`FV_NODE_COUPLING`, `FV_NODE_DT`, `FV_NODE_PICARD`, `FV_NODE_CELL_COUPLING`, `FV_JUNCTION_MODEL`; the first three warn, the last two were already no-ops), readable and writable through `swmm_options_get`/`set`, the Python bindings (`RouteModel.FV`) and the MCP server, and inert rather than rejected under the other routing models, so switching `FLOW_ROUTING` never invalidates a file. Virtual junctions become ordinary interior faces and reproduce the unsplit conduit cell for cell. **Validation to date:** 29 analytic gates (closure, scheme, network) plus 7 local-time-stepping gates and 5 engine-level gates. Ritter and Stoker match the closed-form solutions including shock speed; lake at rest holds to 1e-9 ft across a slope break and while pressurized; mass is conserved to 1e-12 over 10⁵ substeps. On the EPA reference drainage model the routing continuity error is **0.000 % at every mesh resolution**, against 0.026 % for dynamic wave routing on the same file. Finite-volume routing is deliberately outside the legacy bit-parity contract — it is a different discretization, gated on analytic and engineering tolerances instead. @@ -45,9 +45,10 @@ test suites, but ship in pre-release builds until 6.0.0 is final. - **Performance.** The solver runs ~7× dynamic wave wall-clock at one cell per conduit and ~34× at Δx = 20 ft on the reference model. The original expectation of parity at equal element counts is recorded as refuted; the two largest costs found so far (the depth inversion, 3.2×, and the node stability constraint, 2.9×) are already addressed. Remaining levers are kernel fusion and CFL-census interval tuning. - **Parallel and GPU backends.** `FV_BACKEND` and the plugin loader are in place, but no plugin yet exports the 1D network-solver entry point, so execution is CPU-serial today. - **Peak attenuation at the default mesh.** One cell per conduit attenuates the reference model's peaks by 37 % on average (7 % at Δx = 20 ft). The cause is geometric rather than diffusive — a cell-centred scheme places a single cell's bed at the conduit mid-point, presenting an artificial bed step at every manhole — so higher-order reconstruction does not rescue it. Setting `FV_CELL_LENGTH` is the present workaround; a bed-step treatment at junction faces is the fix. -- **Desktop application exposure.** `FLOW_ROUTING FV` and the `FV_*` keys are not yet offered in the OpenSWMM GUI's routing options. - **Broader regression** against published benchmark cases beyond the analytic suite, and quantification of the short-steep-pipe cell-length floor. -- **Sub-atmospheric transients** are outside the slot closure; the two-component pressure approach is the documented extension path if rapid-downsurge fidelity is later required. +- **Sub-atmospheric transients.** The two-component pressure approach (TPA) is IMPLEMENTED in both solvers (#156): `FV_PRESSURE_CLOSURE TPA` in the finite-volume solver (Hydraulics Reference Manual §8.4.5) and `SURCHARGE_METHOD TPA` in the dynamic wave solver (§3.3.11), both default-off and bit-inert when off, validated on the Vasconcelos et al. (2006) laboratory cases with sub-atmospheric heads observable via `REPORT_SIGNED_HEADS`. The high-celerity filling divergence that was pinned here (explicit FV TPA at a = 150 m/s diverging at the reflected surge) **was closed on 2026-09-12** by the slot/free-surface wave-speed bound: Davis's symmetric estimate had carried the acoustic celerity into the wave entering the free-surface side of a pressurization front, and bounding that wave by the Rankine-Hugoniot bore speed removes the mode. The fixture completes at 0.000 % continuity from 150 to 3000 m/s and the gate is now the positive test `FvTpa.HighCelerityFillingCompletes`; the Vasconcelos & Wright (2009) hybrid flux is no longer required for this front. + +(`FLOW_ROUTING FV` and the FV keys, including the #156 additions, are exposed in the OpenSWMM GUI's Routing & Hydraulics options as of openswmm.gui #10; the former desktop-exposure gap is closed.) ### 1.2 2D Overland Flow — Local Inertial Finite Volume Model 🔧 @@ -65,16 +66,17 @@ test suites, but ship in pre-release builds until 6.0.0 is final. - Performance and robustness hardening on large regional meshes based on beta testing feedback. - 2D water quality transport (Section 2.2) on the same mesh. -### 1.3 Spatially Explicit Inlets — Mode-Switching Junction Nodes 🔬 +### 1.3 Spatially Explicit Inlets — Mode-Switching Junction Nodes ✅ **Motivation:** Since SWMM 5.2, inlets are attributes of street and channel conduits (`[INLET_USAGE]`): capture flow is computed from approach hydraulics and applied as a flow modification inside the link, with ponding tracked at a separate reference node. The inlet has no independent hydraulic presence, so inlet-controlled surface/sewer exchange cannot respond to the hydraulic state of the node it drains to. -**Status:** Design recorded. No implementation yet. +**Status:** Implemented as `[INLET_JUNCTIONS]` in the 6.0.0 development line, alongside the conduit-attribute form, which remains supported. An inlet junction is a virtual junction that also carries an inlet: it has its own head, floods above the curb, passes bypass on to the next street conduit, and takes a surcharging sewer's backflow onto the street. Documented in Hydraulics Reference Manual §7 and worked in Application Manual Chapter 5. -**Planned scope:** -- Promote inlets to first-class junction nodes that switch mode based on approach hydraulics: capturing street flow when gutter spread exceeds a threshold, and reverting to passive junctions otherwise. +**Delivered:** +- Inlets promoted to first-class junction nodes that switch mode based on approach hydraulics: capturing street flow when gutter spread exceeds a threshold, and reverting to passive junctions otherwise. - HEC-22 grate and curb-opening capture retained as the capture closure, evaluated at the node rather than as a link post-processing step. -- Ponding, bypass, and re-entry resolved at the inlet node itself, enabling two-way street ↔ sewer exchange under surcharge. +- Ponding, bypass, and re-entry resolved at the inlet node itself, with two-way street ↔ sewer exchange under surcharge. +- A conduit attached to an inlet junction may not also carry an `[INLET_USAGE]` row; the engine refuses the double capture rather than applying it twice. --- @@ -90,9 +92,9 @@ Two routes are being developed, and they are complementary rather than competing *Remaining for Route A:* the transport layer is not yet connected to the project's `[POLLUTANTS]`, land-use buildup/washoff, inflows or treatment — it is exercised through the solver's own gates. Wiring it up, deciding how `QUALITY_SOLVER` selects between the legacy Eulerian solver, Route B and finite-volume transport, and reporting cell-resolved concentrations are the open items. Note that local time stepping is disabled while species are transported, because the flux-corrected transport limiter needs one synchronous sweep. -**Route B — Lagrangian parcel tracking (design study recorded, not implemented).** Parcels of water and their constituent loads are advected along the flow field, with dispersion applied as a superimposed Fickian random-walk step. The approach is free of numerical diffusion, carries no Courant constraint on advection, conserves mass at the parcel level, and — unlike Route A — is independent of the hydraulic discretization, so it works under dynamic wave and kinematic wave routing as well. It is the natural host for the multispecies reaction system of Section 2.4, since reactions are evaluated along parcel trajectories. +**Route B — Lagrangian parcel tracking (implemented, `QUALITY_SOLVER LAGRANGIAN`).** Parcels of water and their constituent loads are advected along the flow field, with dispersion applied as a superimposed Fickian random-walk step. The approach is free of numerical diffusion, carries no Courant constraint on advection, conserves mass at the parcel level, and — unlike Route A — is independent of the hydraulic discretization, so it works under dynamic wave and kinematic wave routing as well. It is the natural host for the multispecies reaction system of Section 2.4, since reactions are evaluated along parcel trajectories. -*Planned scope for Route B:* +*Delivered for Route B:* - Lagrangian parcel-tracking advection for 1D pipe and conduit flow across all SWMM link and node types, driven by velocity fields from the flow routing solver. - Random-walk dispersion 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. @@ -100,11 +102,11 @@ Two routes are being developed, and they are complementary rather than competing - Water age as a built-in reserved species. - Numerical alignment with the legacy Eulerian solver in the degenerate case (single bulk species, first-order decay, no dispersion, complete-mixing storage) as a parity gate, plus comparison to analytical solutions for simple pipe transport problems. -### 2.2 Advection-Dispersion Model — Overland Flow 📋 +### 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. -**Status:** No implementation yet. The 2D marcher solves water only; this work is sequenced behind Route A of Section 2.1, whose contact-upwinded, flux-corrected species update is the same construction applied to an unstructured 2D mesh. +**Status:** Implemented. The 2D marcher carries species mass per cell, species-major, with the same construction as Route A of Section 2.1 applied to the unstructured mesh, plus coverages, buildup, washoff and sweeping on the cells and an isotropic `DISPERSION` coefficient. Which classes are carried is resolved by the transport policy matrix printed in the report. Documented as Chapter 10 of the Water Quality Reference Manual. **Planned scope:** - 2D depth-averaged ADE for overland flow domains. @@ -120,11 +122,11 @@ Two routes are being developed, and they are complementary rather than competing - 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 🔬 +### 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. -**Status:** Design recorded — an EPANET-MSX-equivalent reaction system (user-defined rate ODEs and equilibrium DAEs, bulk and wall species, selectable integrators with adaptive error control) specified as part of the Route B strategy in Section 2.1. No implementation yet. Whichever transport route ships first, the reaction system is intended to be written once and shared, not duplicated per transport scheme. +**Status:** Implemented as the reactions process component: user-defined rate and equilibrium expressions, bulk and wall species, named coefficients and terms, selectable integrators, and pipe, tank and initial-quality scopes, configured from a `.rxn` file named in `[PROCESS_COMPONENTS]`. Five sections of the configuration (`[REACTION_SOURCES]`, `[REACTION_PARAMETERS]`, `[REACTION_PATTERNS]`, `[REACTION_REPORT]`, `[REACTION_SUBCATCHMENTS]`) are recognised and refused rather than ignored, and species concentrations are not yet written to the output file — `[REACTION_REPORT]` is what would select them. Documented as Chapter 8 of the Water Quality Reference Manual. Whichever transport route ships first, the reaction system is intended to be written once and shared, not duplicated per transport scheme. **Planned scope:** - A general reaction network specification (user-defined stoichiometry, rate laws, and kinetic parameters) applicable to pipe, overland, and groundwater transport. @@ -149,12 +151,14 @@ Two routes are being developed, and they are complementary rather than competing --- -## 4. Heat Transport 📋 +## 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. +**Status:** Implemented as the heat process component, `HEAT_TRANSPORT ON` with a `.heat` configuration file: temperature is a reserved transported species carried by the selected transport engine, with surface exchange (latent and sensible), radiative exchange (net shortwave, back longwave, atmospheric and land-cover longwave), LID layer conduction and a bed zone with conduction and hyporheic exchange. Documented as Chapter 9 of the Water Quality Reference Manual. Per-element heat attributes remain outstanding. + +**Delivered:** +- Surface energy balance for water temperature, 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). @@ -174,11 +178,11 @@ Two routes are being developed, and they are complementary rather than competing - The coupling interface for the groundwater ADE transport module (Section 2.3). - Continued legacy-parity verification alongside the rest of the hydrology suite. -### 5.2 Spatially Explicit Groundwater — FV Mesh Integration 🔬 +### 5.2 Spatially Explicit Groundwater — FV Mesh Integration ✅ **Motivation:** The two-zone aquifer of Section 5.1 is integrated per subcatchment: lateral exchange is the empirical SWMM cubic polynomial, there is no inter-subcatchment communication, and the aquifer's only feedback to the rest of the engine is an end-of-step infiltration cap. Four physically real processes are therefore not representable: saturation-excess (Dunne) overland flow when the water table reaches the surface, return flow re-emerging downslope after lateral subsurface transport, head-driven pipe and node ↔ aquifer exchange (the physical mechanism behind RDII, groundwater inflow, and sewer exfiltration), and true capillary rise feeding evaporation. -**Status:** Design recorded, following the semidiscrete finite volume multiprocess watershed formulation of Qu & Duffy (2007). No implementation yet. +**Status:** Implemented, following the semidiscrete finite volume multiprocess watershed formulation of Qu & Duffy (2007): a two-layer aquifer per mesh cell with four selectable soil-characteristic laws, three unsaturated-zone closures resolved from `AUTO`, lateral Darcy exchange, deep loss, MODFLOW-River node exchange, Dunne saturation excess and Feddes-stressed evapotranspiration. Opt-in through `[2D_AQUIFER]` rows. The Russo, Brooks-Corey and van Genuchten recharge closures are **experimental**: their equilibrium is right and their relaxation rate is this engine's generalisation rather than a published result, pending the closure-ladder and HYDRUS-1D benchmarks. Documented as Chapter 9 of the Hydrology Reference Manual. **Planned scope:** - A vertically integrated, complementary two-layer subsurface kernel on the 2D FV mesh — unsaturated moisture depth and saturated thickness per cell, joined at a moving water table. @@ -209,12 +213,11 @@ The following items have been raised in community discussions, or explored and s | Item | Reason for Deferral | |------------------------------------------------|----------------------------------------------------------------------------| -| 2D overland flow (full shallow water equations) | Local inertial finite volume model implemented (Section 1.2); full dynamic-wave SWE not currently scheduled | | Implicit 2D time integration (CVODE/ARKODE) | Explored and retired — the explicit marcher outperformed it on the benchmark models and removed two heavy dependencies | | Real-time data assimilation & sensor fusion | Requires external telemetry infrastructure not yet in scope | | Machine learning surrogate models | Research area; may be introduced as an optional experimental module | -**No longer deferred.** *GPU-accelerated solvers* have moved into development: the +**No longer deferred.** *2D overland flow with the full shallow-water equations* shipped: `MOMENTUM_EQUATION FULL_SWE` selects an Audusse-reconstructed, rotated-HLLC face flux on the same marcher, alongside `LOCAL_INERTIAL` (the default) and `DIFFUSIVE_WAVE` (Hydraulics Reference Manual §9.2). *GPU-accelerated solvers* have moved into development: the 2D marcher's kernels run through Kokkos-based plugin backends (OpenMP, CUDA, HIP, SYCL) selected at runtime above a cell-count gate, shipped as optional plugin binaries so the base build carries no GPU dependency. The 1D finite volume solver @@ -241,7 +244,15 @@ means merged and gated but not yet carried by a tag. | Per-cell 2D parameter surfaces and 2D initial conditions | unreleased | | Explicit finite volume 1D routing, `FLOW_ROUTING FV` (Section 1.1) | unreleased | | Cell-resolved Eulerian scalar transport on the finite volume mesh (Section 2.1, Route A) | unreleased | +| Lagrangian parcel transport, `QUALITY_SOLVER LAGRANGIAN` (Section 2.1, Route B) | unreleased | +| Species transport, coverages, buildup and washoff on the 2D mesh (Section 2.2) | unreleased | +| Multispecies reactions as a process component (Section 2.4) | unreleased | +| Heat transport as a process component, with water age (Section 4) | unreleased | +| Spatially explicit two-layer groundwater on the mesh (Section 5.2) | unreleased | +| Inlet junctions, `[INLET_JUNCTIONS]` (Section 1.3) | unreleased | +| Full shallow-water and diffusive-wave momentum closures for the 2D marcher (Section 1.2) | unreleased | +| Virtual junctions, and the two-component pressure approach in both solvers (Section 1.1) | unreleased | --- -*Last updated: August 2026 — Caleb Buahin, Technical Manager* +*Last updated: September 2026 — Caleb Buahin, Technical Manager* diff --git a/cmake/BundleRuntimeDeps.cmake.in b/cmake/BundleRuntimeDeps.cmake.in index 3d174ca6b..1f35e8a70 100644 --- a/cmake/BundleRuntimeDeps.cmake.in +++ b/cmake/BundleRuntimeDeps.cmake.in @@ -42,6 +42,17 @@ set(_post_excludes @OPENSWMM_BUNDLE_POST_EXCLUDES@) # 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@") +# vcpkg keeps debug binaries in /debug/bin (issue #9). Under a +# multi-config generator the config is only known HERE, at install time, so the +# ordering is decided here rather than at configure time: for a debug install +# the debug tree is searched FIRST, so a DLL that keeps its release name +# (sqlite3.dll) resolves to the debug copy instead of silently pulling the +# release one into a Debug package. The release tree stays on the list as a +# fallback for anything vcpkg ships release-only. +set(_extra_dirs_debug "@OPENSWMM_BUNDLE_EXTRA_DIRS_DEBUG@") +if(_extra_dirs_debug AND "${CMAKE_INSTALL_CONFIG_NAME}" MATCHES "^([Dd]ebug|DEBUG)$") + set(_extra_dirs "${_extra_dirs_debug};${_extra_dirs}") +endif() file(GET_RUNTIME_DEPENDENCIES RESOLVED_DEPENDENCIES_VAR _resolved diff --git a/cmake/CheckNoHydroCoupleSDK.cmake b/cmake/CheckNoHydroCoupleSDK.cmake new file mode 100644 index 000000000..eb95d7474 --- /dev/null +++ b/cmake/CheckNoHydroCoupleSDK.cmake @@ -0,0 +1,167 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2026 Caleb Buahin +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# D1 (program plan §C.1) --- the D-C6 guard. +# +# cmake -DROOTS= [-DFILES=] [-DEXCLUDE=] +# -P cmake/CheckNoHydroCoupleSDK.cmake +# +# ROOTS are scanned recursively; FILES are scanned individually (the top-level +# CMakeLists.txt, which must not drag the whole repository into a recursive +# walk: tests/output alone is ~118k files, none of them target source). +# +# D-C6: the engine, `src/couplers/*` and SWMMVis link ONLY the header-only +# `HydroCouple` interfaces. No `HydroCoupleSDK`, anywhere — recorded as not +# re-openable by a later mounting of the SDK. A rule nobody checks is a rule +# that holds only until someone is in a hurry, so this makes it a test. +# +# What counts as a violation — the two ways a dependency actually arrives: +# 1. an `#include` of anything under `hydrocouplesdk/` (any case, either +# quote style) in a C/C++ source or header; +# 2. `find_package(HydroCoupleSDK …)`, or `HydroCoupleSDK::` / `hydrocouplesdk` +# named inside `target_link_libraries(…)`, in a CMake file. +# +# Known limitation, accepted: a CMake STRING literal that happens to contain +# `find_package(HydroCoupleSDK` or a `target_link_libraries(` span naming it +# is reported, because only comments are stripped. Stripping quoted strings +# would also hide a real, quoted link argument. Outside this script no such +# string exists; if one ever needs to, exclude that file explicitly. +# +# What does NOT count, deliberately: the WORD. The files that state this rule +# (this one, FetchHydroCouple.cmake, the option's docstring) necessarily name +# the SDK in prose, and a guard that fails on its own documentation would be +# switched off within a week. Matching the include and the link, not the +# name, is what lets the guard stay on. +# +# Written as a CMake script rather than Python so it runs on every CI leg +# with nothing installed beyond the build tool itself. + +cmake_minimum_required(VERSION 3.21) + +if(NOT DEFINED ROOTS AND NOT DEFINED FILES) + message(FATAL_ERROR "CheckNoHydroCoupleSDK: give -DROOTS= and/or -DFILES=") +endif() +set(_roots "") +foreach(_r IN LISTS ROOTS) + if(NOT IS_DIRECTORY "${_r}") + # A missing root is a configuration error, not a pass: a guard that + # silently scans nothing is worse than no guard. + message(FATAL_ERROR "CheckNoHydroCoupleSDK: root '${_r}' does not exist") + endif() + get_filename_component(_ra "${_r}" ABSOLUTE) + list(APPEND _roots "${_ra}") +endforeach() + +set(_excl "") +foreach(_e IN LISTS EXCLUDE) + get_filename_component(_ea "${_e}" ABSOLUTE) + list(APPEND _excl "${_ea}") +endforeach() + +set(_sources "") +set(_cmake "") +foreach(_r IN LISTS _roots) + file(GLOB_RECURSE _s LIST_DIRECTORIES FALSE + "${_r}/*.h" "${_r}/*.hh" "${_r}/*.hpp" "${_r}/*.hxx" + "${_r}/*.c" "${_r}/*.cc" "${_r}/*.cpp" "${_r}/*.cxx" + "${_r}/*.inl" "${_r}/*.ipp") + file(GLOB_RECURSE _c LIST_DIRECTORIES FALSE + "${_r}/CMakeLists.txt" "${_r}/*.cmake" "${_r}/*.cmake.in") + list(APPEND _sources ${_s}) + list(APPEND _cmake ${_c}) +endforeach() +foreach(_f IN LISTS FILES) + if(NOT EXISTS "${_f}") + message(FATAL_ERROR "CheckNoHydroCoupleSDK: file '${_f}' does not exist") + endif() + get_filename_component(_fa "${_f}" ABSOLUTE) + if(_fa MATCHES "(CMakeLists\\.txt|\\.cmake|\\.cmake\\.in)$") + list(APPEND _cmake "${_fa}") + else() + list(APPEND _sources "${_fa}") + endif() +endforeach() + +function(_is_excluded _path _out) + set(${_out} FALSE PARENT_SCOPE) + foreach(_x IN LISTS _excl) + string(FIND "${_path}" "${_x}/" _pos) + if(_pos EQUAL 0) + set(${_out} TRUE PARENT_SCOPE) + return() + endif() + endforeach() + # Build trees and fetched dependencies are not ours to police. + if(_path MATCHES "/(build|_deps|\\.git)/") + set(${_out} TRUE PARENT_SCOPE) + endif() + # …nor is this script. It is the one file that must SPELL the patterns it + # forbids — its violation messages are CMake string literals naming + # `find_package(HydroCoupleSDK …)` — and comment-stripping does not strip + # strings. The first run over the real tree failed on exactly that: the + # guard reported itself, twice. Found only because the real-tree case is + # a gate of its own, not an afterthought to the fixtures. + if(_path STREQUAL "${CMAKE_CURRENT_LIST_FILE}") + set(${_out} TRUE PARENT_SCOPE) + endif() +endfunction() + +set(_violations "") +set(_scanned 0) + +foreach(_f IN LISTS _sources) + _is_excluded("${_f}" _skip) + if(_skip) + continue() + endif() + math(EXPR _scanned "${_scanned} + 1") + file(STRINGS "${_f}" _hits + REGEX "^[ \t]*#[ \t]*include[ \t]*[<\"][^>\"]*[Hh][Yy][Dd][Rr][Oo][Cc][Oo][Uu][Pp][Ll][Ee][Ss][Dd][Kk]/") + foreach(_h IN LISTS _hits) + string(STRIP "${_h}" _h) + list(APPEND _violations "${_f}: ${_h}") + endforeach() +endforeach() + +foreach(_f IN LISTS _cmake) + _is_excluded("${_f}" _skip) + if(_skip) + continue() + endif() + math(EXPR _scanned "${_scanned} + 1") + file(READ "${_f}" _text) + # Strip CMake line comments first: prose that names the SDK is allowed, + # a call that uses it is not. + string(REGEX REPLACE "#[^\n]*" "" _code "${_text}") + string(TOLOWER "${_code}" _lc) + if(_lc MATCHES "find_package[ \t\n]*\\([ \t\n]*hydrocouplesdk") + list(APPEND _violations "${_f}: find_package(HydroCoupleSDK …)") + endif() + if(_lc MATCHES "target_link_libraries[ \t\n]*\\([^)]*hydrocouplesdk") + list(APPEND _violations "${_f}: target_link_libraries(… HydroCoupleSDK …)") + endif() +endforeach() + +if(_violations) + list(LENGTH _violations _n) + list(JOIN _violations "\n " _pretty) + message(FATAL_ERROR + "D-C6 violated: ${_n} HydroCoupleSDK dependenc(ies)\n ${_pretty}\n" + "The engine may use only the header-only HydroCouple interfaces " + "(program plan decision D-C6). This is not re-openable.") +endif() + +message(STATUS "D-C6 guard: ${_scanned} files scanned, no HydroCoupleSDK include or link") diff --git a/cmake/FetchHydroCouple.cmake b/cmake/FetchHydroCouple.cmake new file mode 100644 index 000000000..c3493629c --- /dev/null +++ b/cmake/FetchHydroCouple.cmake @@ -0,0 +1,152 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2026 Caleb Buahin +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# HC-1 --- the HydroCouple interface headers, and nothing else. +# +# Program plan D-C6: the engine, `src/couplers/*` and SWMMVis link ONLY the +# header-only `HydroCouple` interfaces. No `HydroCoupleSDK`, anywhere, and +# that decision is recorded as not re-openable by a later mounting of the SDK. +# `HydroCouple` is an INTERFACE library exporting `HydroCouple::HydroCouple`, +# so "link" here costs an include directory and `cxx_std_20` — no objects, no +# runtime dependency, nothing to ship. +# +# THE PIN IS THE POINT. HydroCouple 2.0.0 renumbered `WorkflowStatus` after +# tagging, without a release: a build against a moving ref compiles fine today +# and silently changes the meaning of a status enum tomorrow. The program plan's +# instruction is to pin a commit, so `OPENSWMM_HYDROCOUPLE_GIT_TAG` defaults to +# a full SHA rather than a branch or a tag, and a tag is accepted only because +# a caller may deliberately want one. +# +# Resolution order: +# 1. An already-defined `HydroCouple::HydroCouple` target (superbuild). +# 2. `find_package(HydroCouple CONFIG)` --- a system/vcpkg install, or the +# sibling checkout via `CMAKE_PREFIX_PATH` / `HydroCouple_ROOT`. Preferred +# in development, because it uses the tree the developer already has. +# 3. `FetchContent` at the pinned commit. +# +# Step 3 is deliberately last: silently downloading a second copy of interfaces +# the developer already has checked out is how two versions of an ABI end up in +# one build. + +include_guard(GLOBAL) + +set(OPENSWMM_HYDROCOUPLE_GIT_REPOSITORY + "https://github.com/hydrocouple/HydroCouple.git" + CACHE STRING "Git remote for the HydroCouple interface headers") + +# bef95cb --- "fix(abi): factory-component instances carry their ownership in +# the type" (2026-09-04). ABI 2. On origin/dev since 2026-09-29. +# +# The FULL 40-character SHA, not the abbreviation: an abbreviated SHA is only +# resolvable after the clone has fetched enough history to disambiguate it, +# and the full one cannot become ambiguous as the repository grows. +set(OPENSWMM_HYDROCOUPLE_GIT_TAG + "bef95cb19310c6560e7f35158515bf27ec57ac29" + CACHE STRING + "Pinned HydroCouple commit. A SHA, not a branch: WorkflowStatus was \ +renumbered without a release, so a moving ref is a silent ABI change.") + +# D1 (2026-09-29): the ABI the pin stands for, enforced on EVERY resolution +# path, not only the fetch. Resolution tries a local install before the pinned +# fetch, so without this the pin governs only machines that have no +# HydroCouple installed — anyone with a sibling checkout silently compiles +# against whatever ABI that checkout holds. Measured on the day this was +# written: pin bef95cb = ABI 2, origin/dev = ABI 3 (7e6d142, releaseState), +# the developer's uncommitted working tree = ABI 4. HC-1's own "ON via +# CONFIG" check installed that working tree and compiled against ABI 4 +# believing it was on the pin. `tests/compile_check/hydrocouple_headers.cpp` +# turns this number into a static_assert, so a mismatch is a build error +# naming both ABIs rather than a runtime surprise. Bump it together with +# OPENSWMM_HYDROCOUPLE_GIT_TAG, never separately. +set(OPENSWMM_HYDROCOUPLE_EXPECTED_ABI "2" CACHE STRING + "HYDROCOUPLE_ABI_VERSION the pinned commit declares; asserted at compile time") + +function(_openswmm_hydrocouple_report _how) + if(NOT OPENSWMM_HYDROCOUPLE_QUIET) + message(STATUS "HydroCouple interfaces: ${_how}") + endif() +endfunction() + +# --- 1. already satisfied ----------------------------------------------------- +if(TARGET HydroCouple::HydroCouple) + _openswmm_hydrocouple_report("using the target already defined in this build") + return() +endif() + +# --- 2. an installed or sibling copy ---------------------------------------- +find_package(HydroCouple CONFIG QUIET) +if(HydroCouple_FOUND AND TARGET HydroCouple::HydroCouple) + _openswmm_hydrocouple_report( + "found ${HydroCouple_VERSION} via CONFIG at ${HydroCouple_DIR}") + return() +endif() + +# --- 3. the pinned fetch ----------------------------------------------------- +# +# History worth keeping: on 2026-09-27 this path was verified BROKEN, because +# the pin was 6 commits ahead of origin/dev and unpushed — the ABI-2 work the +# engine targets existed only in the developer's checkout. FetchContent's own +# message for that is the unhelpful "Failed to checkout tag", which sends the +# reader looking for a typo. HydroCouple was pushed on 2026-09-29 and this +# path now resolves; see the verification in the HC-1 commit. +# +# What the pre-flight below can and cannot do. `git ls-remote` asks the remote +# about REFS, so it can prove a branch or tag name is wrong. It cannot ask +# about an arbitrary commit, so for the SHA pin actually in use an unpushed or +# mistyped commit still surfaces as FetchContent's opaque checkout error. If +# you see "Failed to checkout tag" with a SHA pin: check that the commit has +# been pushed to OPENSWMM_HYDROCOUPLE_GIT_REPOSITORY before anything else. +include(FetchContent) + +# Ask the remote whether the pin is even reachable before cloning, so the +# failure is one message instead of a clone plus a checkout error. +find_package(Git QUIET) +if(GIT_EXECUTABLE) + execute_process( + COMMAND ${GIT_EXECUTABLE} ls-remote --exit-code + ${OPENSWMM_HYDROCOUPLE_GIT_REPOSITORY} ${OPENSWMM_HYDROCOUPLE_GIT_TAG} + RESULT_VARIABLE _hc_lsremote_rc + OUTPUT_QUIET ERROR_QUIET) + # A SHA is not a ref, so ls-remote failing is expected for one and + # meaningful for the other. Only a ref-shaped pin is checked here. + if(NOT OPENSWMM_HYDROCOUPLE_GIT_TAG MATCHES "^[0-9a-fA-F]+$" + AND NOT _hc_lsremote_rc EQUAL 0) + message(FATAL_ERROR + "OPENSWMM_HYDROCOUPLE_GIT_TAG '${OPENSWMM_HYDROCOUPLE_GIT_TAG}' is " + "not a ref on ${OPENSWMM_HYDROCOUPLE_GIT_REPOSITORY}.") + endif() +endif() + +# HydroCouple's own CMakeLists offers tests; we want the interface target only. +set(HYDROCOUPLE_BUILD_TESTS OFF CACHE BOOL "" FORCE) + +FetchContent_Declare( + HydroCouple + GIT_REPOSITORY ${OPENSWMM_HYDROCOUPLE_GIT_REPOSITORY} + GIT_TAG ${OPENSWMM_HYDROCOUPLE_GIT_TAG} + GIT_SHALLOW FALSE # a SHA cannot be fetched shallowly from all remotes + EXCLUDE_FROM_ALL +) +FetchContent_MakeAvailable(HydroCouple) + +if(NOT TARGET HydroCouple::HydroCouple) + message(FATAL_ERROR + "HydroCouple was fetched at ${OPENSWMM_HYDROCOUPLE_GIT_TAG} but did " + "not define HydroCouple::HydroCouple. The interface target's name " + "changed, or the pin points at a commit that predates it.") +endif() + +_openswmm_hydrocouple_report("fetched at pinned ${OPENSWMM_HYDROCOUPLE_GIT_TAG}") diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 000000000..fb07d61fc --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +www.hydrocouple.org diff --git a/docs/Doxyfile b/docs/Doxyfile index f3f754413..86f8f7da8 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -48,13 +48,13 @@ PROJECT_NAME = "OpenSWMM Engine" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 6.0.0-alpha.3 +PROJECT_NUMBER = 6.0.0-alpha.4 # 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.3)" +PROJECT_BRIEF = "Data-oriented, plugin-extensible SWMM Engine (6.0.0-alpha.4)" # 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 @@ -293,7 +293,8 @@ TAB_SIZE = 4 ALIASES = "license=\par License\n" \ "description=\par Discription\n" \ "mod=\par Modified\n" \ - "moditem{3}=\b \date\1 \author\2 \3\n" + "moditem{3}=\b \date\1 \author\2 \3\n" \ + "status{1}=\htmlonly\1\endhtmlonly" # 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 @@ -1002,11 +1003,10 @@ INPUT = ../README.md \ ../src/output \ ../include \ ./images/*.png \ - ./manuals/user/figures/*.png \ - ./Updates.md \ + ./manuals/engine/figures/*.png \ ./authors.md \ ./manuals/manuals.md \ - ./manuals/user \ + ./manuals/engine \ ./manuals/reference \ ./manuals/application @@ -1122,7 +1122,7 @@ EXCLUDE_SYMLINKS = NO # exclude all test directories for example use the pattern */test/* EXCLUDE_PATTERNS = */SWMM* \ - manuals/user/figures/* \ + manuals/engine/figures/* \ manuals/reference/*/media/* \ manuals/reference/*/figures/* \ manuals/application/figures/* \ @@ -1140,28 +1140,36 @@ EXCLUDE_SYMBOLS = # that contain example code fragments that are included (see the \include # command). -EXAMPLE_PATH = +# The manuals quote worked-example decks verbatim with \snippet between +# ;//! [marker] pairs, so prose and deck cannot drift apart. +EXAMPLE_PATH = ./figures/decks # 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 = +EXAMPLE_PATTERNS = *.inp \ + *.rxn \ + *.ard \ + *.age \ + *.heat \ + *.py # 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 +EXAMPLE_RECURSIVE = YES # 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 \ + ./figures/png \ + ./manuals/engine/figures \ ./manuals/reference/hydrology/media/media \ ./manuals/reference/hydraulics/media/media \ ./manuals/reference/quality/media/media @@ -1474,7 +1482,8 @@ HTML_STYLESHEET = # documentation. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_EXTRA_STYLESHEET = ./custom/css/doxygen-awesome.css +HTML_EXTRA_STYLESHEET = ./custom/css/doxygen-awesome.css \ + ./custom/css/manual.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 @@ -1489,6 +1498,7 @@ HTML_EXTRA_FILES = custom/js/doxygen-awesome-darkmode-toggle.js \ custom/js/doxygen-awesome-interactive-toc.js \ custom/js/doxygen-awesome-paragraph-link.js \ custom/js/doxygen-awesome-tabs.js \ + custom/js/manual-interactive.js \ custom/mathjax-config.js # The HTML_COLORSTYLE tag can be used to specify if the generated HTML output @@ -2584,7 +2594,7 @@ SKIP_FUNCTION_MACROS = YES # the path). If a tag file is not located in the directory in which Doxygen is # run, you must also specify the path to the tagfile here. -TAGFILES = +TAGFILES = tags/swmmvis-manual.tag=https://www.hydrocouple.org/openswmm.gui/ # When a file name is specified after GENERATE_TAGFILE, Doxygen will create a # tag file that is based on the input files it reads. See section "Linking to @@ -2611,7 +2621,7 @@ EXTERNAL_GROUPS = YES # be listed. # The default value is: YES. -EXTERNAL_PAGES = YES +EXTERNAL_PAGES = NO #--------------------------------------------------------------------------- # Configuration options related to diagram generator tools diff --git a/docs/DoxygenLayout.xml b/docs/DoxygenLayout.xml index e039e7e0c..fbb53652f 100644 --- a/docs/DoxygenLayout.xml +++ b/docs/DoxygenLayout.xml @@ -5,7 +5,10 @@ - + + + @@ -62,7 +65,7 @@ - + diff --git a/docs/analysis/PYTHON_BINDINGS_IMPLEMENTATION_2026-09-26.md b/docs/analysis/PYTHON_BINDINGS_IMPLEMENTATION_2026-09-26.md new file mode 100644 index 000000000..2efe37263 --- /dev/null +++ b/docs/analysis/PYTHON_BINDINGS_IMPLEMENTATION_2026-09-26.md @@ -0,0 +1,132 @@ +# Python bindings implementation and validation + +Date: 2026-09-26. Repository: `openswmm.engine`. + +The approved review's binding gaps are implemented and locally validated using +the **Conda `openswmm` environment, Python 3.12.13**. The original review and its +118-function inventory remain the historical baseline. This report records the +resulting code and separates local evidence from release checks still pending. + +## Outcome + +| Measure | Result | +|---|---:| +| Current modern native exports | 1,173 | +| Recognized executable binding uses | 1,170 | +| Documented compatibility exclusions | 3 | +| Unexplained binding gaps | **0** | +| Original missing exports implemented | 118 | +| Additional staged-writer export implemented during this work | 1 | +| Installed-wheel tests | **1,221 passed** | +| Minimal-build tests, optional native features disabled | **37 passed** | +| Standalone source-contract tests | **34 passed** | +| Engine type-checking | **40 source files passed** | +| Strict consumer typing, source and installed-wheel runs | **passed** | +| Sphinx HTML with warnings treated as errors | **passed** | + +The exclusions are `swmm_get_current_time`, `swmm_get_last_error` and +`swmm_get_last_error_msg`: Python provides current time and typed exceptions. +`swmm_error_message` is recognized through its executable `.pxd` helper. +Reachability verifies that a wrapper calls an export; it is not numerical +validation of every native process. + +## Delivered domains and regression coverage + +| Area | Python surface | Principal evidence | +|---|---|---| +| Aquifer hydrology | `surface2d.groundwater`: rows, options, nodes, scalar/bulk state, sigma columns, ledgers and species | `test_groundwater.py`: mixed cells, authoring units, save/reopen, inactive/stale states, array ownership, water residual | +| Groundwater transport | `.groundwater.transport`: parameters, sorption, initial quality, boundaries, sources and source-species terms | `test_new_domain_tables.py`: all table round trips/removals, quad edge 3, sidecar reference, species snapshots/ledgers | +| Surface quality | `surface2d.quality`: coverage, initial loading, curb length, runtime buildup | `test_new_domain_tables.py`: scope precedence with nonzero buildup, bad coverage, round trip, independent arrays | +| ARD transport and capability | `solver.transport`, `transport_matrix`, thread diagnostics and typed records | `test_transport.py`: invalid coefficients, matrix axes, thread limits, US/SI persistence with registered component file | +| Existing metadata | Initial-quality file/is-file, heat series names, component registry, relative time metadata, authored link orientation, compatibility writer | `test_metadata_bindings.py`: round trips and all compatibility profiles | +| Editing, output and forcing | Batch deletions, live output refresh, local climate/temperature/age/seepage forcing, staged serialization | `test_binding_completion.py`: atomic invalid batches, duplicates, partial output records/footer adoption, callback failures and reentry rejection | +| 2D diagnostics | Rainfall, accumulated rain/coupling volumes, weights and output selections | Nonzero mixed triangle/quad rainfall fixtures in both US and SI units; scalar/bulk parity and volume increments | +| Safety | Owner retention, invalidation, callback error capture, per-owner access guards, file-reader guards | `test_native_safety.py`: destroy/GC/transfer, callbacks, independent and conflicting access, subprocess use-after-destroy regression | +| Packaging and typing | `py.typed`, public exports, new stubs and optional import behavior | Installed marker/source checks, stub/runtime signature checks, strict consumer typing outside the repository | +| Reports | Current routing diagnostics, percentage conversion, snow/forcing/coupling/groundwater totals | Snapshot values compared with the native-backed mass-balance view after a completed run | + +The GeoPackage bulk writer now validates matching lengths, copies strided +arrays, handles empty writes and refuses reads after close. Native series reads +pin their file handles against concurrent close or refresh. The 2D/editor views +retain the Python engine owner rather than retaining an unowned native address. +Callbacks cannot unwind through the C ABI; the first exception is re-raised +when native execution returns. + +## Documentation and CI + +Sphinx now includes guides for native-call safety, transport capabilities, +groundwater/transport, surface quality, batch edits, live output and staged +serialization. They state lifecycle, indices, units, snapshot ownership and +persistence rules. Existing metadata and mass-balance guides, type stubs, +README, changelog and the application manual's Python chapter were updated. + +Two details established by tests are explicit in the documentation: + +- ARD persistence requires a registered process component and configuration-file + path. Setting a dispersion value alone does not add that registration. +- Native continuity-error accessors return fractions; report fields named + `continuity_error_pct` now convert to percentages. Report diagnostics use the + current typed accessor instead of silently probing retired method names. + +CI has a reusable source-contract gate and an actual optional-feature-disabled +build. The gate is a release dependency. Wheel testing includes modern, legacy, +top-level and integration tests; only typing fixtures are excluded from runtime +collection. Typing triggers include Cython and native headers, and the Python +workflows include `swmm6_rel`. The old fuzzy parity report is marked historical; +the regenerated provenance report shares the strict tokenizer and counts +1,173 exports. MCP-only exclusions do not exempt Python bindings. + +The minimal build exposed native 2D references in core routing and reports that +prevented compilation/linking with 2D disabled. Feature guards fixed those +build failures without changing the enabled-feature behavior. + +## Reproducible local validation + +Interpreter: `/Users/calebbuahin/miniforge3/envs/openswmm/bin/python`. +NumPy 2.4.4, Cython 3.2.4 and pytest 9.0.2. Additional mypy/Sphinx dependencies +were loaded from a temporary directory; the Conda installation was not replaced. + +The final local wheel was built with 2D, GeoPackage, HDF5 and the OpenMP GPU +plugin enabled, and **both experimental fast-kernel switches disabled** to use +the release-default numerical behavior. It was installed with `pip --no-deps` +into `/tmp/openswmm-conda-wheel-installed`, then tested with the Conda interpreter. +All 62 Python source, stub and typing-marker files matched the built wheel. + +```sh +conda run -n openswmm python python/scripts/test_staged_package.py \ + /tmp/openswmm-conda-wheel-installed python/tests --ignore=python/tests/typing -q +``` + +`test_staged_package.py` removes only the OpenSWMM editable redirector for that +process and asserts that imported package modules come from the requested +prefix. This matters because the environment's editable install otherwise +selects older extensions despite `PYTHONPATH`. No installed environment files +are changed by this test runner. + +For a plain CMake install, `python/scripts/stage_python_sources.py PREFIX` +copies the Python modules/stubs that scikit-build adds to wheels. The minimal +build disables 2D, GPU, GeoPackage and HDF5 and exercises omitted-feature imports, +core solver lifecycle and transport configuration. + +The companion JSON preserves input hashes, audit output, wheel hash, feature +settings and complete final runtime test summaries. + +## Remaining release qualification + +Local implementation and macOS arm64 validation are complete. The following +remain external release checks, not claimed successes: + +- Run the updated GitHub Linux/Windows/macOS and Python-version wheel matrix. +- Validate portable dependency repair and minimum-OS tags. The locally built + `cp312-macosx_26_0_arm64` wheel is a local test artifact, not a published wheel. +- Run sanitizer builds where supported. The local safety regressions include a + subprocess test, but no ASan/TSan run was performed. +- Validate external documentation links/intersphinx inventories. The successful + Sphinx run disabled external inventories and retained the repository's + existing warning suppression categories. + +The earlier CI fix remains commit `4895765e`. The implementation commit includes the +bindings, documentation, contract gates and their native writer/build dependencies. +The validation above ran in the working tree alongside pre-existing work in +progress; the companion hashes identify the tested inputs. Unrelated changes +remain outside this commit. diff --git a/docs/analysis/PYTHON_BINDINGS_IMPLEMENTATION_2026-09-26_evidence.json b/docs/analysis/PYTHON_BINDINGS_IMPLEMENTATION_2026-09-26_evidence.json new file mode 100644 index 000000000..1c05101e7 --- /dev/null +++ b/docs/analysis/PYTHON_BINDINGS_IMPLEMENTATION_2026-09-26_evidence.json @@ -0,0 +1,394 @@ +{ + "date": "2026-09-26", + "scope": "Python binding implementation and local qualification; original review remains historical", + "python": "/Users/calebbuahin/miniforge3/envs/openswmm/bin/python", + "python_version": "3.12.13", + "conda_environment": "openswmm", + "versions": { + "numpy": "2.4.4", + "Cython": "3.2.4", + "pytest": "9.0.2", + "scikit-build-core": "0.12.2" + }, + "features": { + "2d": true, + "geopackage": true, + "hdf5": true, + "gpu_plugin": "OpenMP", + "fast_xsect_lookup": false, + "fast_manning_pow": false + }, + "wheel": { + "path": "/tmp/openswmm-conda-release-wheels/openswmm-6.0.0a4-cp312-cp312-macosx_26_0_arm64.whl", + "sha256": "f9f6eb334e3dac6e5c23bf6b7f91a757c4ae4494d176151873be9ec4d6d3bd9d", + "installed_prefix": "/tmp/openswmm-conda-wheel-installed", + "portable_repair_validated": false + }, + "audit": { + "totals": { + "c_functions": 1173, + "pxd_declared": 1173, + "c_enums": 42, + "py_enums": 78 + }, + "unbound_without_exemption": [], + "intentional_exclusions": [ + "swmm_get_current_time", + "swmm_get_last_error", + "swmm_get_last_error_msg" + ], + "functions_missing_from_pxd": {}, + "declared_but_unsurfaced_in_pyx": [ + "swmm_get_current_time", + "swmm_get_last_error", + "swmm_get_last_error_msg" + ], + "enums_missing_from_python": {} + }, + "validation": { + "installed_wheel_tests": { + "passed": 1221, + "seconds": 90.36 + }, + "minimal_build_tests": { + "passed": 37, + "seconds": 1.36 + }, + "source_contract_tests": 34, + "mypy_sources": 40, + "strict_consumer_sources": 1, + "installed_consumer_sources": 1, + "sphinx_warnings_as_errors": "pass; external intersphinx inventories disabled", + "python_sources_stubs_marker_identical_to_wheel": 62 + }, + "remaining_release_checks": [ + "GitHub cross-platform wheel matrix", + "portable wheel dependency repair verification", + "sanitizer builds", + "external documentation links/intersphinx inventories" + ], + "source_sha256": { + ".github/workflows/deployment.yml": "cb934f45ffdaee4e244508c4c29669b712aa7f3440abd5769bd22430f2b558bf", + ".github/workflows/python_contracts.yml": "a38515c4372653d89d85a3f6f9ac27fd5205ca51a45d52b5c3d12704ce9d8ed3", + ".github/workflows/typing.yml": "41780d692c6e70a723fcfefb5d1d1f77d0f9be5a5b938ff715d96d7d1a6822cc", + ".github/workflows/unit_testing_python.yml": "13cf0b7a0406853d868ab12f4364d5e419e2948f6e4981931c1a1540488ae225", + "include/openswmm/engine/openswmm_2d.h": "268d399af5e5d0854ee6d9d7007374ca5dbbc46d648f199d98b9c82a3b9a21d9", + "include/openswmm/engine/openswmm_callbacks.h": "b9478a3215576729f8ec9dfbaedc5dfa71fa9a314dbc909bd31f1e13507c23bf", + "include/openswmm/engine/openswmm_climate.h": "abe39b945d12c733b9c5e80e3e0e01283215b0f41b0edda311e988d5f7a0fd79", + "include/openswmm/engine/openswmm_controls.h": "d9044dffb854deb66032db6b64843edf6ace9a807984d9a2e6adbd2ee77c45e8", + "include/openswmm/engine/openswmm_datetime.h": "585fd3307bff50e0118730d511f7e4ebb93ef0441a4636184fdf4f318270861d", + "include/openswmm/engine/openswmm_edit.h": "8aa855f546f523050ac40661167f32c0bd15b47746000d6be1301d3283aa9955", + "include/openswmm/engine/openswmm_engine.h": "60572711a25256d79fe5d4d4792ac5465b63e62c600c8815c14e35e91aec2c44", + "include/openswmm/engine/openswmm_engine_export.h": "2bdfbd8f07f33540dd2225a0f19415000afabe7f5e6c026daeaaae0d62434ff7", + "include/openswmm/engine/openswmm_forcing.h": "0488d4e4f1b0526f14589ca1a531426f99c986df1927e3326613bc36ad212cb1", + "include/openswmm/engine/openswmm_gages.h": "ba2a26d0f4dfcc64d43146ccda1eb253b72ede3c9c9cb281793e2e9f445561ac", + "include/openswmm/engine/openswmm_geopackage.h": "de86f4e904bfcefb65320e7be3e964077fde8b8db06e54e0d9d89f1243650f45", + "include/openswmm/engine/openswmm_gw2d.h": "4c0b3da7ddde9bdcfd4a084c0ab4965e6ea95276b203ded9d102eca2881e5d6f", + "include/openswmm/engine/openswmm_gw_transport.h": "4623357082c47c9d0b5f4e06069d4d167fafbfb331be6c1a186cc2e781f8dbf3", + "include/openswmm/engine/openswmm_heat.h": "8495210d3006a20a823fc3139066257af4f657e5643b094c580ce272b1304208", + "include/openswmm/engine/openswmm_hotstart.h": "1f6ae581f9cecbc5ab05feba89cc79ae66b968031b7c69fd64a0e308c7c9c49e", + "include/openswmm/engine/openswmm_infil2d.h": "9210857c289caa28d2a25161584669439692e60d9b4989e0805ebfe5d89060e0", + "include/openswmm/engine/openswmm_inflows.h": "ef8e7e5c047a750f00d6b4d04ea71d3fe78812b6f951a8d0b6237e60f22b73ff", + "include/openswmm/engine/openswmm_infrastructure.h": "9798057095169c49d7f77f306d6e3e6a686c72f7055a0f03729675aa1f7641b2", + "include/openswmm/engine/openswmm_initial_quality.h": "48ccfc67100b9f70078190a5ac83c9e747e2f2c46042c6adf6dfaf3913ba5917", + "include/openswmm/engine/openswmm_links.h": "23035dd45330ecf1935e1376dd1e25aaa5cb2f56e4d7ccb5d587ce06de079193", + "include/openswmm/engine/openswmm_massbalance.h": "d6f904cc1e7fa45f73adcce1b07beacc95f6ac5210c158c4408fd8737e098a4b", + "include/openswmm/engine/openswmm_model.h": "b59cca6d8e62e28f88b306678a88423a7e489d75faab941e5fd6628cdc3f4d72", + "include/openswmm/engine/openswmm_nodes.h": "7d35699f13ce94002673d87333d2d8acc68e203834b429ca03c65444b65ec070", + "include/openswmm/engine/openswmm_output.h": "0df7cc589e065c7e6611b94b3c997de0c27fe4384d2f593bb8ee97044182b48b", + "include/openswmm/engine/openswmm_pollutants.h": "5a3e13ed50c066ec20ffebc5524a8d6ae7eea74ba41a52894a6c1c09bf166045", + "include/openswmm/engine/openswmm_process_components.h": "9c9d0010018be37aa3bb62a088d5a1a902967fc76832872736a66782fdd57526", + "include/openswmm/engine/openswmm_quality.h": "f1d650bca9a5b03f9d18d3eda98823814acd473e54fc613b3e4a61965de8eeb9", + "include/openswmm/engine/openswmm_reactions.h": "deea68c6852a3a7e780eccc0edf957714662de49592849e21e7c66d3ac4e9e49", + "include/openswmm/engine/openswmm_spatial.h": "45ffcd0fe1421b1c35500a49d2b6b4a6bcfcd191cdf053ecdaa0ddebe0caad4c", + "include/openswmm/engine/openswmm_sq2d.h": "c60ac1ffc13c125687e3916055f09f2e9fb0a83d57ddcf6467af37f4ac240de5", + "include/openswmm/engine/openswmm_statistics.h": "131e483be0a6a2661a36a809077e541149896ef64d1340a8736dd04c4db51c22", + "include/openswmm/engine/openswmm_subcatchments.h": "20592588c70de0ab6abda4ddb0d30e60b417c6c3fef717a9a8429074dccbb71e", + "include/openswmm/engine/openswmm_tables.h": "c37c668e16ef16f5ba0df132955dc86bbf7fbab60936671a42c4aefb3bcc14ce", + "include/openswmm/engine/openswmm_transport.h": "e07bdfea9b24d8707950986dd7e37e2969e2705bc504408ca91235f3791670f2", + "include/openswmm/engine/openswmm_water_age.h": "5e02a102c515600156a1e519ee06b3bfbaed7786238032b9c3843025f1b0bb89", + "include/openswmm/engine/openswmm_xsect.h": "0c601562cddb7a15d124e4efc38ec70a1da3081fcc96b066dabb9cae401cf549", + "python/CMakeLists.txt": "ec1d3ab3fda23627d171def0850dd491f6065bec88c3cf31b28891ae49f4b228", + "python/cmake/CythonHelpers.cmake": "b5d57d299d333dd813299672754a01f577b9f4390349273bc0d32058fdab765f", + "python/docs/api.rst": "0eaf0125e24d9ca49d9923e7bf574617eb4a3bcfa536573ef42c624b0cd37b3b", + "python/docs/conf.py": "ee445eaf0cb92a13db003bb697cf8062c6950a55b603dc529d30b24b47d2a18e", + "python/docs/guide/2d.rst": "574eb3c3ea3a1c8ac0a6144bc5e3412b9fca6dac07ffa80589e2391f354154b9", + "python/docs/guide/binding_updates.rst": "f05f8dea739f04e599c300dcad53b19cc468ac7c7108cd9dd87993f35eabccb3", + "python/docs/guide/climate.rst": "3255d74cf6cf5e392ab8a8ca2598430c5023199a5e8da82c70bed686cf619767", + "python/docs/guide/concepts.rst": "ff7e77d7788c577317e11a499d8217c8e14da848d14d3f4e557cfaf4b533079d", + "python/docs/guide/controls.rst": "babb416a7c727dd2c6f928da69c4486be2046a30dc867f2c99f41817d0485327", + "python/docs/guide/datetime.rst": "0f8bc61a5919e1ed337c19f254f066db326c74b1d14e055cf4609bb60f138c7d", + "python/docs/guide/editing.rst": "3b716d94397ef09d4ad5fa8bd3ad0ac3cff51105db7c4a6780bd2f5b72c4ffef", + "python/docs/guide/error_handling.rst": "038b7c48432bee9d86e553ed09492ad00c6668dbec4d145c3cb2540a0d466ff7", + "python/docs/guide/forcing.rst": "1e3fdafcb6aa7a7a1a81b803b33c05514e41098d8360fa22d328a75df00aa2a7", + "python/docs/guide/gages.rst": "7cc0be27d677ad612ca58a37c175fa6d5990f8532298eecec48e0ea330da9768", + "python/docs/guide/geopackage.rst": "daea25d08f4d7475a3a26d12983de653fbb452faaee8ca528fd7e0ac74a91b4d", + "python/docs/guide/groundwater.rst": "fb5a160a28c35d4a8d1622dc40acb45669d34bce17d3e550de80ea4d0d4c410a", + "python/docs/guide/heat.rst": "1f4391d06ec3deef9ac435816167ffb4b7f16680e81c7314983ac36ec7f66915", + "python/docs/guide/hotstart.rst": "a9bf60bc027f1cea556a68aeb99fce5c0dc13a2047f5d60eef83af685050d485", + "python/docs/guide/index.rst": "d40be4729dfb7e0e6e136c0e70a5f741b79507dc06f2d108c3c1f8345c5ab846", + "python/docs/guide/inflows.rst": "9925e74d465658c085f83d8961621f44bf73a17b9e4419147602f97c3f496f3e", + "python/docs/guide/infrastructure.rst": "657a493bdba7b8d10fa9577edd0ecb94462699c48136022c5a4657b3fbe2cce3", + "python/docs/guide/initial_quality.rst": "6779b35f3e574b6ca764ee696c67b29e8045ce7da2c5debe1326fc741b5e6931", + "python/docs/guide/install.rst": "38a64479c78b92ad104035d64a6d4af9115a11c8a7c69bc78a6339bbe9527100", + "python/docs/guide/links.rst": "2a5faa82a670a35a1954497b7b3f586f1de5bd9e53fa59d5b817f61de83b1d88", + "python/docs/guide/massbalance.rst": "0f5df4e96f6d0cea4867ff55b6164005ec4d8cfb5e8e459e6d86fb5c4ed4d734", + "python/docs/guide/model_builder.rst": "993b9ac47157a630701397636d61dcb6643fec3bfad0ab4ae09a3c7bfd6e0705", + "python/docs/guide/native_safety.rst": "f40d4f6a54edf43df4d5df65f6aa9ed34391dcfa69043dbc52fc71a7623bd9f0", + "python/docs/guide/nodes.rst": "7ebac5e2080d0175f72600826f822d2210559db1e6ded5c7c678da2534c1984d", + "python/docs/guide/output_reader.rst": "64fc6f8d56f9fbfedbfdd7cbd47eea6d485784e9d59c75eeea755f43983fd1d7", + "python/docs/guide/plotting.rst": "f9173d62e83d2d79dc9bea9702c2c39924ea8c2554129cf8395ba455889e6a5e", + "python/docs/guide/pollutants.rst": "3c7856e9f973545402c701ad2dcecceb149faa6d98a943e258f8c7a1731269ab", + "python/docs/guide/process_components.rst": "c4f72dc7802e4ef6f0021316b20b557430b91f1d1c64febaed2322d6521fe846", + "python/docs/guide/quality.rst": "5a284ea6d2f059e259ea8dc276e1a357dc3227baa88b213583ce127aae89d924", + "python/docs/guide/quickstart.rst": "f63631d93aa5d337a8cc8f41ea75e73bd01238363955ce8d4e29ecf34e462308", + "python/docs/guide/reactions.rst": "1dc363c09c3eb6890cbfd6d596d5b4f564c5f404ef36ff11b046820159dbe0bf", + "python/docs/guide/solver.rst": "66db755252d8e56ddea02191e24e99687112c86df880e4bdc362a5fc8a244d1c", + "python/docs/guide/spatial.rst": "d62b8f309e74a6d1d32b00831913659da434aac4dacd337de2e6c9ff59842ba4", + "python/docs/guide/statistics.rst": "692221411cd6dba10beec9982c8c9e59cffa737462607208d60cb1974366f163", + "python/docs/guide/subcatchments.rst": "e3c00128d109bbce40e528baca3408600b5a20978eb4ca86c69a4176445a0f49", + "python/docs/guide/surface_quality.rst": "ca3e7726229470fbaeea757bd71b030e7a816570bb95e8f0c2569311b4d34270", + "python/docs/guide/tables.rst": "e1085046b1cdaab9e1ff763796d7ea5a22b5d6c422861d26aae0f2a6e9836bbe", + "python/docs/guide/transport.rst": "71732b4cbe861130a4cff66e0cc5cafd97e358c65f6e94582afd12ebd5803131", + "python/docs/guide/water_age.rst": "b24a7852e8c567d8711cabb793768e80a4a639757caf8532684d98862660282d", + "python/docs/guide/xsect_geometry.rst": "18f9421de50772a8e8b639d2a68143c010637165d5fb2d0a284803ab7bbf99aa", + "python/docs/index.rst": "2ac546b5c12c0dbdf7f4db1155b5cff53b80c11c98e8b60a5f58554063610ea7", + "python/docs/legacy/index.rst": "2ebcbf032a47a6b6079a15ffdf792a78e928d9fd578a118eccc6179f783e9428", + "python/docs/legacy/output.rst": "8ae087871b39a173259ca19f6cec23d6eef2cfdc7180199331f0765b4a948071", + "python/docs/legacy/solver.rst": "958949e9b0a4e8337d9ed8503ae483254af34bdeb301ab6b2f75ca5209e20ebb", + "python/docs/license.rst": "e45c68e560c9ec410008321fa0470d163c2ec8ffe4bfc3154a7b87eea339f835", + "python/docs/migration/swmm5_to_swmm6.rst": "3f963d36e02eea26cde22c82eb7b2c2984566e5e628cc501e92dc15f5e71d1fc", + "python/openswmm/CMakeLists.txt": "9c74c2b1cd674e38192e79d5adbb8855f89645843380318384a601954f26e9f5", + "python/openswmm/__init__.py": "a1457d58b2aa3ca34faacef4c87cc3c5cd6c85cfddf73d98ca22934b913e35bb", + "python/openswmm/data/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "python/openswmm/engine/CMakeLists.txt": "5b7cca3cc0fccfd59aa1ebde96930d4be286a30355f8cf5d90ddb3e5eedcf714", + "python/openswmm/engine/_2d.pxd": "828bf46e8a9b72dcfc1c32cdd7eddc81f3bff2a917d186ad61fba7dba118b76e", + "python/openswmm/engine/_2d.pyi": "262ccf72b65348ff2af4a2d2d441a3a0e82469acd93659230979fc7b5668ac80", + "python/openswmm/engine/_2d.pyx": "a57529fafc4d83404112a6f185a02d377a8e3af189969a18e5a3e72280a7896a", + "python/openswmm/engine/__init__.pxd": "15e77b6f88304122823ffcdaaa59cda9e78ccf21ddd744a96f6ddc0b44044b76", + "python/openswmm/engine/__init__.py": "69373d9856e082350fb896a6a9713d9a41996d9bf85b95973ee136553d4d51d2", + "python/openswmm/engine/__init__.pyi": "59e41d9a84e2b63114fb90a9e0b2a4ad1041fc720637e9180fcfe1bf1306f908", + "python/openswmm/engine/_access.py": "f236d12f627cfccf5c4fcacf260affd6d51570a702ded62a4495d3aa4857e741", + "python/openswmm/engine/_climate.pyi": "36218586d6d88695c08db76b748449cf1eafc955b02c37c6a7dbb103afbde966", + "python/openswmm/engine/_climate.pyx": "93dde91cd1ae8ec54a2eeca8c044f3ee7c5342d50a3b6463ef54fa31f8177304", + "python/openswmm/engine/_common.pxd": "b5f5363dd9f665f75c879a486a6a6d5771500f944aa857857ff08eb5eaa44c02", + "python/openswmm/engine/_controls.pyi": "72f315dab3f59d4fc623d06471b7aa8d376bcf21e56fde8ab14507e04cf12843", + "python/openswmm/engine/_controls.pyx": "0c4bff6e72680a62a0990e7cbb0b546e4be4f1b2350725ee11f14c7bae9c0600", + "python/openswmm/engine/_dates.py": "fff1d2eafa776616c2773d67caab6d63f41cccafbce9bd983d3331fe3f85d655", + "python/openswmm/engine/_dates.pyi": "da0aa2d7be8bc29223ba4a94ce2a5df9740a1cdf10ab47430615d370f3c28d31", + "python/openswmm/engine/_datetime.pyi": "cb252791b765467e8cfd01e7d502321f5b3481c8de1b3fb5f5d776d669c6ad5d", + "python/openswmm/engine/_datetime.pyx": "ae2bce8e1ead01d4a539d703f837cbf979ea71060ad0eb40414a4111248cb588", + "python/openswmm/engine/_edit.pyi": "8f6f05235b0a85fbb7797f5ba8da31f6de35f4530eb178f42b3397771b206cd8", + "python/openswmm/engine/_edit.pyx": "557de4283f7c9cdc06f73f7d734f3279de088104132fb199913de531dd44ec0f", + "python/openswmm/engine/_enums.py": "2769ebeede9aecef37b67687beb6a56c716dede0f5745fbdec4a5f9d4d259407", + "python/openswmm/engine/_enums.pyi": "750aa30dd2e46dd9fc66fb2584e63b294fabc959c6f6fb4060dd2ad18fea0b32", + "python/openswmm/engine/_exceptions.py": "3be9371c3a77101a248d5ea7fec064bc5894ba3d6582d77c7095159c8e2ca37e", + "python/openswmm/engine/_exceptions.pyi": "cf365bd80fc95cfe51b4b881cb7e47e47e5e2a20298693cc64a22d993e897d27", + "python/openswmm/engine/_forcing.pyi": "d3b2ecc8433181259211b4fa1b92cbf46d55e9147394b86487f5e9afae5c056b", + "python/openswmm/engine/_forcing.pyx": "d28049451d99e69bd4091bf3da96f066bb9fb18afe1389e746711996c41597eb", + "python/openswmm/engine/_gages.pyi": "c5166e72835b280b65abbb3b84e437ea3e50651e2609738a2bf281b08a1d8ad0", + "python/openswmm/engine/_gages.pyx": "8f3ac58547b72800aaaa65905909b7b54a0edc92fd38e815de9323a0a6af723e", + "python/openswmm/engine/_geometry.py": "380ed55ffc17fa84276a7415518e97ba88e5e463cae41ccc1f0131d629022549", + "python/openswmm/engine/_geopackage.pyi": "7d3e452b7c181e76c62223363f0663f53076352c51625a3a9cdd453d4e70efe8", + "python/openswmm/engine/_geopackage.pyx": "4f1b5b24cd3fabd6d6fc8f287359658752e88ac6eb4e5c0bd8d447164dfcd057", + "python/openswmm/engine/_groundwater.pyi": "426bfa4556728d76ee2b2061aa3c49ee5c1852f8135962f5bc2b4ece16d0fcda", + "python/openswmm/engine/_groundwater.pyx": "c1b69b9d47344577386edd924487bc6c3626b730ab61e5c3cd37d2e8d773dc5d", + "python/openswmm/engine/_gw_transport.pyi": "6fc2c018fe351b6578379d2d48c237a98281348dce7b15d02606b9cdae0725c8", + "python/openswmm/engine/_gw_transport.pyx": "0131e523e6ab98c45d3ed099e37795c41cfd86c37c8aec5edd76d6913cad187d", + "python/openswmm/engine/_heat.pyi": "9994202dff8f4e0a532bdedcdbd8fd1bda59dedcd4a818c9d25eec15caddb698", + "python/openswmm/engine/_heat.pyx": "d38d2aa9aaf91e1d65bd02a0e26eaa851e6792c38f53e2c78c6e9df5c804f5e3", + "python/openswmm/engine/_hotstart.pyi": "366c0d948bdf70f27e920cbb1f642fc9434b0c6d804c323f4db914f7baac262e", + "python/openswmm/engine/_hotstart.pyx": "81152602030c9410a33bb659a1631ac4e000b19ed0eb651e0362cd8f0764d9d8", + "python/openswmm/engine/_inflows.pyi": "48a7e6020ba067bd75f5a16c35f494a32f39b9455a04d6f1e1cb2e5a5aa8c1bd", + "python/openswmm/engine/_inflows.pyx": "73cab4cacb2ba3991db46f09f11ad4dfdb88054a13a688ba90fe2f4000841017", + "python/openswmm/engine/_infrastructure.pyi": "824ca5a5f17d826389ba79bbf89c13531eda7b88e8df3ccde8c00d871234929e", + "python/openswmm/engine/_infrastructure.pyx": "cff580fc4df7ffd0ba4ee9041f0ca1111c856044c1f45028c8e7ddb16f39aac6", + "python/openswmm/engine/_initial_quality.pyi": "0260be4ed72f25cadfe1847fed8327b185caa1dfac1954f1d6b0c6d82dae34ea", + "python/openswmm/engine/_initial_quality.pyx": "bd0e79798c5cee786e44d7632e8a39efde4b45ee7da2182e42d6beafafa29440", + "python/openswmm/engine/_links.pyi": "0b1a90750566c5928ce4bc1a6d931d3be5fe3e5fdff616ca4cdcb647d28c6b44", + "python/openswmm/engine/_links.pyx": "020b315f33f062556000dd231fe2ba483905058a41e236fc51e03e861a5e643d", + "python/openswmm/engine/_massbalance.pyi": "3cf3c3a056ef77bfdfcd736e57273b9489aa8fd9381c3167d873147bd8853406", + "python/openswmm/engine/_massbalance.pyx": "79d370f31cb9c671ced97e4e632b30dade40e8b418b634f7d96828a1f5594035", + "python/openswmm/engine/_model.pyi": "b4d8a1d0d4852252f204b0f43758ef801ad1ab543ab9162b8c39e6d12e2a9a2b", + "python/openswmm/engine/_model.pyx": "009e5dbaa19f33f029c2883afc23093de5eeef5e2db5c56b9f777ebb94344625", + "python/openswmm/engine/_nodes.pyi": "86f32c19b887376b0b63c2d3e1fe20541e9530a9a7824d1022b1f9f356ba6694", + "python/openswmm/engine/_nodes.pyx": "fa409cd3d24713876135bf95ff86b587f3d23f82b302e68c719d3bde6e6c6e0a", + "python/openswmm/engine/_output_reader.pyi": "881a7d9bee717105768d4540c3e985b73d8d031d6f66069677596d000c73311b", + "python/openswmm/engine/_output_reader.pyx": "aaf4c2ad900154fc258777b055849f99b4003aabc75644a908e133dac1515c37", + "python/openswmm/engine/_pollutants.pyi": "3709a7b81bfafbddd77c36c86510c82b05b987d47e74d34592f83c374dfed9d0", + "python/openswmm/engine/_pollutants.pyx": "123eeba3e36cfe3d5afd6b8f2e07aae7368426f5e66087286e8a6d9809e720a2", + "python/openswmm/engine/_process_components.pyi": "e2690929c68113ca9c6c408bf06157e0b6148e11cf78e1b6f8a2f180b18ca033", + "python/openswmm/engine/_process_components.pyx": "ecf32250113dd5cd3c2881d01445514f80de54bdcddbeaab391e36de05c4b159", + "python/openswmm/engine/_quality.pyi": "ee1a785b33f2c7797fff2d0bf7b04b929b26e53e0eeac269d63af886e3465f12", + "python/openswmm/engine/_quality.pyx": "a499fada754e6726dc608e40f1cb43f0e6e596902d1eb356f23811dec4e4f218", + "python/openswmm/engine/_reactions.pyi": "34399884a50f2fc387909557906f5e5e4560c12cdb40779b7cb2f54c389e4f37", + "python/openswmm/engine/_reactions.pyx": "8b3394209e7db162f728f5404f56d911f0ab1e1ff95e22679b9d0466c128d80a", + "python/openswmm/engine/_report.py": "3f324cebe4f80661d621b98962dc4bee9d04b90251d5e1cf72edd780b504d52b", + "python/openswmm/engine/_solver.pxd": "fea0a6d6fb28b25fa733296a01a58e2e085213e98573b98d6a40051c9d1f83ef", + "python/openswmm/engine/_solver.pyi": "91a266838c6440563f58b023c7d2ad150808a5091155df9ce314821c1454ee74", + "python/openswmm/engine/_solver.pyx": "e86778c7dbbb98ed769e77336f8a82d2e0c65fb241c270efa33276e8995d5da5", + "python/openswmm/engine/_spatial.pyi": "1bef90a1db14836d68effbacf36811a3f2e4a04eabfa004f82dd19641053d9f8", + "python/openswmm/engine/_spatial.pyx": "b9063c1321a5b69510198e1a80179b10a30cffd6ab543b06ce68c84ab9059271", + "python/openswmm/engine/_statistics.pyi": "3e36d9625dbcb445b51ac9897b3aa840672a1c66cd4a79ddc173ca0b05a2d8bb", + "python/openswmm/engine/_statistics.pyx": "e6596e77d584692b707ce08e5b262c454de27f0185c316facbd01ced0aa48cdd", + "python/openswmm/engine/_subcatchments.pyi": "a892f6472bf9e76e2d026c46e9675ff5cdf0816870a69fae3a9ab14bb45325ab", + "python/openswmm/engine/_subcatchments.pyx": "d848f7afa65fb1dc3d11b852a494f4978a94ca6682b9b0b41d77b48ab1c3ad39", + "python/openswmm/engine/_surface_quality.pyi": "addb38fae9a31d4611a04821d14dad5c874d4ad011fbce637e1d57214c863c83", + "python/openswmm/engine/_surface_quality.pyx": "95c4e0ee1d5677fbf184def777d04ae6ef6351496f1d12fa7ee5b7ef21b8a069", + "python/openswmm/engine/_tables.pyi": "adf38dcc669e4c5dd11bbe6996e32e0a95112eb6b9d9d130e877957de270c657", + "python/openswmm/engine/_tables.pyx": "227172992226816f875fa4e75f6a8e482be301cb7e70066cc3cd7af71224e379", + "python/openswmm/engine/_transport.pyi": "28dc438f04899f2476f29c32e95c4e62613bfea0ab9c3e28b2633fcd0171d61f", + "python/openswmm/engine/_transport.pyx": "b7d3882d8744532482b65ce54d0218e6905c82e8c27351a622ae2fceb528293d", + "python/openswmm/engine/_water_age.pyi": "3849b79d63db0dc4ad43b3af0c38ffc4d448860d6d7ab8642bbad28d72f87c14", + "python/openswmm/engine/_water_age.pyx": "11dcea5943bc3be1cecf02928e67c8066ed45b6e76153d9db30c4b36c5c733c2", + "python/openswmm/engine/_xsect.pyi": "2193922ca40d318f8d58ae7d274fe4a8b130bb341818b52beb241d9438a76507", + "python/openswmm/engine/_xsect.pyx": "82d626833df95cb7d3561190721f67a25085a918682055a17f306567ddb32bb2", + "python/openswmm/legacy/CMakeLists.txt": "5e907830f0a2281fbec3e9de86cf992fb1d4c51982a93cfa00271e541ed41441", + "python/openswmm/legacy/__init__.py": "774096ffcb03cd1931738e08d948798f1a4135212dd816d1c35bd167522243a8", + "python/openswmm/legacy/engine/CMakeLists.txt": "52064338884176e1864647f31cd2e63b552ce63438caf8def4723341f2d30fd9", + "python/openswmm/legacy/engine/__init__.pxd": "73aeafe1f91337e94fe80d5fbbc9e9aba34c6d56346054841b6ca091c9317346", + "python/openswmm/legacy/engine/__init__.py": "8e1b72ae62ad97d4c0fe41041503ff531ed0a9d3f116c3a59fcb1cab99883b27", + "python/openswmm/legacy/engine/_forcing_log.py": "d514ac58547b155b369cbd4b1f3a1c498c3879c8480dee987891801f1825452e", + "python/openswmm/legacy/engine/_links.py": "59287f6354b0302f94864ec8c8e5b5f3b39942190afffbad6de26b68779c61d0", + "python/openswmm/legacy/engine/_nodes.py": "cd8f2745076badae83193f304184e61fafbb36ddcdae6c41dd0165b69f9a0241", + "python/openswmm/legacy/engine/_raingages.py": "5df0581a5324ec19b015e31fd6e5f8bc7fd4e5abdf1bf7ae38d1dc1a93b846f6", + "python/openswmm/legacy/engine/_solver.pyi": "a4d1ffc15fa0b98b6934639387ff19627215967c0c422d7a22b7554c6772087d", + "python/openswmm/legacy/engine/_solver.pyx": "5d090cae9bea9a0d6f48b43d2c0dd1f2b89194335cbeb21b9d6eb76722cbefba", + "python/openswmm/legacy/engine/_subcatchments.py": "7e1d4869b89146fa2c2d7cde6a5f8a464066a6d632a1879b4f27ef2ab4cae82c", + "python/openswmm/legacy/engine/_system.py": "c4c12edbd3f9b5106f4d146c15d73fd6e7d5fae45b5b4a9b0887ae0c50a924d1", + "python/openswmm/legacy/engine/solver.pxd": "8b5507162c632dfd27e18170f60e84b4ba1d05d2fbe789e583c20177a88a4630", + "python/openswmm/legacy/output/CMakeLists.txt": "1f67cb167ffd5b23492be9ce9ebc1e8ed1b294913f110761e07974613f9472a0", + "python/openswmm/legacy/output/__init__.pxd": "3fa69206162dab8fcb723c813b98cb833c5ebfbba1195583b3f5f78e9ff448ff", + "python/openswmm/legacy/output/__init__.py": "21916cc3ef500ce4e6f85a17adf30ef3bee9c5c4b75655e9198f43014beed3f9", + "python/openswmm/legacy/output/_output.pyi": "2d37273d1a019bd5a1be686a6f66c25f5019e983f1f81236c3af4fac834691b7", + "python/openswmm/legacy/output/_output.pyx": "1f3b5ac42553aa7e8b69c53f255bcc21f0d3e4f25b478128906edab8e091562b", + "python/openswmm/legacy/output/output.pxd": "656538d69f0324cb44da0e6d4d7d0d1b6323538b3f08ef5647b0ae578feadf25", + "python/openswmm/output/CMakeLists.txt": "77a7ec1714557e8bfc5a6a6dde358826b30aa655c9b75b12a2ac080446194b94", + "python/openswmm/output/__init__.pxd": "3fa69206162dab8fcb723c813b98cb833c5ebfbba1195583b3f5f78e9ff448ff", + "python/openswmm/output/__init__.py": "65f5589276ef8e920a6ef5db80f3c00693f18575cf2c3c67c76046f7aca7f9b3", + "python/openswmm/output/_output.pyi": "c2dd394b5fa230e1b37beb71a20c02c204eb18dc4b97ffa0e1103aab1e91cfd7", + "python/openswmm/output/_output.pyx": "dab595b344eda4dbf25045c15cb9ac3883baaec7dadd7ccbaf0b9ec13f5262b0", + "python/openswmm/output/output.pxd": "656538d69f0324cb44da0e6d4d7d0d1b6323538b3f08ef5647b0ae578feadf25", + "python/openswmm/py.typed": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "python/openswmm/solver/CMakeLists.txt": "6e41f36d62705c846288dd9b0e94e640366521d540c88a455df388266eb38270", + "python/openswmm/solver/__init__.pxd": "73aeafe1f91337e94fe80d5fbbc9e9aba34c6d56346054841b6ca091c9317346", + "python/openswmm/solver/__init__.py": "441e966d5cd5d2e9be6b760f289afa3372b6fbb8b11153a7e7828b5d005df4b4", + "python/openswmm/solver/_solver.pyi": "af426d21df5267dcad01537fb14910373ac1571e13f0342635998ea014bfd074", + "python/openswmm/solver/_solver.pyx": "9c15079e96645b3aa8f28dc5c502cfea0df4533a0eb6a67d72108ef37889cd88", + "python/openswmm/solver/solver.pxd": "bb641331ca9c9e56f6f52a5df37fba62bd3e236d4521e71f726f8adf76dc7ea8", + "python/pyproject.toml": "d7beac51e580933095ad7aca27be8977a3a9fe93811a4922a24500e2d2c440f4", + "python/scripts/api_drift_audit.py": "8be09dc46e57aa06c6181cbce45ffe8dbfd74f057b2b4394a1b17a374f772037", + "python/scripts/cibw_repair_windows.py": "5edc8787d374a3ffacf7e4c03a3e377f986ab11ec847551fbb82700c73e0e091", + "python/scripts/stage_python_sources.py": "68b79b5fe367c225ca6f36585717daf28b326ee98ef6741136ae02f8ce8eb059", + "python/scripts/test_staged_package.py": "204378ad3b1e8b089fa9ec4564f57da5c8e033e272a8db6f72c818e894e0891f", + "python/tests/__init__.py": "835bc84fd621a7633a38980b473afaa01da9b23b7f577649c31f4cbcb03c91e6", + "python/tests/_paths.py": "1bc96c79fc2cdb8f15ffba1b69bc7cf846a4c33b123490ecbee5d32baffb36e6", + "python/tests/data/__init__.py": "b226b242b47656004b8c0e8e9825273d1fdc7ba1034e67c89147afb235918014", + "python/tests/data/output/__init__.py": "57ca69e1cc4ea9b4cec8546bd84462b56ac2bea4d5f30eeb7f4970ff1a2407b7", + "python/tests/data/solver/__init__.py": "9d9ceddc89d0a3f4b48a0ddd8cbc6cd40cf022114c1de0d825bfab72a050f411", + "python/tests/engine/__init__.py": "79198af1bcffec8d8a6c67254cd11f48ce5efff34232ff3dbece39cc4e60a3d9", + "python/tests/engine/_groundwater_cases.py": "7f9bd03065f2d0fa9770fc55425dea3dc0d91d5aff6849bf2708c49f022b9e23", + "python/tests/engine/_solver_cases.py": "9b6cb63b1ae7e79f4f531a5ec93bef86b6f956f74140a9f77c1ba510b5d9e823", + "python/tests/engine/output/probe_case_sensitivity.py": "06a31ce047b9b12c02b668a1c5fbf30291ff8f08cf4ed56ef17a3eeb9cfa1d83", + "python/tests/engine/output/probe_lowercase_ingest.py": "4dc42841be34aa968afc927b5ed964ecc7628f53544cb469863296a7cbadd5b0", + "python/tests/engine/test_2d_evap.py": "0c273f49efc481c2ff8f94318afb9f3771d1e78e203c3781a45901390dd352f6", + "python/tests/engine/test_2d_ts_stage.py": "c5281c0ce21785d5351d1bbbaf3b5229a9a89f68064e3a201f1bc24d352051c6", + "python/tests/engine/test_aquifers_pythonic.py": "3461da6360582148ded8189fb9508aaa1246c8ad8530b91fde6364d5275e3007", + "python/tests/engine/test_binding_completion.py": "96ffdeacc2f142636370de23768fb026b2eb9f571d957b8092ff1c6e7e71f206", + "python/tests/engine/test_callbacks_and_xsect.py": "37aeb458ec96332c2057a8f3ac8a91cc7adf75d5695ef0c9684ee8b348096998", + "python/tests/engine/test_climate_pythonic.py": "aef93880b63a68b8c5f91cb30720a423d1cf2f57e03bfdf57884e5d295392a31", + "python/tests/engine/test_common.py": "2d004e2c30e43ae262a06cdb606744f7a1ff157024a0b5764819c6841895e559", + "python/tests/engine/test_concurrent_simulation.py": "54f1a70cc5fd476697bcc0ee5595f6caf0d008cb142b53290f081a76d31b2e9d", + "python/tests/engine/test_controls_advancement.py": "fa7136dbb7d89261fdc4cd61b43bb13afa384c4167b37426cb80fc3a709047ed", + "python/tests/engine/test_controls_inflows.py": "99bc5ebec635b3945e74c122ff2423d1b66bbcef255c88951ccb9394eefc3364", + "python/tests/engine/test_controls_references.py": "1ae5a6cbf74d51f95048231f8eb643061e631a1f055d7ba29e60f9fbbc2f75f7", + "python/tests/engine/test_edit.py": "1bc23f2b655421217876dea11447a561ce70acfbe5fa0e7aeecd4c6b4f10ecb1", + "python/tests/engine/test_edit_extended.py": "00cda7ac735d667cabdf11e79891c0c338377aee793af2fe4e68c76eac07deb9", + "python/tests/engine/test_editor_roundtrip.py": "a0af5887f9a5c7c722025c97f3ac0949ab8b776621c92de10afb543cbe57cd27", + "python/tests/engine/test_enums.py": "ba2f6ca089d56e3f089773b0a6e270f271645ca38413c2b02d63f75613984d36", + "python/tests/engine/test_files_iface_gaps.py": "9b97c0a5c6b56004b44379a4777a89cdab41b3088cd16ae52145619a8c844c73", + "python/tests/engine/test_forcing_climate.py": "284cb3eb20545086d725c033238400d5906b686fc9b092da9328237f8246a8c0", + "python/tests/engine/test_forcing_climate_evap.py": "ba518882f0233a871ed24382b9c8ccf3dbd4a557a897f41d955bdb9a4f3f959d", + "python/tests/engine/test_forcing_pet.py": "9c9e908d90460f078c66f1b9d2c885e8376116b679937216e5621c9b53e17500", + "python/tests/engine/test_forcing_quality.py": "4edc3b4cfbb9c63ddedb7b9f0cb3defcc1bf326891ff736d519b053dcf17a675", + "python/tests/engine/test_forcing_snow.py": "ec39fa3cf98816f56b15666e77e3922e61145e31b68c2280d000a700ef7816d1", + "python/tests/engine/test_gages_pythonic.py": "fb8cc4c925672d1659a1a119d006d373d0630c97e68e42553dcf84a5bc7f3f6e", + "python/tests/engine/test_geopackage.py": "57395e48df79ba9fef9e059c37474b49e794c93281519b59a9c68dcf2cae51fa", + "python/tests/engine/test_geopackage_export.py": "9f8ad0ebc92efed784f0d0006cd552d7d6b3a817b532e2a2ff168f3aa18effec", + "python/tests/engine/test_groundwater.py": "33706c2273f153643f79b9e7bda02436d4d757e5bf0d4f5f1fd5a655a82c1c3b", + "python/tests/engine/test_heat.py": "56ae02c10a394750ab54e545487277a71173080f1064f9c4e4387b5d57a57434", + "python/tests/engine/test_hotstart_pythonic.py": "68db3049b4f0dc7b4b885943dc9e1c3fbf788ae40c108343432f332164633792", + "python/tests/engine/test_iface_routing.py": "bfa0b122ada9ebd80a5d8b3b5196897b6845b7ad4fe560233a2e4cb147394bcb", + "python/tests/engine/test_initial_quality.py": "2442807916349c1815b865b08818f52719f3b58cc43193d1415cca95b4021608", + "python/tests/engine/test_inlet_api.py": "2e8a1df79418ab170df641afce65c57cea42aa6e7fab7571376e2c95d6e2fa80", + "python/tests/engine/test_integration.py": "2cf54a3d049f00a10a0c453e949a50f50abfe2a0d1c6f6d08900114d6c8e6c1a", + "python/tests/engine/test_lenient_open.py": "402d4e694ac95781efc570b8f509e55dc9d817b208da8618ab95c0d618d95a5f", + "python/tests/engine/test_links_pythonic.py": "3b1b3115ed549ba6d95c966a08bf9147889a0f25282b73fcd67bf3b8ba99ae10", + "python/tests/engine/test_mass_balance_pythonic.py": "4430c6372a87d96d3c90b1c0c1cb38ed5ba727399c4b88a683322e5c14aafbad", + "python/tests/engine/test_metadata_bindings.py": "9db13d4c09816bed4e4a3e8ecbf8340d12bd91393c94e65d74e39a9170bc7fdd", + "python/tests/engine/test_model_builder.py": "69191f5ee9e869571acf4bbc901a2426021c56796a22f890eb4d02a714d7216f", + "python/tests/engine/test_model_file_paths.py": "3ff1458bccf2562ee06dd9bf7e45d45980ef2b353738775c366747a0e733799e", + "python/tests/engine/test_native_safety.py": "4d7672d9dbc0200dcf957a3ebe65fd8aba364bbb6cdaf0546bcfa87f5b716f0d", + "python/tests/engine/test_new_binding_stubs.py": "e1006be0814ad0762011ffc933f09f91a23afa1bcff8334c50a0fa97af477ed2", + "python/tests/engine/test_new_domain_tables.py": "ccc2802b02851b25a2db33968fbae03875515c10be57bbd85fe1300918e71c1a", + "python/tests/engine/test_nodes_pythonic.py": "858d673c928f95c7dc8c7a064ada74c21490955912e80ea5640dfbb75ab17bbb", + "python/tests/engine/test_opened_state_editing.py": "bb502442736b1672cdc266e6ef8bfe5b887e4d717217069f7c8976dbf0a1e6ad", + "python/tests/engine/test_optional_imports.py": "2388d5f24ef754ac43d7138829a2e81d33b94c929a8093fc9f81a966782d8069", + "python/tests/engine/test_output_reader_pythonic.py": "712eb8afee71e80d5bdbd9fbdd9ad4a63fb36ddd2a30a06af03f774994fe2c5b", + "python/tests/engine/test_output_species_ids.py": "08e2d352738f155cd602709206bd725226ff6cd1ded69823fd25ee622e4f690e", + "python/tests/engine/test_p7_pythonic.py": "f193c89e1eb10149d722bc24a4892244273bec098d70819a8aa56d2299214cd7", + "python/tests/engine/test_p8_pythonic.py": "74de8252290823caf44bb80c694b3e16f04c8235b2a588c50015b6a74855ddce", + "python/tests/engine/test_param_runtime.py": "a9c46b2b9961d25521f8729c83cbeb28ba5bbd575e3fe74645fc43fd51d4f731", + "python/tests/engine/test_process_components.py": "6b74257a9ee9179e51d1759ee6dccdf697e18e57c82636028739c8bbc0270ddc", + "python/tests/engine/test_quality_editing.py": "8664f40275cc62fb620c323d12b1c96793db74d5a5dc98def3656b58b5590677", + "python/tests/engine/test_rdii_advancement.py": "480571987fdeb1f56261e2872aa4c5161b0dafda1d029dc650b00adca53fc793", + "python/tests/engine/test_reactions.py": "9f0ba8d2e59e00a3732b4a561e4fc69f6cab44bd9ac4bdfb54e097713f1f7ccb", + "python/tests/engine/test_runoff_interface.py": "b12485337069adaa958107daf96d170b8501f0cf753e484fa35ceccb33a7a663", + "python/tests/engine/test_snowpacks_pythonic.py": "0238914e676884220c9dcaa58c32678cbaaee701e0d827a94b79c66b7f8afaf9", + "python/tests/engine/test_solver.py": "6e9a9b6557c27b256294fa22db3797b5e586fb34e458f297e7325072ea14a48b", + "python/tests/engine/test_solver_introspection.py": "9593b46c5665c110dc758e717999f1b52915b50ffc83cec1f9b42349b3bedeac", + "python/tests/engine/test_solver_pythonic.py": "b4b791632ee72c32351dcae75218616b699aabce65cb950f076bb459c650df14", + "python/tests/engine/test_solver_units.py": "02fbc544d946be904efcf941737a319c9e768e9164485664631891dab4e629fe", + "python/tests/engine/test_state_injection.py": "7bf52e381e70ff2d57f2cc0321685a2fa343b1063297ebc03e47b3526a67a6d7", + "python/tests/engine/test_statistics_pythonic.py": "12772f691a687b3a111b7cfb59f501d77d866c461dde901eac5e47575dae86df", + "python/tests/engine/test_storage_shapes_pythonic.py": "e49001b264e97c6df3eb3ac6f0ac2f90a473f42f82b5d72a95dbe7571b647d43", + "python/tests/engine/test_streets_params.py": "9412ca0057c31ccecc1f7d64ad9c53ac856ccd63be90a9a5268dea9cfffb86a8", + "python/tests/engine/test_subcatchments_pythonic.py": "ae1c07d9292b6a84cb2dca52585403a00599ec915166cbc1ac0051d43f0c52f2", + "python/tests/engine/test_surface2d_run_stats.py": "70c78e33dd51c5de5d11f4d22c89cff775d4f40a073bf57f01ab60fab4f34735", + "python/tests/engine/test_surface2d_view.py": "0531d00450cd47113c3a6a69b827c26d2a82201fe75caaab580b765bcbc25b25", + "python/tests/engine/test_transects_pythonic.py": "fbbc240ef33568abad244901d2ed52367e5678674bf4908d49c523120009c5d4", + "python/tests/engine/test_transport.py": "1a68cb80dc058e69af760fe5702a4a0bbcb52cd6eb8e928a29cde0cdee48a36c", + "python/tests/engine/test_userflags_schema.py": "fb5e178f8762b1868db3344a1f8402ec30df0c30420f6f03bdf7b162652bc64a", + "python/tests/engine/test_water_age.py": "59e6c2fda88387fb8b67751ef466a3c9a35a0647b14db6d67208f4129c38d4d8", + "python/tests/engine/test_workflow.py": "cc734534456af9818416b0db0e6ade8234c87d78e32225fb3ac2f5d92ae5a853", + "python/tests/engine/test_xsect_geometry.py": "112e5b5f8a410cdc654c5c5b5282e99205d94765af9f4a77c5db807c8aa1683b", + "python/tests/legacy/__init__.py": "7958978068e4eb11b871d9b7062ddc0f1d9993ae50eccf8f968ef252d8fdc5e5", + "python/tests/legacy/test_output.py": "03b239c05ccd8a52ffdf16b48d73df09be62c9b6861ae02026e9f5ac2fa3bd34", + "python/tests/legacy/test_param_runtime.py": "6ac859d67dc20286a5864562d44593e3432df636ee64a48becd0a5cf89637e19", + "python/tests/legacy/test_phase1_enum_coverage.py": "ee4efe8f3e4d11da4509e3914f1f30810d0ca57ee751b5ebaa7f31532895c503", + "python/tests/legacy/test_phase2_statistics.py": "41415bd453b1f99d6c30b345cbac1551f45573473cbe11dd5f230e27fbd79d14", + "python/tests/legacy/test_phase3_oop_wrappers.py": "ae169cb9abbc7a786f68d8cebdb9404074e3d4768d1a7489f0eb2ff7d12bcbe7", + "python/tests/legacy/test_quality_sources.py": "dcd780f0e78995a97d180f43e7d0ec8bf48a50cff39cab30ce39925d43da1f6c", + "python/tests/legacy/test_snow_api.py": "2df824c149d99ecdf34284ed44deee36025f8dc43ced2972d5db0bfeb146e27c", + "python/tests/legacy/test_solver.py": "fb534492e02e1ae1865ec5ea21cae1fb9ac54fa5bf4c72a5fc9030657015fa20", + "python/tests/legacy/test_subcatch_api_pet.py": "aea501aaa1010656d7134ef6a20b1d4bbb6d2af4583642c517d68716249192c0", + "python/tests/legacy/test_system_api_climate.py": "85a973332af65f58ee16dac6eec06e824b3dacbf930336cafcebbd1855b79212", + "python/tests/minimal_features.py": "18442a47a0f581fa1d6f4ba5d846bf0e9cec19679b3c7b6d4c37b5d49d097573", + "python/tests/test_2d_marcher_options.py": "226bdb69a28ddd190aaf09a08d8706867b0ef804418c88c0e724b8259b264741", + "python/tests/test_2d_triangle_coupling.py": "63858ee2f2c080d67163eb74980807a8df634d2329258448d218adde363b35f2", + "python/tests/test_2d_triangle_initial_state.py": "1a7275edc6762b355100c2f41b586eba5ab23dd2c543da1d75ec5bc22be95b2f", + "python/tests/test_api_coverage.py": "5fa2e8ec1fd2a8242efdcf6cea2ec8d06c5923b5f8e833cd4040197a177c9618", + "python/tests/test_enum_drift.py": "444b6659001e23e2c89bf8765d97344aea6037258fe5abd52112a143f824c5f7", + "python/tests/test_fv_routing_options.py": "537a688d7f34dc71ce5a416f381bbf012ffdeeffdceb5a9d5fc28d2925efa5c7", + "python/tests/test_pet_parity.py": "a0ef99c30b415c373f1229899e383eb7af18f489b91ab344e474d2095d3de1a4", + "python/tests/test_solver_pxd_attrs.py": "c83f6fd1fa4e75531c0de8e5a559d7fc522b6ff3b40352d5ccaaa44d2f087c11", + "python/tests/typing/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "python/tests/typing/test_surface.py": "4b0f0b70836ab3be094baaf8ab3a647324349dbcf6636b47385d328fa1e51300", + "src/engine/core/SWMMEngine.cpp": "ee41044b5860a503495d83e315cfd3520ecaac04db062b591293fd53a71efcbc", + "src/engine/plugins/DefaultReportPlugin.cpp": "b42820eacb23f3ed0364930cfa287e2169c09ec60bb153f3c48dd9987d7b6745" + }, + "logs": { + "wheel_tests": "Python: /Users/calebbuahin/miniforge3/envs/openswmm/bin/python\nBindings: /private/tmp/openswmm-conda-wheel-installed/openswmm/engine/_solver.cpython-312-darwin.so\n============================= test session starts ==============================\nplatform darwin -- Python 3.12.13, pytest-9.0.2, pluggy-1.6.0\nrootdir: /Users/calebbuahin/Documents/Projects/cbuahin_github/openswmm.engine/python\nconfigfile: pyproject.toml\nplugins: asyncio-1.3.0, anyio-4.13.0\nasyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function\ncollected 1221 items\n\npython/tests/engine/test_2d_evap.py .... [ 0%]\npython/tests/engine/test_2d_ts_stage.py .... [ 0%]\npython/tests/engine/test_aquifers_pythonic.py ......... [ 1%]\npython/tests/engine/test_binding_completion.py ......... [ 2%]\npython/tests/engine/test_callbacks_and_xsect.py ....... [ 2%]\npython/tests/engine/test_climate_pythonic.py .... [ 3%]\npython/tests/engine/test_common.py ................ [ 4%]\npython/tests/engine/test_concurrent_simulation.py ... [ 4%]\npython/tests/engine/test_controls_advancement.py ...... [ 5%]\npython/tests/engine/test_controls_inflows.py .... [ 5%]\npython/tests/engine/test_controls_references.py ........ [ 6%]\npython/tests/engine/test_edit.py ....................................... [ 9%]\n........ [ 9%]\npython/tests/engine/test_edit_extended.py ......... [ 10%]\npython/tests/engine/test_editor_roundtrip.py ...... [ 11%]\npython/tests/engine/test_enums.py ...................................... [ 14%]\n...... [ 14%]\npython/tests/engine/test_files_iface_gaps.py ... [ 14%]\npython/tests/engine/test_forcing_climate.py ........... [ 15%]\npython/tests/engine/test_forcing_climate_evap.py .... [ 16%]\npython/tests/engine/test_forcing_pet.py ............. [ 17%]\npython/tests/engine/test_forcing_quality.py ..... [ 17%]\npython/tests/engine/test_forcing_snow.py ....... [ 18%]\npython/tests/engine/test_gages_pythonic.py ........................ [ 20%]\npython/tests/engine/test_geopackage.py .......................... [ 22%]\npython/tests/engine/test_geopackage_export.py .... [ 22%]\npython/tests/engine/test_groundwater.py ... [ 22%]\npython/tests/engine/test_heat.py ....................................... [ 26%]\n... [ 26%]\npython/tests/engine/test_hotstart_pythonic.py .......... [ 27%]\npython/tests/engine/test_iface_routing.py ... [ 27%]\npython/tests/engine/test_initial_quality.py .................. [ 28%]\npython/tests/engine/test_inlet_api.py .... [ 29%]\npython/tests/engine/test_integration.py ... [ 29%]\npython/tests/engine/test_lenient_open.py ..... [ 29%]\npython/tests/engine/test_links_pythonic.py ............................. [ 32%]\n. [ 32%]\npython/tests/engine/test_mass_balance_pythonic.py ...... [ 32%]\npython/tests/engine/test_metadata_bindings.py ...... [ 33%]\npython/tests/engine/test_model_builder.py .............................. [ 35%]\n... [ 36%]\npython/tests/engine/test_model_file_paths.py ... [ 36%]\npython/tests/engine/test_native_safety.py ......... [ 37%]\npython/tests/engine/test_new_binding_stubs.py .... [ 37%]\npython/tests/engine/test_new_domain_tables.py ... [ 37%]\npython/tests/engine/test_nodes_pythonic.py .......................... [ 39%]\npython/tests/engine/test_opened_state_editing.py ........... [ 40%]\npython/tests/engine/test_optional_imports.py ..... [ 41%]\npython/tests/engine/test_output_reader_pythonic.py ..................... [ 42%]\n... [ 42%]\npython/tests/engine/test_output_species_ids.py ... [ 43%]\npython/tests/engine/test_p7_pythonic.py ....................... [ 45%]\npython/tests/engine/test_p8_pythonic.py ............. [ 46%]\npython/tests/engine/test_param_runtime.py ...................... [ 47%]\npython/tests/engine/test_process_components.py ................... [ 49%]\npython/tests/engine/test_quality_editing.py ..... [ 49%]\npython/tests/engine/test_rdii_advancement.py ........................... [ 52%]\n.. [ 52%]\npython/tests/engine/test_reactions.py .................................. [ 55%]\n.................. [ 56%]\npython/tests/engine/test_runoff_interface.py ..... [ 57%]\npython/tests/engine/test_snowpacks_pythonic.py ...... [ 57%]\npython/tests/engine/test_solver.py .......................... [ 59%]\npython/tests/engine/test_solver_introspection.py .... [ 59%]\npython/tests/engine/test_solver_pythonic.py ............................ [ 62%]\n... [ 62%]\npython/tests/engine/test_solver_units.py ..... [ 62%]\npython/tests/engine/test_state_injection.py ...... [ 63%]\npython/tests/engine/test_statistics_pythonic.py .... [ 63%]\npython/tests/engine/test_storage_shapes_pythonic.py ......... [ 64%]\npython/tests/engine/test_streets_params.py . [ 64%]\npython/tests/engine/test_subcatchments_pythonic.py ..................... [ 66%]\n. [ 66%]\npython/tests/engine/test_surface2d_run_stats.py . [ 66%]\npython/tests/engine/test_surface2d_view.py .......... [ 67%]\npython/tests/engine/test_transects_pythonic.py ........ [ 67%]\npython/tests/engine/test_transport.py ..... [ 68%]\npython/tests/engine/test_userflags_schema.py .................... [ 69%]\npython/tests/engine/test_water_age.py .................. [ 71%]\npython/tests/engine/test_workflow.py .... [ 71%]\npython/tests/engine/test_xsect_geometry.py ............................. [ 74%]\n.............................. [ 76%]\npython/tests/legacy/test_output.py ..................................... [ 79%]\n............ [ 80%]\npython/tests/legacy/test_param_runtime.py ................ [ 81%]\npython/tests/legacy/test_phase1_enum_coverage.py ....................... [ 83%]\n......... [ 84%]\npython/tests/legacy/test_phase2_statistics.py .......................... [ 86%]\n........... [ 87%]\npython/tests/legacy/test_phase3_oop_wrappers.py ........................ [ 89%]\n................ [ 90%]\npython/tests/legacy/test_quality_sources.py ......... [ 91%]\npython/tests/legacy/test_snow_api.py .... [ 91%]\npython/tests/legacy/test_solver.py ........................ [ 93%]\npython/tests/legacy/test_subcatch_api_pet.py ........... [ 94%]\npython/tests/legacy/test_system_api_climate.py .... [ 95%]\npython/tests/test_2d_marcher_options.py ..... [ 95%]\npython/tests/test_2d_triangle_coupling.py .... [ 95%]\npython/tests/test_2d_triangle_initial_state.py ....... [ 96%]\npython/tests/test_api_coverage.py ...... [ 96%]\npython/tests/test_enum_drift.py .......................... [ 99%]\npython/tests/test_fv_routing_options.py ........ [ 99%]\npython/tests/test_pet_parity.py .. [ 99%]\npython/tests/test_solver_pxd_attrs.py .. [100%]\n\n======================= 1221 passed in 90.36s (0:01:30) ========================\n", + "minimal_tests": "Python: /Users/calebbuahin/miniforge3/envs/openswmm/bin/python\nBindings: /private/tmp/openswmm-conda-minimal-stage/openswmm/engine/_solver.cpython-312-darwin.so\n============================= test session starts ==============================\nplatform darwin -- Python 3.12.13, pytest-9.0.2, pluggy-1.6.0\nrootdir: /Users/calebbuahin/Documents/Projects/cbuahin_github/openswmm.engine/python\nconfigfile: pyproject.toml\nplugins: asyncio-1.3.0, anyio-4.13.0\nasyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function\ncollected 37 items\n\npython/tests/minimal_features.py . [ 2%]\npython/tests/engine/test_solver_pythonic.py ............................ [ 78%]\n... [ 86%]\npython/tests/engine/test_transport.py ..... [100%]\n\n============================== 37 passed in 1.36s ==============================\n" + } +} diff --git a/docs/analysis/PYTHON_BINDINGS_REVIEW_2026-09-26.md b/docs/analysis/PYTHON_BINDINGS_REVIEW_2026-09-26.md new file mode 100644 index 000000000..b19d00a8b --- /dev/null +++ b/docs/analysis/PYTHON_BINDINGS_REVIEW_2026-09-26.md @@ -0,0 +1,384 @@ +# Python bindings review and implementation strategy + +Date: 2026-09-26. Repository: `openswmm.engine`. + +> Historical review baseline. Implementation and current validation are recorded in [the implementation report](PYTHON_BINDINGS_IMPLEMENTATION_2026-09-26.md). + +## Scope and evidence + +This is a review and implementation plan, not an implementation of the missing bindings. It examines the current working-tree public C headers, modern Cython bindings, Python enums and stubs, tests, packaging, Sphinx sources, and relevant CI workflows. Existing local edits are included in the review. HEAD at evidence capture is `2daef06bec8ac48172010e31e709bc15827853b8`; the companion JSON records hashes of reviewed inputs because a commit alone does not describe this working tree. + +The modern API is the primary scope. The legacy solver/output bindings and compatibility import paths receive a separate maintenance assessment; the plugin SDK is not assumed to require a one-to-one Python wrapper. + +Evidence files: + +- `PYTHON_BINDINGS_REVIEW_2026-09-26_inventory.tsv`: every modern C export and its source-level binding status. +- `PYTHON_BINDINGS_REVIEW_2026-09-26_evidence.json`: counts, missing functions, stub findings, input hashes, and validation results. +- Appendix A below: the complete missing-function inventory grouped by header. + +### Current state + +| Measure | Result | Interpretation | +|---|---:|---| +| Modern public C functions extracted | 1,172 | Exported declarations in the engine headers | +| Functions referenced outside extern blocks in `.pyx` | 1,050 | Source-level reachability signal, not behavioral coverage | +| Entries in existing intentional-exclusion list | 4 | Requires review; one is actually used by a `.pxd` helper | +| Functions with no binding use and no exclusion | **118** | Current failing API-coverage assertion | +| Modern Cython extension modules checked | 29 | All translate and pass generated-C++ syntax checking | +| Engine Python test files / test definitions | 63 / 891 | Inventory only; not a claim that these tests ran or passed | + +The older parity report, `plans/parity/parity_matrix.md`, reports 552 functions and zero Python gaps. It is stale relative to this inventory and should not be used as a release-readiness claim. + +### Validation performed + +| Check | Result | +|---|---| +| `python3 -m unittest discover -s python/tests -p test_api_coverage.py -v` | 5 tests: 4 pass, headline coverage test fails with 118 functions | +| `python3 -m unittest discover -s python/tests -p test_solver_pxd_attrs.py -v` | 2 pass | +| Cython translation of all 29 modern `.pyx` modules | Pass | +| Apple Clang C++20 syntax check of all 29 generated translation units against current public headers, Python 3.13 and NumPy headers | Pass; no linking or runtime execution implied | +| Existing mypy engine check | Pass: 35 source files | +| Existing strict typing smoke file | Pass: 1 source file | +| Sphinx HTML, warnings treated as errors | Pass with external intersphinx inventories disabled | + +Sphinx used the existing Python virtual environment plus `sphinxcontrib-mermaid` installed into a temporary directory. No project dependency configuration was changed. External links/inventories were not validated. A clean native rebuild, full functional suite, installed-wheel checks, Linux/Windows verification, and runtime memory-safety tests remain acceptance work for implementation. + +## Findings, ordered by risk + +### R1 — The 2D view can outlive the engine handle (P0) + +`python/openswmm/engine/_2d.pyx` stores only `void* _engine`, populated from an integer. `Solver.surface2d` constructs it from `self.handle`; `Solver.destroy()` destroys and nulls the solver's own handle without invalidating the retained pointer. In `src/engine/2d/api/Api2D.cpp`, the entry guard checks for null, then dereferences the engine. A retained surface/infiltration view can therefore reach a freed pointer. + +This is a concrete ownership defect identified in source; a crash was not intentionally triggered. Retaining the solver alone is insufficient because explicit `destroy()` must also invalidate access. + +**Action:** use an owner-backed handle accessor with destruction/generation checks, including nested infiltration views. Define the behavior of views across close, reopen, mesh edits, builder-to-solver ownership transfer, and explicit destruction. Deprecate or clearly isolate the raw-pointer constructor. + +**Tests:** retained view after destroy; solver garbage collection; nested view lifetime; repeated close/destroy; reopen with a different mesh; access concurrent with destruction. Run unsafe-path regressions in subprocesses and use sanitizer builds where supported. + +### R2 — CI omits the existing drift guards (P0) + +`python/pyproject.toml` sets the wheel test command to `pytest {package}/tests/engine ... --ignore=.../test_integration.py`. It does not run top-level `test_api_coverage.py`, `test_solver_pxd_attrs.py`, the other top-level tests, or `tests/legacy`. This is why a substantial binding gap can coexist with passing wheel tests. + +The typing workflow watches `.py`, `.pyi`, and selected configuration files, but not `.pyx`, `.pxd`, or public headers. Python/typing workflows target main/develop, whereas documentation also targets the active `swmm6_rel` branch. Align the actual branch/release policy instead of relying on comments about manual triggers. + +**Action:** add an inexpensive source-contract job before wheel builds, and include it as a release dependency. Run legacy/top-level functional tests in explicit jobs. Review and either migrate or document the exclusion of `test_integration.py`. + +**Tests:** prove the guard fails when a declaration is added without a wrapper, a wrapper is removed, an enum member changes, or a stub signature diverges. Comments and extern declarations must not satisfy reachability. + +### R3 — Four entire API domains are absent (P1) + +Groundwater authoring/results, groundwater transport, surface water quality, and ARD transport configuration account for **82 of the 118 gaps**. All exported functions in their four headers are absent from the modern binding call surface. Generic string options and INP editing do not replace these structured APIs. + +Expose these as coherent domain views. Proposed names in this document are design suggestions, not existing Python features. Preserve the distinction between authoring data, runtime state, diagnostics, and supported engine capabilities. + +### R4 — Enum and report drift persists inside already-bound domains (P1) + +Confirmed missing named members include: + +- `RefType`: 15 reference kinds with codes 7–21. `_edit.pyx` already has their string names, so the exported enum disagrees with the editor rather than the whole editor being absent. +- `FilePathRole`: `MESH_2D`, `OUTPUT_2D`, `LID_REPORT` (11–13). +- `RunoffTotal`: initial/final snow storage (7–8). +- `RoutingTotal`: 1D-to-2D coupling outflow (12), link groundwater inflow (13). +- `ForcingType`: four per-element climate channels and link seepage (13–17). + +The name-based audit also finds nine C enum types without a same-named Python enum: `EvapType`, `TempSource`, `WindType`, `HumidityType`, `HumidityVar`, `HeatElemKind`, `InpProfile`, `TransportDispersionMode`, and `UnitSystem`. Some underlying operations already accept integers; classify these as typed-surface decisions, not nine entirely missing behaviors. Review the absent `ForcingMode.NONE` sentinel separately rather than automatically advertising it as a valid setter mode. + +The enum audit ignores public `#define` selectors, including groundwater scope/zone/soil/closure/ledger constants and transport matrix state/domain/class constants. Those need explicit mappings too. + +`_report.py` omits the new snow and coupling/groundwater totals. The `RoutingTotal.FORCING_INFLOW` Python docstring says it is distinct from EXTERNAL, while the C header now describes it as a subset. That distinction matters to consumers assembling water balances. + +**Action:** compare names, numeric values, aliases, sentinels and macro constants against C; synchronize `.py`, `.pyi`, exports, report records, examples and accounting descriptions. Generic integer getters already make some numeric selectors callable; that does not provide named discovery or report completeness. + +### R5 — Runtime members, stubs, and generated reference docs differ (P1) + +Confirmed examples of members present in `.pyx` but absent from their `.pyi` surface: + +| Runtime class | Missing stub members | +|---|---| +| `Pollutants` | `rename` | +| `Landuse` | `rename` | +| `Quality` | `validate_treatment_expression` | +| `Subcatchment` | `coverages`, `loadings`, `rain_scale_factor`, `snow_scale_factor` | + +Some are in current uncommitted implementation changes. They should ship with their companion stubs and documentation. This member-name comparison is a lower-bound finding, not an exhaustive proof of signature/default/type parity. + +There is no `py.typed` marker under `python/openswmm`, and the inspected packaging files do not create one. Source-tree mypy success does not establish that downstream installed-package consumers receive typing support. + +`python/docs/conf.py` imports `.pyi` files in preference to the compiled implementation. Consequently, the claim in `api.rst` that stubs are always synchronized with compiled extensions is not enforced. A successful Sphinx build can publish an incomplete API reference. + +**Action:** add installed-wheel typing checks, marker/package-content checks, and runtime-versus-stub comparisons for public names, signatures, keyword-only parameters, defaults, return types and read-only properties. Treat `.pyx` runtime docstrings as the implementation contract and `.pyi` as its checked typing projection. Publish reference docs from the tested wheel; retain a stub-based fast preview only with parity guards. + +### R6 — Exception, callback, and concurrency contracts need a common design (P1) + +Most modules use `_common.pxd`'s typed exception dispatch; `_2d.pyx` instead raises bare `RuntimeError` with a numeric code. The shared helper uses the generic `swmm_error_message(code)`, losing handle-specific details where those are available. Some older `ModelBuilder` mutations return raw status codes while newer facades raise exceptions. + +Callback trampolines in `_solver.pyx` call user Python code inside `noexcept with gil` functions without capturing exceptions for the initiating Python call. Decide and test propagation at a safe `step`/`stride`/`run` boundary; do not allow a controller failure to be silently lost. Abort timing and whether the current native step can finish must be explicit. + +`step`, `stride`, and bulk operations release the GIL. That supports independent solvers, but does not by itself serialize mutation/destruction of the same solver. Existing concurrency tests concentrate on speedup and concurrent reads, not the whole ownership contract. + +**Action:** define one per-handle operation/reentrancy policy, including callback-safe operations. Add deterministic correctness/progress tests; keep wall-clock speedup thresholds in a performance lane. Preserve older raw-status API behavior until a documented deprecation/migration can be made. + +### R7 — Documentation examples contain lifecycle and mesh drift (P1) + +`guide/initial_quality.rst` mutates initial-quality rows inside `with Solver(...)`. `Solver.__enter__` opens, initializes and starts the run, while the same guide and C API require BUILDING/OPENED for those mutations. That example cannot exercise the advertised editable state. + +The `_model.pyx` module example uses the old numeric step-return convention and tests RUNNING immediately after start. The current binding returns `timedelta`, with STARTED preceding RUNNING. These examples need executable fixtures. + +`guide/index.rst` still introduces the 2D router as a triangular CVODE solver. Current bindings expose the explicit marcher and mixed triangle/quad mesh helpers. `get_edge_geometry_bulk` allocates using `edge_stride` but its docstring still describes a fixed stride of three. The C header explicitly aliases triangle count to cell count; this review does **not** identify that count alias as a buffer-size defect. + +**Action:** fix concepts and executable examples together. Specify lifecycle, physical units, array layout, padding, indexing, ownership, optional capability behavior, and save/reload semantics at each public entry point. + +### R8 — Optional-module and compatibility testing can hide broken installations (P2) + +The package catches any `ImportError` from optional modules and reports `HAS_2D=False` or `HAS_GEOPACKAGE=False`. Several tests skip on import failure. Distinguish an intentionally disabled feature from a wheel that was meant to contain it but has a loader/dependency failure. + +A separate legacy symbol scan finds two solver exports not referenced in the legacy `.pyx`: `swmm_getRunningMassBalErr` and `swmm_setWarningCallback`; all 34 legacy output exports have references. This is not a signature or behavioral parity proof. Decide whether the two omissions are intentional, expose them if required, and keep legacy compatibility tests active. Documentation that calls the legacy engine “verbatim” or says no development happens there should reflect the actual maintained extensions. + +## Work packages for all 118 missing functions + +Names below are proposed Python entry points. Every package includes declarations, wrappers, public imports, type stubs, tests, code documentation, Sphinx reference entries, a guide example and a changelog entry. + +| Package / C header | Missing | Proposed surface | Required behavioral tests and docs | +|---|---:|---|---| +| Groundwater: `openswmm_gw2d.h` | 26 | `solver.groundwater2d`: options, authored rows/node beds, state, species, ledgers | Global/tag/cell precedence; BUILDING/OPENED mutation; automatic node enrollment and exchange flags; state/column shapes; species order; continuity; project-unit authoring versus SI runtime results. New `groundwater2d.rst`. | +| Groundwater transport: `openswmm_gw_transport.h` | 29 | `solver.groundwater_transport`: options, parameters, sorption, sources/species, boundary/initial quality | CRUD/upsert/removal semantics; selectors; sidecars; INP round trips and supported container round trips; runtime rejection of authoring edits; pollutant/age/temperature semantics. New `groundwater_transport.rst`. | +| Surface quality: `openswmm_sq2d.h` | 14 | `solver.surface2d.quality`: coverage, curb lengths, loading, buildup | Row width versus land-use count, precedence, dry/wet cases, mixed-cell meshes, species/cell array order, nonzero buildup response. New `surface_quality.rst`. | +| Transport configuration: `openswmm_transport.h` | 13 | `solver.transport`: dispersion, target dx, conduit overrides, source/boundary rows | OFF/FISCHER/VALUE; zero/default handling; finite/nonnegative parameters; ft²/s versus m²/s; read-only row access; negative extraction rates. New `transport.rst`. | +| 2D diagnostics: `openswmm_2d.h` | 8 | Rainfall/volume arrays, interpolation weights, output-variable metadata | Scalar/bulk agreement, configured rainfall modes, empty arrays, cell order, weights, cumulative versus instantaneous units, output masks/names. Update `2d.rst`. | +| Engine diagnostics: `openswmm_engine.h` | 5 | `thread_info()`, `solver.effective_threads()`, `solver.transport_matrix` | OpenMP absent/present; auto/explicit requests; full 4×4 matrix, reasons and refresh after edits. Update solver guide and new capabilities section. | +| Forcing: `openswmm_forcing.h` | 5 | Per-element climate, node age/temperature, link seepage on `solver.forcing` | RESET/PERSIST; ADD/OVERRIDE; clear/fallback; signed values; project temperature units; isolation from global climate; effect after an actual step. Update forcing/heat/age guides. | +| Batch editing: `openswmm_edit.h` | 4 | Batch-delete methods on `solver.editor` | Duplicate/invalid selections, preflight/atomicity per C contract, cascade/restrict, order independence, stale views and identity remapping. Update editing guide. | +| Initial quality: `openswmm_initial_quality.h` | 3 | Sidecar path and per-row provenance on `initial_quality` | Original/resolved paths, set versus load-on-next-open behavior, file/inline rows, clear/save/reopen, missing file diagnostics. Update initial-quality guide. | +| Live output: `openswmm_output.h` | 3 | `OutputReader.open_live`, `is_live`, `refresh` | Zero complete periods, incremental flush, incomplete tail, finalization, repeated refresh, array snapshots and close semantics. Update output-reader guide. | +| Heat: `openswmm_heat.h` | 2 | Shortwave/cloud timeseries names | Missing/assigned series and save/reopen consistency. Update heat guide. | +| Process registry: `openswmm_process_components.h` | 2 | Known component vocabulary, separate from configured instances | Enumerate metadata; unknown IDs; registry versus model membership. Update process-components guide. | +| Timeseries: `openswmm_tables.h` | 2 | Relative-time metadata on a timeseries | Relative/absolute distinction, start-date changes, authored round trip. Update tables/datetime guides. | +| Link orientation: `openswmm_links.h` | 1 | Explicit authored-orientation restoration | Adverse-slope reversal, endpoints/offsets/vertices, idempotency and save/reopen. Update links/editing guides. | +| Compatibility writing: `openswmm_model.h` | 1 | `solver.write_compat(path, profile)` with `InpProfile` | FULL, legacy and stock profiles; warnings for dropped content; original model unchanged; stock-load compatibility; file failures. Update model-builder/migration guides. | +| **Total** | **118** | | | + +Transport boundary/source rows are explicitly **read-only in the C API revision reviewed**. Their missing setters are an upstream API-design item, not something the Python wrapper should fake. Similarly, do not imply that adding wrappers implements a physical process absent from a selected transport backend. Expose the transport capability matrix and test meaningful supported configurations. + +## Test design and code-documentation requirements + +### Shared contract tests + +Build reusable fixtures that cover: + +1. **Lifecycle:** new/building/opened/initialized/started/running/ended/closed/destroyed, including reads versus authoring mutations and invalid handles. +2. **Ownership and identity:** retained views, deleted/renamed objects, structural generation changes, builder ownership transfer, garbage collection and callbacks holding references. +3. **Units and value semantics:** US/SI equivalents; seconds versus days/hours; project versus SI output quantities; concentration versus mass; signed age/temperature/extraction values; NaN/Inf and range validation. +4. **Array and ABI contracts:** exact dtype/shape/order; owned snapshots versus borrowed views; empty inputs, noncontiguous inputs, invalid sizes, undersized output buffers; platform C `long` width and integer overflow; triangle/quad padding. Compile C-derived enum/struct probes on each platform. +5. **Persistence:** author → write → reopen → compare both values and provenance. Cover relative paths, Unicode paths/identifiers, external sidecars, profiles and write failures. Apply GeoPackage/HDF5 expectations only where that serializer supports the feature; report unsupported behavior explicitly. +6. **Physical observables:** nonzero forcing changes the intended recipient, dry cells remain well-defined, conservative transfers balance within justified tolerances, and supported backend combinations agree on the wrapper contract. Use small analytical/reference fixtures; do not duplicate the entire numerical solver verification suite in Python. +7. **Failures:** correct exception class/code/context; unknown IDs; forbidden states; dangling references; callback exceptions; failed import of an expected native dependency. + +Reuse relevant C++ fixture/test intent from `test_gwf_api.cpp`, `test_gw_transport_authoring.cpp`, `test_gw_transport_kernel.cpp`, `test_2d_surface_quality.cpp`, `test_transport_options_api.cpp`, `test_transport_policy.cpp`, `test_output_reader_live.cpp`, `test_2d_quad_mesh.cpp`, and `test_thread_info.cpp`. Give Python tests independent expected results rather than simply asserting that a call returns success. Every new binding must be exercised by at least one reachable public Python test; every mutable subsystem needs a round-trip test and every numerical subsystem an observable-effect test. + +### Documentation contract for each public member + +Record purpose and underlying C function; parameters and accepted enums; return type and shape; physical units and signs; zero-based API versus one-based file indexing; allowed lifecycle; ownership/copy behavior; mutation invalidation; threading/GIL restrictions; errors and warnings; optional build/runtime capability; persistence and path semantics; and a minimal usable example. + +Use one consistent Sphinx-compatible docstring style for new work and normalize touched older epytext blocks. Do not invent documentation for units that the C implementation does not actually honor: resolve contradictory header/implementation contracts first. + +### Sphinx and related deliverables + +- Add the four new domain guides listed above and connect them through `guide/index.rst`, `api.rst`, and package/module maps. +- Add a units/lifecycle/capability reference shared by all guides; explain SI groundwater results alongside project-unit authoring and 2D conventions. +- Correct initial-quality and model-builder examples; execute guide examples against small checked-in fixture models in CI. +- Update quickstart, model editing, forcing, mass balance, live output, mixed meshes, compatibility migration and optional-installation troubleshooting. +- Update `.pyi`, `__init__.py`, `__init__.pyi`, `__all__`, any Cython re-export declarations and build module registration together. +- Add and verify the typing marker in built wheels; run downstream mypy from outside the source tree. +- Rebuild the parity inventory and retire stale claims/links; update README, CHANGELOG, package support tables and the application manual's Python chapter. +- Keep Sphinx `-W --keep-going`; inspect suppressed duplicate/reference warning categories and replace broad suppression with narrow documented exceptions where feasible. Add executable-example and periodic external-link checks. An HTML build alone is not a behavioral docs test. + +## Ordered implementation strategy + +| Phase | Work and dependencies | Completion gate | +|---|---|---| +| 0 — Reproducible baseline | Capture source hashes and feature configuration; reconcile the two audit tools and old parity matrix; map functions → Python entry points → tests → docs. Add a source-only contract job. | No newly introduced unexplained drift. The current 118 are explicit tracked implementation debt, not silently added to an intentional-exclusion list. | +| 1 — Safety and shared contracts | Fix 2D ownership/invalidation, callback exception capture, same-handle concurrency policy and diagnostic propagation. Synchronize enum members, known stub gaps and report semantics. Depends on baseline. | Regression tests fail on old behavior and pass on fixes; strict typing and public imports agree; retained views cannot dereference a destroyed engine. | +| 2 — Complete existing domain views | Compatibility writer, batch edits, initial-quality sidecars, relative timeseries, heat names, component registry, link orientation, live output and new forcing. | All corresponding gap rows closed with behavior/round-trip/error tests; guides and stubs ship in the same changes. | +| 3 — Transport and capability views | Bind the 13 transport-config functions and 5 engine diagnostics; introduce typed records and constants. | Matrix matches configured engine behavior; read-only limits documented; US/SI and configuration round-trip tests pass. | +| 4 — Groundwater | Bind 26 groundwater and 29 groundwater-transport functions. Split authoring and runtime/results into reviewable changes. Reuse Phase 1 ownership and Phase 3 capability contracts. | Explicit 55-function closure; state and species array tests; authoring round trips; meaningful water/species ledger checks; no false capability claims. | +| 5 — Surface quality and remaining 2D | Bind 14 surface-quality functions and 8 2D diagnostics; finalize mixed-mesh examples and naming. | Explicit 22-function closure; nonzero quality/rainfall fixtures, weights/masks, shape/stride and scalar/bulk parity tests. | +| 6 — Release qualification | Clean wheel builds; installed-package typing and examples; full modern, legacy and top-level test lanes; optional-feature build matrix; Sphinx from tested artifacts. Documentation is developed throughout, not postponed to this phase. | Zero unexplained binding gaps; correct intentionally unsupported inventory; supported wheel matrix green; reproducible docs and no unreviewed missing-feature skips. | + +Use one focused PR per domain or safety contract, with code, tests, stubs and documentation together. Avoid a single large wrapper-generation change that obscures ownership, units, or physical meaning. Preserve established Python names and calling conventions; add aliases/deprecations where API consistency requires a migration. Assign owners and estimates after the shared safety contract and upstream-only requirements are agreed. + +## Definition of done + +- Every modern public C function is classified as supported, intentionally replaced by a Python idiom, intentionally native-only, or explicitly deferred with an owner and issue. Release qualification has no unexplained or silently allowlisted backlog. +- Supported functions have consistent declarations, callable public wrappers, verified enum/struct/array contracts, stubs, exports, meaningful tests and current reference documentation. +- New domains include runnable examples, unit/lifecycle tables, persistence behavior and capability limitations. +- Source-contract, runtime, installed-wheel typing, documentation-example and supported-platform gates run on the branches and tags that actually release the package. +- Legacy compatibility remains tested, and proposed breaking changes carry a migration path. +- Validation results distinguish static checks, syntax compilation, linking, runtime tests and cross-platform results; none stands in for another. + + +## Appendix A — Complete missing-function inventory + +Generated from the existing source-level API coverage guard. These functions occur in public engine headers but have no recognized `.pyx` use and are not in its intentional-exclusion list. + +### `openswmm_2d.h` (8) + +- `swmm_2d_get_coupling_volume_bulk` +- `swmm_2d_get_rain_volume_bulk` +- `swmm_2d_get_rainfall_bulk` +- `swmm_2d_get_rainfall_weights` +- `swmm_2d_output_variable_count` +- `swmm_2d_output_variable_mask` +- `swmm_2d_output_variable_name` +- `swmm_2d_output_variable_text` + +### `openswmm_edit.h` (4) + +- `swmm_gage_delete_many` +- `swmm_link_delete_many` +- `swmm_node_delete_many` +- `swmm_subcatch_delete_many` + +### `openswmm_engine.h` (5) + +- `swmm_get_effective_threads` +- `swmm_get_thread_info` +- `swmm_get_transport_matrix` +- `swmm_transport_class_name` +- `swmm_transport_domain_name` + +### `openswmm_forcing.h` (5) + +- `swmm_forcing_element_climate` +- `swmm_forcing_element_climate_get` +- `swmm_forcing_link_seepage` +- `swmm_forcing_node_age` +- `swmm_forcing_node_temperature` + +### `openswmm_gw2d.h` (26) + +- `swmm_gw2d_get_cell` +- `swmm_gw2d_get_cell_bulk` +- `swmm_gw2d_get_cell_conc` +- `swmm_gw2d_get_column` +- `swmm_gw2d_get_continuity_error` +- `swmm_gw2d_get_dimensions` +- `swmm_gw2d_get_ledger` +- `swmm_gw2d_get_species_ledger` +- `swmm_gw2d_get_tier_histogram` +- `swmm_gw2d_is_active` +- `swmm_gw2d_node_add` +- `swmm_gw2d_node_count` +- `swmm_gw2d_node_get` +- `swmm_gw2d_node_get_flags` +- `swmm_gw2d_node_remove` +- `swmm_gw2d_node_set_exchange` +- `swmm_gw2d_option_get` +- `swmm_gw2d_option_set` +- `swmm_gw2d_row_add` +- `swmm_gw2d_row_count` +- `swmm_gw2d_row_get` +- `swmm_gw2d_row_get_property` +- `swmm_gw2d_row_remove` +- `swmm_gw2d_row_set_property` +- `swmm_gw2d_species_count` +- `swmm_gw2d_species_name` + +### `openswmm_gw_transport.h` (29) + +- `swmm_gw_boundary_quality_count` +- `swmm_gw_boundary_quality_get` +- `swmm_gw_boundary_quality_remove` +- `swmm_gw_boundary_quality_set` +- `swmm_gw_init_quality_count` +- `swmm_gw_init_quality_file_get` +- `swmm_gw_init_quality_file_set` +- `swmm_gw_init_quality_get` +- `swmm_gw_init_quality_remove` +- `swmm_gw_init_quality_set` +- `swmm_gw_params_count` +- `swmm_gw_params_get` +- `swmm_gw_params_remove` +- `swmm_gw_params_set` +- `swmm_gw_sorption_count` +- `swmm_gw_sorption_get` +- `swmm_gw_sorption_remove` +- `swmm_gw_sorption_set` +- `swmm_gw_source_count` +- `swmm_gw_source_get` +- `swmm_gw_source_remove` +- `swmm_gw_source_set` +- `swmm_gw_source_species_count` +- `swmm_gw_source_species_get` +- `swmm_gw_source_species_remove` +- `swmm_gw_source_species_set` +- `swmm_gw_transport_authored` +- `swmm_gw_transport_option_get` +- `swmm_gw_transport_option_set` + +### `openswmm_heat.h` (2) + +- `swmm_heat_get_cloud_timeseries` +- `swmm_heat_get_shortwave_timeseries` + +### `openswmm_initial_quality.h` (3) + +- `swmm_init_quality_file_get` +- `swmm_init_quality_file_set` +- `swmm_init_quality_is_file` + +### `openswmm_links.h` (1) + +- `swmm_links_restore_authored_orientation` + +### `openswmm_model.h` (1) + +- `swmm_model_write_compat` + +### `openswmm_output.h` (3) + +- `swmm_output_is_live` +- `swmm_output_open_live` +- `swmm_output_refresh` + +### `openswmm_process_components.h` (2) + +- `swmm_process_component_known_count` +- `swmm_process_component_known_get` + +### `openswmm_sq2d.h` (14) + +- `swmm_2d_coverage_count` +- `swmm_2d_coverage_get` +- `swmm_2d_coverage_remove` +- `swmm_2d_coverage_row_size` +- `swmm_2d_coverage_set` +- `swmm_2d_curb_length_count` +- `swmm_2d_curb_length_get` +- `swmm_2d_curb_length_remove` +- `swmm_2d_curb_length_set` +- `swmm_2d_get_buildup_bulk` +- `swmm_2d_loading_count` +- `swmm_2d_loading_get` +- `swmm_2d_loading_remove` +- `swmm_2d_loading_set` + +### `openswmm_tables.h` (2) + +- `swmm_timeseries_get_relative_info` +- `swmm_timeseries_set_relative_info` + +### `openswmm_transport.h` (13) + +- `swmm_transport_boundary_count` +- `swmm_transport_conduit_disp_count` +- `swmm_transport_get_boundary` +- `swmm_transport_get_conduit_disp` +- `swmm_transport_get_configured` +- `swmm_transport_get_dispersion_mode` +- `swmm_transport_get_dispersion_value` +- `swmm_transport_get_source` +- `swmm_transport_get_target_dx` +- `swmm_transport_set_dispersion_mode` +- `swmm_transport_set_dispersion_value` +- `swmm_transport_set_target_dx` +- `swmm_transport_source_count` diff --git a/docs/analysis/PYTHON_BINDINGS_REVIEW_2026-09-26_evidence.json b/docs/analysis/PYTHON_BINDINGS_REVIEW_2026-09-26_evidence.json new file mode 100644 index 000000000..fd106a6ed --- /dev/null +++ b/docs/analysis/PYTHON_BINDINGS_REVIEW_2026-09-26_evidence.json @@ -0,0 +1,764 @@ +{ + "date": "2026-09-26", + "head": "2daef06bec8ac48172010e31e709bc15827853b8", + "scope": "working tree, including existing local edits; hashes define reviewed inputs", + "source_counts": { + "c_exports": 1172, + "exports_with_pyx_use": 1050, + "existing_allowlist": 4, + "unexplained_gaps": 118 + }, + "existing_allowlist": [ + "swmm_error_message", + "swmm_get_current_time", + "swmm_get_last_error", + "swmm_get_last_error_msg" + ], + "allowlist_caveat": "swmm_error_message is called by the shared .pxd helper; the current .pyx-only guard misses that use. Counts are the existing guard metrics, not a runtime coverage percentage.", + "missing_functions": { + "swmm_2d_coverage_count": "openswmm_sq2d.h", + "swmm_2d_coverage_get": "openswmm_sq2d.h", + "swmm_2d_coverage_remove": "openswmm_sq2d.h", + "swmm_2d_coverage_row_size": "openswmm_sq2d.h", + "swmm_2d_coverage_set": "openswmm_sq2d.h", + "swmm_2d_curb_length_count": "openswmm_sq2d.h", + "swmm_2d_curb_length_get": "openswmm_sq2d.h", + "swmm_2d_curb_length_remove": "openswmm_sq2d.h", + "swmm_2d_curb_length_set": "openswmm_sq2d.h", + "swmm_2d_get_buildup_bulk": "openswmm_sq2d.h", + "swmm_2d_get_coupling_volume_bulk": "openswmm_2d.h", + "swmm_2d_get_rain_volume_bulk": "openswmm_2d.h", + "swmm_2d_get_rainfall_bulk": "openswmm_2d.h", + "swmm_2d_get_rainfall_weights": "openswmm_2d.h", + "swmm_2d_loading_count": "openswmm_sq2d.h", + "swmm_2d_loading_get": "openswmm_sq2d.h", + "swmm_2d_loading_remove": "openswmm_sq2d.h", + "swmm_2d_loading_set": "openswmm_sq2d.h", + "swmm_2d_output_variable_count": "openswmm_2d.h", + "swmm_2d_output_variable_mask": "openswmm_2d.h", + "swmm_2d_output_variable_name": "openswmm_2d.h", + "swmm_2d_output_variable_text": "openswmm_2d.h", + "swmm_forcing_element_climate": "openswmm_forcing.h", + "swmm_forcing_element_climate_get": "openswmm_forcing.h", + "swmm_forcing_link_seepage": "openswmm_forcing.h", + "swmm_forcing_node_age": "openswmm_forcing.h", + "swmm_forcing_node_temperature": "openswmm_forcing.h", + "swmm_gage_delete_many": "openswmm_edit.h", + "swmm_get_effective_threads": "openswmm_engine.h", + "swmm_get_thread_info": "openswmm_engine.h", + "swmm_get_transport_matrix": "openswmm_engine.h", + "swmm_gw2d_get_cell": "openswmm_gw2d.h", + "swmm_gw2d_get_cell_bulk": "openswmm_gw2d.h", + "swmm_gw2d_get_cell_conc": "openswmm_gw2d.h", + "swmm_gw2d_get_column": "openswmm_gw2d.h", + "swmm_gw2d_get_continuity_error": "openswmm_gw2d.h", + "swmm_gw2d_get_dimensions": "openswmm_gw2d.h", + "swmm_gw2d_get_ledger": "openswmm_gw2d.h", + "swmm_gw2d_get_species_ledger": "openswmm_gw2d.h", + "swmm_gw2d_get_tier_histogram": "openswmm_gw2d.h", + "swmm_gw2d_is_active": "openswmm_gw2d.h", + "swmm_gw2d_node_add": "openswmm_gw2d.h", + "swmm_gw2d_node_count": "openswmm_gw2d.h", + "swmm_gw2d_node_get": "openswmm_gw2d.h", + "swmm_gw2d_node_get_flags": "openswmm_gw2d.h", + "swmm_gw2d_node_remove": "openswmm_gw2d.h", + "swmm_gw2d_node_set_exchange": "openswmm_gw2d.h", + "swmm_gw2d_option_get": "openswmm_gw2d.h", + "swmm_gw2d_option_set": "openswmm_gw2d.h", + "swmm_gw2d_row_add": "openswmm_gw2d.h", + "swmm_gw2d_row_count": "openswmm_gw2d.h", + "swmm_gw2d_row_get": "openswmm_gw2d.h", + "swmm_gw2d_row_get_property": "openswmm_gw2d.h", + "swmm_gw2d_row_remove": "openswmm_gw2d.h", + "swmm_gw2d_row_set_property": "openswmm_gw2d.h", + "swmm_gw2d_species_count": "openswmm_gw2d.h", + "swmm_gw2d_species_name": "openswmm_gw2d.h", + "swmm_gw_boundary_quality_count": "openswmm_gw_transport.h", + "swmm_gw_boundary_quality_get": "openswmm_gw_transport.h", + "swmm_gw_boundary_quality_remove": "openswmm_gw_transport.h", + "swmm_gw_boundary_quality_set": "openswmm_gw_transport.h", + "swmm_gw_init_quality_count": "openswmm_gw_transport.h", + "swmm_gw_init_quality_file_get": "openswmm_gw_transport.h", + "swmm_gw_init_quality_file_set": "openswmm_gw_transport.h", + "swmm_gw_init_quality_get": "openswmm_gw_transport.h", + "swmm_gw_init_quality_remove": "openswmm_gw_transport.h", + "swmm_gw_init_quality_set": "openswmm_gw_transport.h", + "swmm_gw_params_count": "openswmm_gw_transport.h", + "swmm_gw_params_get": "openswmm_gw_transport.h", + "swmm_gw_params_remove": "openswmm_gw_transport.h", + "swmm_gw_params_set": "openswmm_gw_transport.h", + "swmm_gw_sorption_count": "openswmm_gw_transport.h", + "swmm_gw_sorption_get": "openswmm_gw_transport.h", + "swmm_gw_sorption_remove": "openswmm_gw_transport.h", + "swmm_gw_sorption_set": "openswmm_gw_transport.h", + "swmm_gw_source_count": "openswmm_gw_transport.h", + "swmm_gw_source_get": "openswmm_gw_transport.h", + "swmm_gw_source_remove": "openswmm_gw_transport.h", + "swmm_gw_source_set": "openswmm_gw_transport.h", + "swmm_gw_source_species_count": "openswmm_gw_transport.h", + "swmm_gw_source_species_get": "openswmm_gw_transport.h", + "swmm_gw_source_species_remove": "openswmm_gw_transport.h", + "swmm_gw_source_species_set": "openswmm_gw_transport.h", + "swmm_gw_transport_authored": "openswmm_gw_transport.h", + "swmm_gw_transport_option_get": "openswmm_gw_transport.h", + "swmm_gw_transport_option_set": "openswmm_gw_transport.h", + "swmm_heat_get_cloud_timeseries": "openswmm_heat.h", + "swmm_heat_get_shortwave_timeseries": "openswmm_heat.h", + "swmm_init_quality_file_get": "openswmm_initial_quality.h", + "swmm_init_quality_file_set": "openswmm_initial_quality.h", + "swmm_init_quality_is_file": "openswmm_initial_quality.h", + "swmm_link_delete_many": "openswmm_edit.h", + "swmm_links_restore_authored_orientation": "openswmm_links.h", + "swmm_model_write_compat": "openswmm_model.h", + "swmm_node_delete_many": "openswmm_edit.h", + "swmm_output_is_live": "openswmm_output.h", + "swmm_output_open_live": "openswmm_output.h", + "swmm_output_refresh": "openswmm_output.h", + "swmm_process_component_known_count": "openswmm_process_components.h", + "swmm_process_component_known_get": "openswmm_process_components.h", + "swmm_subcatch_delete_many": "openswmm_edit.h", + "swmm_timeseries_get_relative_info": "openswmm_tables.h", + "swmm_timeseries_set_relative_info": "openswmm_tables.h", + "swmm_transport_boundary_count": "openswmm_transport.h", + "swmm_transport_class_name": "openswmm_engine.h", + "swmm_transport_conduit_disp_count": "openswmm_transport.h", + "swmm_transport_domain_name": "openswmm_engine.h", + "swmm_transport_get_boundary": "openswmm_transport.h", + "swmm_transport_get_conduit_disp": "openswmm_transport.h", + "swmm_transport_get_configured": "openswmm_transport.h", + "swmm_transport_get_dispersion_mode": "openswmm_transport.h", + "swmm_transport_get_dispersion_value": "openswmm_transport.h", + "swmm_transport_get_source": "openswmm_transport.h", + "swmm_transport_get_target_dx": "openswmm_transport.h", + "swmm_transport_set_dispersion_mode": "openswmm_transport.h", + "swmm_transport_set_dispersion_value": "openswmm_transport.h", + "swmm_transport_set_target_dx": "openswmm_transport.h", + "swmm_transport_source_count": "openswmm_transport.h" + }, + "stub_member_findings": { + "_pollutants.pyx": { + "Pollutants": [ + "rename" + ] + }, + "_quality.pyx": { + "Landuse": [ + "rename" + ], + "Quality": [ + "validate_treatment_expression" + ] + }, + "_subcatchments.pyx": { + "Subcatchment": [ + "coverages", + "loadings", + "rain_scale_factor", + "snow_scale_factor" + ] + } + }, + "cython_translation": [ + { + "module": "_2d.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_climate.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_controls.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_datetime.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_edit.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_forcing.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_gages.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_geopackage.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_heat.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_hotstart.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_inflows.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_infrastructure.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_initial_quality.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_links.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_massbalance.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_model.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_nodes.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_output_reader.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_pollutants.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_process_components.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_quality.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_reactions.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_solver.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_spatial.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_statistics.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_subcatchments.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_tables.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_water_age.pyx", + "returncode": 0, + "output": "" + }, + { + "module": "_xsect.pyx", + "returncode": 0, + "output": "" + } + ], + "native_syntax_checks": [ + { + "module": "_2d", + "returncode": 0, + "output": "" + }, + { + "module": "_climate", + "returncode": 0, + "output": "" + }, + { + "module": "_controls", + "returncode": 0, + "output": "" + }, + { + "module": "_datetime", + "returncode": 0, + "output": "" + }, + { + "module": "_edit", + "returncode": 0, + "output": "" + }, + { + "module": "_forcing", + "returncode": 0, + "output": "" + }, + { + "module": "_gages", + "returncode": 0, + "output": "" + }, + { + "module": "_geopackage", + "returncode": 0, + "output": "" + }, + { + "module": "_heat", + "returncode": 0, + "output": "" + }, + { + "module": "_hotstart", + "returncode": 0, + "output": "" + }, + { + "module": "_inflows", + "returncode": 0, + "output": "" + }, + { + "module": "_infrastructure", + "returncode": 0, + "output": "" + }, + { + "module": "_initial_quality", + "returncode": 0, + "output": "" + }, + { + "module": "_links", + "returncode": 0, + "output": "" + }, + { + "module": "_massbalance", + "returncode": 0, + "output": "" + }, + { + "module": "_model", + "returncode": 0, + "output": "" + }, + { + "module": "_nodes", + "returncode": 0, + "output": "" + }, + { + "module": "_output_reader", + "returncode": 0, + "output": "" + }, + { + "module": "_pollutants", + "returncode": 0, + "output": "" + }, + { + "module": "_process_components", + "returncode": 0, + "output": "" + }, + { + "module": "_quality", + "returncode": 0, + "output": "" + }, + { + "module": "_reactions", + "returncode": 0, + "output": "" + }, + { + "module": "_solver", + "returncode": 0, + "output": "" + }, + { + "module": "_spatial", + "returncode": 0, + "output": "" + }, + { + "module": "_statistics", + "returncode": 0, + "output": "" + }, + { + "module": "_subcatchments", + "returncode": 0, + "output": "" + }, + { + "module": "_tables", + "returncode": 0, + "output": "" + }, + { + "module": "_water_age", + "returncode": 0, + "output": "" + }, + { + "module": "_xsect", + "returncode": 0, + "output": "" + } + ], + "validation": { + "api_coverage": "4 pass; 1 fail (118 gaps)", + "solver_pxd_attributes": "2 pass", + "mypy_engine": "Success: no issues found in 35 source files", + "mypy_strict_typing_smoke": "Success: no issues found in 1 source file", + "sphinx": "HTML build passed with warningiserror=True, keep_going=True, intersphinx_mapping={}; published configuration and existing warning suppressions otherwise unchanged", + "not_run": [ + "full runtime suite", + "clean native link/wheel build", + "installed-wheel type checks", + "Linux/Windows verification", + "memory-safety reproductions", + "external intersphinx/link validation" + ] + }, + "sha256": { + ".github/workflows/benchmark_dispatch.yml": "72e25bbe98571ca79303ccab5c1bd70262af2416d7b41d86176bdb433d8914e7", + ".github/workflows/codeql.yml": "41135f087d8f83495353f1c96d24cdfc64f6f9478b0e8ba885f2d2f9c63d2b50", + ".github/workflows/deployment.yml": "c42663b444b652eab546ea0e3ce75669cf020128e3998ff1e09fa74765baa307", + ".github/workflows/documentation.yml": "55531bf780e7952a63b2538c3034d01762886d2205a3326ff6eff374b097b65b", + ".github/workflows/regression_testing.yml": "34b0f843e07d5a8218b427ab255fae7062e14153b5f55f3312f260dc2905999f", + ".github/workflows/scorecard.yml": "64c2a000bec81892165c2d49a6e5cfa13dba479a01d69711ea4d85794a078830", + ".github/workflows/typing.yml": "849feeaf6138a1f8ce777622256ecf594158577482f994644b4015fbb68cf3c8", + ".github/workflows/unit_testing.yml": "cb22e7360cadc017bbdfc12125c1eff002f84b6822aa9543ee8f5aac75340d2b", + ".github/workflows/unit_testing_python.yml": "a7218866e1fde1abd2cda5edf6f36c1c26a3b387b50affe13b933a25b1dd47e2", + "include/openswmm/engine/openswmm_2d.h": "268d399af5e5d0854ee6d9d7007374ca5dbbc46d648f199d98b9c82a3b9a21d9", + "include/openswmm/engine/openswmm_callbacks.h": "b9478a3215576729f8ec9dfbaedc5dfa71fa9a314dbc909bd31f1e13507c23bf", + "include/openswmm/engine/openswmm_climate.h": "abe39b945d12c733b9c5e80e3e0e01283215b0f41b0edda311e988d5f7a0fd79", + "include/openswmm/engine/openswmm_controls.h": "d9044dffb854deb66032db6b64843edf6ace9a807984d9a2e6adbd2ee77c45e8", + "include/openswmm/engine/openswmm_datetime.h": "585fd3307bff50e0118730d511f7e4ebb93ef0441a4636184fdf4f318270861d", + "include/openswmm/engine/openswmm_edit.h": "8aa855f546f523050ac40661167f32c0bd15b47746000d6be1301d3283aa9955", + "include/openswmm/engine/openswmm_engine.h": "60572711a25256d79fe5d4d4792ac5465b63e62c600c8815c14e35e91aec2c44", + "include/openswmm/engine/openswmm_engine_export.h": "2bdfbd8f07f33540dd2225a0f19415000afabe7f5e6c026daeaaae0d62434ff7", + "include/openswmm/engine/openswmm_forcing.h": "0488d4e4f1b0526f14589ca1a531426f99c986df1927e3326613bc36ad212cb1", + "include/openswmm/engine/openswmm_gages.h": "ba2a26d0f4dfcc64d43146ccda1eb253b72ede3c9c9cb281793e2e9f445561ac", + "include/openswmm/engine/openswmm_geopackage.h": "de86f4e904bfcefb65320e7be3e964077fde8b8db06e54e0d9d89f1243650f45", + "include/openswmm/engine/openswmm_gw2d.h": "4c0b3da7ddde9bdcfd4a084c0ab4965e6ea95276b203ded9d102eca2881e5d6f", + "include/openswmm/engine/openswmm_gw_transport.h": "a1f8d30f709d1eb7baa29ed6dab8c25c1d4cfed8855d79adfad06279b2b2a168", + "include/openswmm/engine/openswmm_heat.h": "8495210d3006a20a823fc3139066257af4f657e5643b094c580ce272b1304208", + "include/openswmm/engine/openswmm_hotstart.h": "1f6ae581f9cecbc5ab05feba89cc79ae66b968031b7c69fd64a0e308c7c9c49e", + "include/openswmm/engine/openswmm_infil2d.h": "9210857c289caa28d2a25161584669439692e60d9b4989e0805ebfe5d89060e0", + "include/openswmm/engine/openswmm_inflows.h": "ef8e7e5c047a750f00d6b4d04ea71d3fe78812b6f951a8d0b6237e60f22b73ff", + "include/openswmm/engine/openswmm_infrastructure.h": "9798057095169c49d7f77f306d6e3e6a686c72f7055a0f03729675aa1f7641b2", + "include/openswmm/engine/openswmm_initial_quality.h": "48ccfc67100b9f70078190a5ac83c9e747e2f2c46042c6adf6dfaf3913ba5917", + "include/openswmm/engine/openswmm_links.h": "23035dd45330ecf1935e1376dd1e25aaa5cb2f56e4d7ccb5d587ce06de079193", + "include/openswmm/engine/openswmm_massbalance.h": "d6f904cc1e7fa45f73adcce1b07beacc95f6ac5210c158c4408fd8737e098a4b", + "include/openswmm/engine/openswmm_model.h": "92f0171f8298f30b732451fbf6255636b6fe26e280b4f858bab77fb4424e436f", + "include/openswmm/engine/openswmm_nodes.h": "7d35699f13ce94002673d87333d2d8acc68e203834b429ca03c65444b65ec070", + "include/openswmm/engine/openswmm_output.h": "0df7cc589e065c7e6611b94b3c997de0c27fe4384d2f593bb8ee97044182b48b", + "include/openswmm/engine/openswmm_pollutants.h": "5a3e13ed50c066ec20ffebc5524a8d6ae7eea74ba41a52894a6c1c09bf166045", + "include/openswmm/engine/openswmm_process_components.h": "9c9d0010018be37aa3bb62a088d5a1a902967fc76832872736a66782fdd57526", + "include/openswmm/engine/openswmm_quality.h": "f1d650bca9a5b03f9d18d3eda98823814acd473e54fc613b3e4a61965de8eeb9", + "include/openswmm/engine/openswmm_reactions.h": "deea68c6852a3a7e780eccc0edf957714662de49592849e21e7c66d3ac4e9e49", + "include/openswmm/engine/openswmm_spatial.h": "45ffcd0fe1421b1c35500a49d2b6b4a6bcfcd191cdf053ecdaa0ddebe0caad4c", + "include/openswmm/engine/openswmm_sq2d.h": "c60ac1ffc13c125687e3916055f09f2e9fb0a83d57ddcf6467af37f4ac240de5", + "include/openswmm/engine/openswmm_statistics.h": "131e483be0a6a2661a36a809077e541149896ef64d1340a8736dd04c4db51c22", + "include/openswmm/engine/openswmm_subcatchments.h": "20592588c70de0ab6abda4ddb0d30e60b417c6c3fef717a9a8429074dccbb71e", + "include/openswmm/engine/openswmm_tables.h": "c37c668e16ef16f5ba0df132955dc86bbf7fbab60936671a42c4aefb3bcc14ce", + "include/openswmm/engine/openswmm_transport.h": "e07bdfea9b24d8707950986dd7e37e2969e2705bc504408ca91235f3791670f2", + "include/openswmm/engine/openswmm_water_age.h": "5e02a102c515600156a1e519ee06b3bfbaed7786238032b9c3843025f1b0bb89", + "include/openswmm/engine/openswmm_xsect.h": "0c601562cddb7a15d124e4efc38ec70a1da3081fcc96b066dabb9cae401cf549", + "python/docs/LEGACY_API_EXPANSION_PLAN.md": "7be1a40d638354c952559ab34f65e91464138fb881cb696d09beb7fdfa8ba581", + "python/docs/api.rst": "0eaf0125e24d9ca49d9923e7bf574617eb4a3bcfa536573ef42c624b0cd37b3b", + "python/docs/conf.py": "ee445eaf0cb92a13db003bb697cf8062c6950a55b603dc529d30b24b47d2a18e", + "python/docs/guide/2d.rst": "574eb3c3ea3a1c8ac0a6144bc5e3412b9fca6dac07ffa80589e2391f354154b9", + "python/docs/guide/climate.rst": "3255d74cf6cf5e392ab8a8ca2598430c5023199a5e8da82c70bed686cf619767", + "python/docs/guide/concepts.rst": "ff7e77d7788c577317e11a499d8217c8e14da848d14d3f4e557cfaf4b533079d", + "python/docs/guide/controls.rst": "babb416a7c727dd2c6f928da69c4486be2046a30dc867f2c99f41817d0485327", + "python/docs/guide/datetime.rst": "0f8bc61a5919e1ed337c19f254f066db326c74b1d14e055cf4609bb60f138c7d", + "python/docs/guide/editing.rst": "3b716d94397ef09d4ad5fa8bd3ad0ac3cff51105db7c4a6780bd2f5b72c4ffef", + "python/docs/guide/error_handling.rst": "038b7c48432bee9d86e553ed09492ad00c6668dbec4d145c3cb2540a0d466ff7", + "python/docs/guide/forcing.rst": "1e3fdafcb6aa7a7a1a81b803b33c05514e41098d8360fa22d328a75df00aa2a7", + "python/docs/guide/gages.rst": "7cc0be27d677ad612ca58a37c175fa6d5990f8532298eecec48e0ea330da9768", + "python/docs/guide/geopackage.rst": "daea25d08f4d7475a3a26d12983de653fbb452faaee8ca528fd7e0ac74a91b4d", + "python/docs/guide/heat.rst": "b5d92d90d16eaa9158391adae5407ae0ac991de78c1c9f91ad7888e578b0e825", + "python/docs/guide/hotstart.rst": "a9bf60bc027f1cea556a68aeb99fce5c0dc13a2047f5d60eef83af685050d485", + "python/docs/guide/index.rst": "9ac969066fbe0c575e7f64be6c680bee1b2ee471030a7071f2acc8faa256b0d2", + "python/docs/guide/inflows.rst": "9925e74d465658c085f83d8961621f44bf73a17b9e4419147602f97c3f496f3e", + "python/docs/guide/infrastructure.rst": "657a493bdba7b8d10fa9577edd0ecb94462699c48136022c5a4657b3fbe2cce3", + "python/docs/guide/initial_quality.rst": "af1e22f237d5c69766273fb7834eab8c2935e986e23c91130102bd5b24536fe1", + "python/docs/guide/install.rst": "38a64479c78b92ad104035d64a6d4af9115a11c8a7c69bc78a6339bbe9527100", + "python/docs/guide/links.rst": "a4fd3ed6947c2a126b98afbfcf3f2f46001cfab6e08e329a76241328d4c09e43", + "python/docs/guide/massbalance.rst": "cf5d1a73af86c54a782707641c4907e9ca8acb5e4b5d29b5a4adec5fe104fc7a", + "python/docs/guide/model_builder.rst": "993b9ac47157a630701397636d61dcb6643fec3bfad0ab4ae09a3c7bfd6e0705", + "python/docs/guide/nodes.rst": "7ebac5e2080d0175f72600826f822d2210559db1e6ded5c7c678da2534c1984d", + "python/docs/guide/output_reader.rst": "64fc6f8d56f9fbfedbfdd7cbd47eea6d485784e9d59c75eeea755f43983fd1d7", + "python/docs/guide/plotting.rst": "f9173d62e83d2d79dc9bea9702c2c39924ea8c2554129cf8395ba455889e6a5e", + "python/docs/guide/pollutants.rst": "3c7856e9f973545402c701ad2dcecceb149faa6d98a943e258f8c7a1731269ab", + "python/docs/guide/process_components.rst": "7e2edd56134c25f35ee41ad48c4f6454e99cbb167f89041cbf15a5bb2bc9e566", + "python/docs/guide/quality.rst": "5a284ea6d2f059e259ea8dc276e1a357dc3227baa88b213583ce127aae89d924", + "python/docs/guide/quickstart.rst": "f63631d93aa5d337a8cc8f41ea75e73bd01238363955ce8d4e29ecf34e462308", + "python/docs/guide/reactions.rst": "1dc363c09c3eb6890cbfd6d596d5b4f564c5f404ef36ff11b046820159dbe0bf", + "python/docs/guide/solver.rst": "a0cdc5933cba4b61e658d3b927dd6768dfe9117b7622a25eb9f88e5c4bae8f3f", + "python/docs/guide/spatial.rst": "d62b8f309e74a6d1d32b00831913659da434aac4dacd337de2e6c9ff59842ba4", + "python/docs/guide/statistics.rst": "692221411cd6dba10beec9982c8c9e59cffa737462607208d60cb1974366f163", + "python/docs/guide/subcatchments.rst": "e3c00128d109bbce40e528baca3408600b5a20978eb4ca86c69a4176445a0f49", + "python/docs/guide/tables.rst": "ea6edbf66fcd33d6a5d1864ef305facc5f40a6c50f88931667ca68bdb1c22811", + "python/docs/guide/water_age.rst": "b24a7852e8c567d8711cabb793768e80a4a639757caf8532684d98862660282d", + "python/docs/guide/xsect_geometry.rst": "18f9421de50772a8e8b639d2a68143c010637165d5fb2d0a284803ab7bbf99aa", + "python/docs/index.rst": "2ac546b5c12c0dbdf7f4db1155b5cff53b80c11c98e8b60a5f58554063610ea7", + "python/docs/legacy/index.rst": "2ebcbf032a47a6b6079a15ffdf792a78e928d9fd578a118eccc6179f783e9428", + "python/docs/legacy/output.rst": "8ae087871b39a173259ca19f6cec23d6eef2cfdc7180199331f0765b4a948071", + "python/docs/legacy/solver.rst": "958949e9b0a4e8337d9ed8503ae483254af34bdeb301ab6b2f75ca5209e20ebb", + "python/docs/license.rst": "e45c68e560c9ec410008321fa0470d163c2ec8ffe4bfc3154a7b87eea339f835", + "python/docs/migration/swmm5_to_swmm6.rst": "3f963d36e02eea26cde22c82eb7b2c2984566e5e628cc501e92dc15f5e71d1fc", + "python/docs/requirements.txt": "9f73ad40f09b6a25eceb194ec292e1b9292ed8c23c9ec3c90d21a5aba6c3944e", + "python/openswmm/CMakeLists.txt": "9c74c2b1cd674e38192e79d5adbb8855f89645843380318384a601954f26e9f5", + "python/openswmm/__init__.py": "a1457d58b2aa3ca34faacef4c87cc3c5cd6c85cfddf73d98ca22934b913e35bb", + "python/openswmm/data/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "python/openswmm/engine/CMakeLists.txt": "c5df95898ae1228805ff5486aca76ac57daf8cbc62d5a245d9be31179d9e8d0a", + "python/openswmm/engine/_2d.pxd": "828bf46e8a9b72dcfc1c32cdd7eddc81f3bff2a917d186ad61fba7dba118b76e", + "python/openswmm/engine/_2d.pyi": "24cbcdd8910b2169f880f58e8eafc0dd603c683018f66e83458e2d3932259ba5", + "python/openswmm/engine/_2d.pyx": "5766b64800d00be347cc7275afb335af86658499d3b29c1c8176b8b866cdbc89", + "python/openswmm/engine/__init__.pxd": "15e77b6f88304122823ffcdaaa59cda9e78ccf21ddd744a96f6ddc0b44044b76", + "python/openswmm/engine/__init__.py": "c75b42a51972fe5ef7867a9c6fbbcad3bcf451d0ffa72e8d02a7737123a4322a", + "python/openswmm/engine/__init__.pyi": "c03b131111edee881ca2f92433d84ffc80d5729f55f2435a4c31e8a277700a5f", + "python/openswmm/engine/_climate.pyi": "36218586d6d88695c08db76b748449cf1eafc955b02c37c6a7dbb103afbde966", + "python/openswmm/engine/_climate.pyx": "93dde91cd1ae8ec54a2eeca8c044f3ee7c5342d50a3b6463ef54fa31f8177304", + "python/openswmm/engine/_common.pxd": "b5f5363dd9f665f75c879a486a6a6d5771500f944aa857857ff08eb5eaa44c02", + "python/openswmm/engine/_controls.pyi": "72f315dab3f59d4fc623d06471b7aa8d376bcf21e56fde8ab14507e04cf12843", + "python/openswmm/engine/_controls.pyx": "0c4bff6e72680a62a0990e7cbb0b546e4be4f1b2350725ee11f14c7bae9c0600", + "python/openswmm/engine/_dates.py": "fff1d2eafa776616c2773d67caab6d63f41cccafbce9bd983d3331fe3f85d655", + "python/openswmm/engine/_dates.pyi": "da0aa2d7be8bc29223ba4a94ce2a5df9740a1cdf10ab47430615d370f3c28d31", + "python/openswmm/engine/_datetime.pyi": "cb252791b765467e8cfd01e7d502321f5b3481c8de1b3fb5f5d776d669c6ad5d", + "python/openswmm/engine/_datetime.pyx": "ae2bce8e1ead01d4a539d703f837cbf979ea71060ad0eb40414a4111248cb588", + "python/openswmm/engine/_edit.pyi": "a915107248f4747a4cff9eafe4980a43de9ed95eda99f44e54b6f139d1a3eb43", + "python/openswmm/engine/_edit.pyx": "ae1bec1b0c8ec51226b9caf72e85c876abd4e09f219bb524429db6bdcb0419fc", + "python/openswmm/engine/_enums.py": "556993819fa76531d26259a639d36df9352289354dc8248f593b460e8a06e35a", + "python/openswmm/engine/_enums.pyi": "46396721e392789ab5d354843eb0de62a33482cbeea5a5c4181faf18ce0c5f8d", + "python/openswmm/engine/_exceptions.py": "3be9371c3a77101a248d5ea7fec064bc5894ba3d6582d77c7095159c8e2ca37e", + "python/openswmm/engine/_exceptions.pyi": "cf365bd80fc95cfe51b4b881cb7e47e47e5e2a20298693cc64a22d993e897d27", + "python/openswmm/engine/_forcing.pyi": "1f02bd3f44b36431356fac6acdc16745ed06313fd9d6ea7af8accb7289678305", + "python/openswmm/engine/_forcing.pyx": "4af21b58a4a34aeb015a93a70f4f3573237fad767bff56b4d1b9afcaa5448deb", + "python/openswmm/engine/_gages.pyi": "c5166e72835b280b65abbb3b84e437ea3e50651e2609738a2bf281b08a1d8ad0", + "python/openswmm/engine/_gages.pyx": "a62202e6b8c3591f04b46ee3fcec8c82c81b3795edc830157d460109017e197f", + "python/openswmm/engine/_geometry.py": "380ed55ffc17fa84276a7415518e97ba88e5e463cae41ccc1f0131d629022549", + "python/openswmm/engine/_geopackage.pyi": "7d3e452b7c181e76c62223363f0663f53076352c51625a3a9cdd453d4e70efe8", + "python/openswmm/engine/_geopackage.pyx": "58be68241c1ab4769c3e9d52542058fc253a7afd20ab271fc1b690828d72961f", + "python/openswmm/engine/_heat.pyi": "c71b866882e688341689b1e023115627b7b77197ff55b5cf20dc468ebb67b01a", + "python/openswmm/engine/_heat.pyx": "01bcd08dbb448b86065e6f988592f6a139df734b608ac8f5077765a72fad2566", + "python/openswmm/engine/_hotstart.pyi": "366c0d948bdf70f27e920cbb1f642fc9434b0c6d804c323f4db914f7baac262e", + "python/openswmm/engine/_hotstart.pyx": "5d72761a8c7b1bda61c98e0c37bcd0d47b569deda809c3164f688ecbd4c4e6ef", + "python/openswmm/engine/_inflows.pyi": "48a7e6020ba067bd75f5a16c35f494a32f39b9455a04d6f1e1cb2e5a5aa8c1bd", + "python/openswmm/engine/_inflows.pyx": "73cab4cacb2ba3991db46f09f11ad4dfdb88054a13a688ba90fe2f4000841017", + "python/openswmm/engine/_infrastructure.pyi": "824ca5a5f17d826389ba79bbf89c13531eda7b88e8df3ccde8c00d871234929e", + "python/openswmm/engine/_infrastructure.pyx": "cff580fc4df7ffd0ba4ee9041f0ca1111c856044c1f45028c8e7ddb16f39aac6", + "python/openswmm/engine/_initial_quality.pyi": "1c5324ebb833e81dfb33dcf4bcf7cb8883d6791816372c1cd5f6ba8faefc2990", + "python/openswmm/engine/_initial_quality.pyx": "926356755dddde7e49e5be9117306e5c465b8d3c0dc21a66052acaa6036ed73f", + "python/openswmm/engine/_links.pyi": "5dbd3236161b36e2710f9275589f08a0e445897652d384859f97889aaf68b004", + "python/openswmm/engine/_links.pyx": "2ad5195174ad8b6ffe9dcc21a41b99c17e523ad4a5f71c2e82eaa95e05b8849a", + "python/openswmm/engine/_massbalance.pyi": "3cf3c3a056ef77bfdfcd736e57273b9489aa8fd9381c3167d873147bd8853406", + "python/openswmm/engine/_massbalance.pyx": "727a31668947ad71d4188a76a4ecbe82a1e0624babe369b50cbccd93651f1cab", + "python/openswmm/engine/_model.pyi": "ecf0281e5e9db22ecd330b756dad45c6133f3ac18b108c9646c03a9dc06e4800", + "python/openswmm/engine/_model.pyx": "656c75ddda8c44312e5f13c5d9cdbc85c59ee20592d4ef477c47fb5a9855b0f9", + "python/openswmm/engine/_nodes.pyi": "86f32c19b887376b0b63c2d3e1fe20541e9530a9a7824d1022b1f9f356ba6694", + "python/openswmm/engine/_nodes.pyx": "8393e4328aff50df352100065063407e0080519ff402af267a5fb1f4bf3ee825", + "python/openswmm/engine/_output_reader.pyi": "aca8e762379f7d4c994fd16625f5d19d9b41c2d56b65b201b23ffb7f33086b1e", + "python/openswmm/engine/_output_reader.pyx": "9953e8d2c420bfafa0b69414ce1e0875348f380878493e44dddc9c09f6b63ebb", + "python/openswmm/engine/_pollutants.pyi": "d642cb352e9dff581d2acb77004f728e79f8186243790e7f0b15e0aa4a81f7dc", + "python/openswmm/engine/_pollutants.pyx": "123eeba3e36cfe3d5afd6b8f2e07aae7368426f5e66087286e8a6d9809e720a2", + "python/openswmm/engine/_process_components.pyi": "5dae437eaf0786af511e4a9c29153dce150fef09d8a65c569edfcbdfeae54395", + "python/openswmm/engine/_process_components.pyx": "181452877477d16fd636b48186783ff9d52615ed6b113ecb3570eb15cd20069f", + "python/openswmm/engine/_quality.pyi": "044c714264287d9ef1ed2d5ccb6d3a11281e3baaf5bb16fbf2737f27af2d4386", + "python/openswmm/engine/_quality.pyx": "a499fada754e6726dc608e40f1cb43f0e6e596902d1eb356f23811dec4e4f218", + "python/openswmm/engine/_reactions.pyi": "34399884a50f2fc387909557906f5e5e4560c12cdb40779b7cb2f54c389e4f37", + "python/openswmm/engine/_reactions.pyx": "8b3394209e7db162f728f5404f56d911f0ab1e1ff95e22679b9d0466c128d80a", + "python/openswmm/engine/_report.py": "a848f9a0db349f4ffb7ed2d060c6d06d557c1ddbc38c0b899403630561720550", + "python/openswmm/engine/_solver.pxd": "2fc3d361bd3ed9855dc3b09bd1707c70173d6e03cb458fbdf483aca9a24643a8", + "python/openswmm/engine/_solver.pyi": "b0829d7671125a201094322f71d43a06f14e5786eaa2db1315e8b8e45d8745fa", + "python/openswmm/engine/_solver.pyx": "9d8642d702a5c283e5f87115676a9317d7349439f583b76d15588700362a0a50", + "python/openswmm/engine/_spatial.pyi": "1bef90a1db14836d68effbacf36811a3f2e4a04eabfa004f82dd19641053d9f8", + "python/openswmm/engine/_spatial.pyx": "b9063c1321a5b69510198e1a80179b10a30cffd6ab543b06ce68c84ab9059271", + "python/openswmm/engine/_statistics.pyi": "3e36d9625dbcb445b51ac9897b3aa840672a1c66cd4a79ddc173ca0b05a2d8bb", + "python/openswmm/engine/_statistics.pyx": "d622355b49abbfd49c22c2a6cb438c909ce802b1a8be4641ee11e2ee3538f6ae", + "python/openswmm/engine/_subcatchments.pyi": "31b98c70c7fa0e08082279ad41c8c18363bb22d2e5e0c6fa1fe44e5308da8af1", + "python/openswmm/engine/_subcatchments.pyx": "44e006f4f1790ae29d0f556d7d625f13b17117139cb11f3ae9a161a08823b5fc", + "python/openswmm/engine/_tables.pyi": "6362a82f986a93ea6daa4c9cdc297cdf1e82de51d7fc6bd62865e376a56ad814", + "python/openswmm/engine/_tables.pyx": "495e420bed811ba83f9eb308adc72227986b8856e7cc43229a574473e24e173a", + "python/openswmm/engine/_water_age.pyi": "44b2fa4e5ab47f91ea61875122c1909da0a43d4f9a3a08a2dd5bef63d3814a57", + "python/openswmm/engine/_water_age.pyx": "11dcea5943bc3be1cecf02928e67c8066ed45b6e76153d9db30c4b36c5c733c2", + "python/openswmm/engine/_xsect.pyi": "2193922ca40d318f8d58ae7d274fe4a8b130bb341818b52beb241d9438a76507", + "python/openswmm/engine/_xsect.pyx": "82d626833df95cb7d3561190721f67a25085a918682055a17f306567ddb32bb2", + "python/openswmm/legacy/CMakeLists.txt": "5e907830f0a2281fbec3e9de86cf992fb1d4c51982a93cfa00271e541ed41441", + "python/openswmm/legacy/__init__.py": "774096ffcb03cd1931738e08d948798f1a4135212dd816d1c35bd167522243a8", + "python/openswmm/legacy/engine/CMakeLists.txt": "52064338884176e1864647f31cd2e63b552ce63438caf8def4723341f2d30fd9", + "python/openswmm/legacy/engine/__init__.pxd": "73aeafe1f91337e94fe80d5fbbc9e9aba34c6d56346054841b6ca091c9317346", + "python/openswmm/legacy/engine/__init__.py": "8e1b72ae62ad97d4c0fe41041503ff531ed0a9d3f116c3a59fcb1cab99883b27", + "python/openswmm/legacy/engine/_forcing_log.py": "d514ac58547b155b369cbd4b1f3a1c498c3879c8480dee987891801f1825452e", + "python/openswmm/legacy/engine/_links.py": "59287f6354b0302f94864ec8c8e5b5f3b39942190afffbad6de26b68779c61d0", + "python/openswmm/legacy/engine/_nodes.py": "cd8f2745076badae83193f304184e61fafbb36ddcdae6c41dd0165b69f9a0241", + "python/openswmm/legacy/engine/_raingages.py": "5df0581a5324ec19b015e31fd6e5f8bc7fd4e5abdf1bf7ae38d1dc1a93b846f6", + "python/openswmm/legacy/engine/_solver.pyi": "a4d1ffc15fa0b98b6934639387ff19627215967c0c422d7a22b7554c6772087d", + "python/openswmm/legacy/engine/_solver.pyx": "5d090cae9bea9a0d6f48b43d2c0dd1f2b89194335cbeb21b9d6eb76722cbefba", + "python/openswmm/legacy/engine/_subcatchments.py": "7e1d4869b89146fa2c2d7cde6a5f8a464066a6d632a1879b4f27ef2ab4cae82c", + "python/openswmm/legacy/engine/_system.py": "c4c12edbd3f9b5106f4d146c15d73fd6e7d5fae45b5b4a9b0887ae0c50a924d1", + "python/openswmm/legacy/engine/solver.pxd": "8b5507162c632dfd27e18170f60e84b4ba1d05d2fbe789e583c20177a88a4630", + "python/openswmm/legacy/output/CMakeLists.txt": "1f67cb167ffd5b23492be9ce9ebc1e8ed1b294913f110761e07974613f9472a0", + "python/openswmm/legacy/output/__init__.pxd": "3fa69206162dab8fcb723c813b98cb833c5ebfbba1195583b3f5f78e9ff448ff", + "python/openswmm/legacy/output/__init__.py": "21916cc3ef500ce4e6f85a17adf30ef3bee9c5c4b75655e9198f43014beed3f9", + "python/openswmm/legacy/output/_output.pyi": "2d37273d1a019bd5a1be686a6f66c25f5019e983f1f81236c3af4fac834691b7", + "python/openswmm/legacy/output/_output.pyx": "1f3b5ac42553aa7e8b69c53f255bcc21f0d3e4f25b478128906edab8e091562b", + "python/openswmm/legacy/output/output.pxd": "656538d69f0324cb44da0e6d4d7d0d1b6323538b3f08ef5647b0ae578feadf25", + "python/openswmm/output/CMakeLists.txt": "77a7ec1714557e8bfc5a6a6dde358826b30aa655c9b75b12a2ac080446194b94", + "python/openswmm/output/__init__.pxd": "3fa69206162dab8fcb723c813b98cb833c5ebfbba1195583b3f5f78e9ff448ff", + "python/openswmm/output/__init__.py": "65f5589276ef8e920a6ef5db80f3c00693f18575cf2c3c67c76046f7aca7f9b3", + "python/openswmm/output/_output.pyi": "c2dd394b5fa230e1b37beb71a20c02c204eb18dc4b97ffa0e1103aab1e91cfd7", + "python/openswmm/output/_output.pyx": "dab595b344eda4dbf25045c15cb9ac3883baaec7dadd7ccbaf0b9ec13f5262b0", + "python/openswmm/output/output.pxd": "656538d69f0324cb44da0e6d4d7d0d1b6323538b3f08ef5647b0ae578feadf25", + "python/openswmm/solver/CMakeLists.txt": "6e41f36d62705c846288dd9b0e94e640366521d540c88a455df388266eb38270", + "python/openswmm/solver/__init__.pxd": "73aeafe1f91337e94fe80d5fbbc9e9aba34c6d56346054841b6ca091c9317346", + "python/openswmm/solver/__init__.py": "441e966d5cd5d2e9be6b760f289afa3372b6fbb8b11153a7e7828b5d005df4b4", + "python/openswmm/solver/_solver.pyi": "af426d21df5267dcad01537fb14910373ac1571e13f0342635998ea014bfd074", + "python/openswmm/solver/_solver.pyx": "9c15079e96645b3aa8f28dc5c502cfea0df4533a0eb6a67d72108ef37889cd88", + "python/openswmm/solver/solver.pxd": "bb641331ca9c9e56f6f52a5df37fba62bd3e236d4521e71f726f8adf76dc7ea8", + "python/pyproject.toml": "a83c832eacba6ba5bd56eeb125016a79edf1da274d25269540ac5c4ac46fec81", + "python/scripts/api_drift_audit.py": "ed83e50a761f760138f91316b79a5c3b86ba9f8bc25fec3b02f6a7f5f07a10bd", + "python/tests/__init__.py": "835bc84fd621a7633a38980b473afaa01da9b23b7f577649c31f4cbcb03c91e6", + "python/tests/_paths.py": "1bc96c79fc2cdb8f15ffba1b69bc7cf846a4c33b123490ecbee5d32baffb36e6", + "python/tests/data/__init__.py": "b226b242b47656004b8c0e8e9825273d1fdc7ba1034e67c89147afb235918014", + "python/tests/data/output/__init__.py": "57ca69e1cc4ea9b4cec8546bd84462b56ac2bea4d5f30eeb7f4970ff1a2407b7", + "python/tests/data/solver/__init__.py": "9d9ceddc89d0a3f4b48a0ddd8cbc6cd40cf022114c1de0d825bfab72a050f411", + "python/tests/engine/__init__.py": "79198af1bcffec8d8a6c67254cd11f48ce5efff34232ff3dbece39cc4e60a3d9", + "python/tests/engine/_solver_cases.py": "9b6cb63b1ae7e79f4f531a5ec93bef86b6f956f74140a9f77c1ba510b5d9e823", + "python/tests/engine/output/probe_case_sensitivity.py": "06a31ce047b9b12c02b668a1c5fbf30291ff8f08cf4ed56ef17a3eeb9cfa1d83", + "python/tests/engine/output/probe_lowercase_ingest.py": "4dc42841be34aa968afc927b5ed964ecc7628f53544cb469863296a7cbadd5b0", + "python/tests/engine/test_2d_evap.py": "0c273f49efc481c2ff8f94318afb9f3771d1e78e203c3781a45901390dd352f6", + "python/tests/engine/test_2d_ts_stage.py": "c5281c0ce21785d5351d1bbbaf3b5229a9a89f68064e3a201f1bc24d352051c6", + "python/tests/engine/test_aquifers_pythonic.py": "3461da6360582148ded8189fb9508aaa1246c8ad8530b91fde6364d5275e3007", + "python/tests/engine/test_callbacks_and_xsect.py": "37aeb458ec96332c2057a8f3ac8a91cc7adf75d5695ef0c9684ee8b348096998", + "python/tests/engine/test_climate_pythonic.py": "aef93880b63a68b8c5f91cb30720a423d1cf2f57e03bfdf57884e5d295392a31", + "python/tests/engine/test_common.py": "2d004e2c30e43ae262a06cdb606744f7a1ff157024a0b5764819c6841895e559", + "python/tests/engine/test_concurrent_simulation.py": "54f1a70cc5fd476697bcc0ee5595f6caf0d008cb142b53290f081a76d31b2e9d", + "python/tests/engine/test_controls_advancement.py": "fa7136dbb7d89261fdc4cd61b43bb13afa384c4167b37426cb80fc3a709047ed", + "python/tests/engine/test_controls_inflows.py": "99bc5ebec635b3945e74c122ff2423d1b66bbcef255c88951ccb9394eefc3364", + "python/tests/engine/test_controls_references.py": "1ae5a6cbf74d51f95048231f8eb643061e631a1f055d7ba29e60f9fbbc2f75f7", + "python/tests/engine/test_edit.py": "1bc23f2b655421217876dea11447a561ce70acfbe5fa0e7aeecd4c6b4f10ecb1", + "python/tests/engine/test_edit_extended.py": "00cda7ac735d667cabdf11e79891c0c338377aee793af2fe4e68c76eac07deb9", + "python/tests/engine/test_editor_roundtrip.py": "a0af5887f9a5c7c722025c97f3ac0949ab8b776621c92de10afb543cbe57cd27", + "python/tests/engine/test_enums.py": "ba2f6ca089d56e3f089773b0a6e270f271645ca38413c2b02d63f75613984d36", + "python/tests/engine/test_files_iface_gaps.py": "9b97c0a5c6b56004b44379a4777a89cdab41b3088cd16ae52145619a8c844c73", + "python/tests/engine/test_forcing_climate.py": "284cb3eb20545086d725c033238400d5906b686fc9b092da9328237f8246a8c0", + "python/tests/engine/test_forcing_climate_evap.py": "ba518882f0233a871ed24382b9c8ccf3dbd4a557a897f41d955bdb9a4f3f959d", + "python/tests/engine/test_forcing_pet.py": "9c9e908d90460f078c66f1b9d2c885e8376116b679937216e5621c9b53e17500", + "python/tests/engine/test_forcing_quality.py": "4edc3b4cfbb9c63ddedb7b9f0cb3defcc1bf326891ff736d519b053dcf17a675", + "python/tests/engine/test_forcing_snow.py": "ec39fa3cf98816f56b15666e77e3922e61145e31b68c2280d000a700ef7816d1", + "python/tests/engine/test_gages_pythonic.py": "fb8cc4c925672d1659a1a119d006d373d0630c97e68e42553dcf84a5bc7f3f6e", + "python/tests/engine/test_geopackage.py": "57395e48df79ba9fef9e059c37474b49e794c93281519b59a9c68dcf2cae51fa", + "python/tests/engine/test_geopackage_export.py": "66f33f873b14382b8cdda784107a3051abf0e95f0153d919601e8d71f217df05", + "python/tests/engine/test_heat.py": "56ae02c10a394750ab54e545487277a71173080f1064f9c4e4387b5d57a57434", + "python/tests/engine/test_hotstart_pythonic.py": "68db3049b4f0dc7b4b885943dc9e1c3fbf788ae40c108343432f332164633792", + "python/tests/engine/test_iface_routing.py": "bfa0b122ada9ebd80a5d8b3b5196897b6845b7ad4fe560233a2e4cb147394bcb", + "python/tests/engine/test_initial_quality.py": "2442807916349c1815b865b08818f52719f3b58cc43193d1415cca95b4021608", + "python/tests/engine/test_inlet_api.py": "2e8a1df79418ab170df641afce65c57cea42aa6e7fab7571376e2c95d6e2fa80", + "python/tests/engine/test_integration.py": "2cf54a3d049f00a10a0c453e949a50f50abfe2a0d1c6f6d08900114d6c8e6c1a", + "python/tests/engine/test_lenient_open.py": "402d4e694ac95781efc570b8f509e55dc9d817b208da8618ab95c0d618d95a5f", + "python/tests/engine/test_links_pythonic.py": "3b1b3115ed549ba6d95c966a08bf9147889a0f25282b73fcd67bf3b8ba99ae10", + "python/tests/engine/test_mass_balance_pythonic.py": "4430c6372a87d96d3c90b1c0c1cb38ed5ba727399c4b88a683322e5c14aafbad", + "python/tests/engine/test_model_builder.py": "69191f5ee9e869571acf4bbc901a2426021c56796a22f890eb4d02a714d7216f", + "python/tests/engine/test_model_file_paths.py": "3ff1458bccf2562ee06dd9bf7e45d45980ef2b353738775c366747a0e733799e", + "python/tests/engine/test_nodes_pythonic.py": "858d673c928f95c7dc8c7a064ada74c21490955912e80ea5640dfbb75ab17bbb", + "python/tests/engine/test_opened_state_editing.py": "bb502442736b1672cdc266e6ef8bfe5b887e4d717217069f7c8976dbf0a1e6ad", + "python/tests/engine/test_output_reader_pythonic.py": "712eb8afee71e80d5bdbd9fbdd9ad4a63fb36ddd2a30a06af03f774994fe2c5b", + "python/tests/engine/test_output_species_ids.py": "08e2d352738f155cd602709206bd725226ff6cd1ded69823fd25ee622e4f690e", + "python/tests/engine/test_p7_pythonic.py": "f193c89e1eb10149d722bc24a4892244273bec098d70819a8aa56d2299214cd7", + "python/tests/engine/test_p8_pythonic.py": "74de8252290823caf44bb80c694b3e16f04c8235b2a588c50015b6a74855ddce", + "python/tests/engine/test_param_runtime.py": "a9c46b2b9961d25521f8729c83cbeb28ba5bbd575e3fe74645fc43fd51d4f731", + "python/tests/engine/test_process_components.py": "6b74257a9ee9179e51d1759ee6dccdf697e18e57c82636028739c8bbc0270ddc", + "python/tests/engine/test_quality_editing.py": "4a87921b5b9020e2ec5e753aad6dc0a74d554909f2c33dfb532a5f5c2c8dca7c", + "python/tests/engine/test_rdii_advancement.py": "480571987fdeb1f56261e2872aa4c5161b0dafda1d029dc650b00adca53fc793", + "python/tests/engine/test_reactions.py": "9f0ba8d2e59e00a3732b4a561e4fc69f6cab44bd9ac4bdfb54e097713f1f7ccb", + "python/tests/engine/test_runoff_interface.py": "b12485337069adaa958107daf96d170b8501f0cf753e484fa35ceccb33a7a663", + "python/tests/engine/test_snowpacks_pythonic.py": "0238914e676884220c9dcaa58c32678cbaaee701e0d827a94b79c66b7f8afaf9", + "python/tests/engine/test_solver.py": "6e9a9b6557c27b256294fa22db3797b5e586fb34e458f297e7325072ea14a48b", + "python/tests/engine/test_solver_introspection.py": "9593b46c5665c110dc758e717999f1b52915b50ffc83cec1f9b42349b3bedeac", + "python/tests/engine/test_solver_pythonic.py": "b4b791632ee72c32351dcae75218616b699aabce65cb950f076bb459c650df14", + "python/tests/engine/test_solver_units.py": "02fbc544d946be904efcf941737a319c9e768e9164485664631891dab4e629fe", + "python/tests/engine/test_state_injection.py": "7bf52e381e70ff2d57f2cc0321685a2fa343b1063297ebc03e47b3526a67a6d7", + "python/tests/engine/test_statistics_pythonic.py": "12772f691a687b3a111b7cfb59f501d77d866c461dde901eac5e47575dae86df", + "python/tests/engine/test_storage_shapes_pythonic.py": "e49001b264e97c6df3eb3ac6f0ac2f90a473f42f82b5d72a95dbe7571b647d43", + "python/tests/engine/test_streets_params.py": "9412ca0057c31ccecc1f7d64ad9c53ac856ccd63be90a9a5268dea9cfffb86a8", + "python/tests/engine/test_subcatchments_pythonic.py": "ae1c07d9292b6a84cb2dca52585403a00599ec915166cbc1ac0051d43f0c52f2", + "python/tests/engine/test_surface2d_run_stats.py": "70c78e33dd51c5de5d11f4d22c89cff775d4f40a073bf57f01ab60fab4f34735", + "python/tests/engine/test_surface2d_view.py": "e401afed50a4818397eb80319e976f15cd4366667db760cb1aa8093796195333", + "python/tests/engine/test_transects_pythonic.py": "fbbc240ef33568abad244901d2ed52367e5678674bf4908d49c523120009c5d4", + "python/tests/engine/test_userflags_schema.py": "fb5e178f8762b1868db3344a1f8402ec30df0c30420f6f03bdf7b162652bc64a", + "python/tests/engine/test_water_age.py": "59e6c2fda88387fb8b67751ef466a3c9a35a0647b14db6d67208f4129c38d4d8", + "python/tests/engine/test_workflow.py": "cc734534456af9818416b0db0e6ade8234c87d78e32225fb3ac2f5d92ae5a853", + "python/tests/engine/test_xsect_geometry.py": "112e5b5f8a410cdc654c5c5b5282e99205d94765af9f4a77c5db807c8aa1683b", + "python/tests/legacy/__init__.py": "7958978068e4eb11b871d9b7062ddc0f1d9993ae50eccf8f968ef252d8fdc5e5", + "python/tests/legacy/test_output.py": "03b239c05ccd8a52ffdf16b48d73df09be62c9b6861ae02026e9f5ac2fa3bd34", + "python/tests/legacy/test_param_runtime.py": "6ac859d67dc20286a5864562d44593e3432df636ee64a48becd0a5cf89637e19", + "python/tests/legacy/test_phase1_enum_coverage.py": "ee4efe8f3e4d11da4509e3914f1f30810d0ca57ee751b5ebaa7f31532895c503", + "python/tests/legacy/test_phase2_statistics.py": "41415bd453b1f99d6c30b345cbac1551f45573473cbe11dd5f230e27fbd79d14", + "python/tests/legacy/test_phase3_oop_wrappers.py": "ae169cb9abbc7a786f68d8cebdb9404074e3d4768d1a7489f0eb2ff7d12bcbe7", + "python/tests/legacy/test_quality_sources.py": "2380a63ef8eca783b90d82dbaab87c20823fea9becd9589878c30036d227f560", + "python/tests/legacy/test_snow_api.py": "2df824c149d99ecdf34284ed44deee36025f8dc43ced2972d5db0bfeb146e27c", + "python/tests/legacy/test_solver.py": "fb534492e02e1ae1865ec5ea21cae1fb9ac54fa5bf4c72a5fc9030657015fa20", + "python/tests/legacy/test_subcatch_api_pet.py": "aea501aaa1010656d7134ef6a20b1d4bbb6d2af4583642c517d68716249192c0", + "python/tests/legacy/test_system_api_climate.py": "85a973332af65f58ee16dac6eec06e824b3dacbf930336cafcebbd1855b79212", + "python/tests/test_2d_marcher_options.py": "226bdb69a28ddd190aaf09a08d8706867b0ef804418c88c0e724b8259b264741", + "python/tests/test_2d_triangle_coupling.py": "b86501fa82476ec5ae071c0166fc064c9e93b000a12fb76cb738517ee36fa9e9", + "python/tests/test_2d_triangle_initial_state.py": "058d8ad31d48603f97f4b5b5339a4e779dfc3ea1e3470c49d55de62db16663ec", + "python/tests/test_api_coverage.py": "e5370b5e120445bfacdb1f35c21012c326c90302332726d5e3aae5b930f5fee5", + "python/tests/test_fv_routing_options.py": "feba88943d380b23ff511bdaeef7818e1d791801849a3647805858485e449a7e", + "python/tests/test_pet_parity.py": "a0ef99c30b415c373f1229899e383eb7af18f489b91ab344e474d2095d3de1a4", + "python/tests/test_solver_pxd_attrs.py": "c83f6fd1fa4e75531c0de8e5a559d7fc522b6ff3b40352d5ccaaa44d2f087c11", + "python/tests/typing/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "python/tests/typing/test_surface.py": "1eec75eab37c6bc68f4dbd8e569dd777d9793a62655f6f330bbb59e9ab3bc1a2" + } +} diff --git a/docs/analysis/PYTHON_BINDINGS_REVIEW_2026-09-26_inventory.tsv b/docs/analysis/PYTHON_BINDINGS_REVIEW_2026-09-26_inventory.tsv new file mode 100644 index 000000000..0c0fbca40 --- /dev/null +++ b/docs/analysis/PYTHON_BINDINGS_REVIEW_2026-09-26_inventory.tsv @@ -0,0 +1,1173 @@ +function header line status reference_files +swmm_2d_add_triangle_coupling include/openswmm/engine/openswmm_2d.h 380 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_boundary_edge_count include/openswmm/engine/openswmm_2d.h 839 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_cell_count include/openswmm/engine/openswmm_2d.h 178 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_cell_get_neighbours include/openswmm/engine/openswmm_2d.h 208 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_cell_get_vertices include/openswmm/engine/openswmm_2d.h 201 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_cell_vertex_count include/openswmm/engine/openswmm_2d.h 195 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_clear_triangle_couplings include/openswmm/engine/openswmm_2d.h 392 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_coverage_count include/openswmm/engine/openswmm_sq2d.h 57 gap +swmm_2d_coverage_get include/openswmm/engine/openswmm_sq2d.h 82 gap +swmm_2d_coverage_remove include/openswmm/engine/openswmm_sq2d.h 88 gap +swmm_2d_coverage_row_size include/openswmm/engine/openswmm_sq2d.h 75 gap +swmm_2d_coverage_set include/openswmm/engine/openswmm_sq2d.h 69 gap +swmm_2d_curb_length_count include/openswmm/engine/openswmm_sq2d.h 114 gap +swmm_2d_curb_length_get include/openswmm/engine/openswmm_sq2d.h 122 gap +swmm_2d_curb_length_remove include/openswmm/engine/openswmm_sq2d.h 126 gap +swmm_2d_curb_length_set include/openswmm/engine/openswmm_sq2d.h 118 gap +swmm_2d_edge_get_geometry_bulk include/openswmm/engine/openswmm_2d.h 312 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_edge_stride include/openswmm/engine/openswmm_2d.h 191 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_force_clear_all include/openswmm/engine/openswmm_2d.h 799 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_force_coupling_flux include/openswmm/engine/openswmm_2d.h 793 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_force_evap include/openswmm/engine/openswmm_2d.h 780 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_force_evap_uniform include/openswmm/engine/openswmm_2d.h 786 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_force_rainfall include/openswmm/engine/openswmm_2d.h 759 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_force_rainfall_uniform include/openswmm/engine/openswmm_2d.h 765 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_buildup_bulk include/openswmm/engine/openswmm_sq2d.h 140 gap +swmm_2d_get_continuity_error include/openswmm/engine/openswmm_2d.h 731 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_coupling_flux include/openswmm/engine/openswmm_2d.h 476 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_coupling_fluxes_bulk include/openswmm/engine/openswmm_2d.h 519 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_coupling_volume_bulk include/openswmm/engine/openswmm_2d.h 555 gap +swmm_2d_get_depth include/openswmm/engine/openswmm_2d.h 468 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_depths_bulk include/openswmm/engine/openswmm_2d.h 511 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_dry_depth include/openswmm/engine/openswmm_2d.h 807 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_edge_bc_cum_flux include/openswmm/engine/openswmm_2d.h 961 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_edge_bc_flow include/openswmm/engine/openswmm_2d.h 915 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_edge_bc_flow_tseries_name include/openswmm/engine/openswmm_2d.h 935 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_edge_bc_head include/openswmm/engine/openswmm_2d.h 862 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_edge_bc_rating_curve_name include/openswmm/engine/openswmm_2d.h 953 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_edge_bc_slope include/openswmm/engine/openswmm_2d.h 875 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_edge_bc_tseries_name include/openswmm/engine/openswmm_2d.h 903 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_edge_bc_type include/openswmm/engine/openswmm_2d.h 846 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_edge_conveyance include/openswmm/engine/openswmm_2d.h 595 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_edge_conveyance_bulk include/openswmm/engine/openswmm_2d.h 620 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_edge_flux_bulk include/openswmm/engine/openswmm_2d.h 573 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_head include/openswmm/engine/openswmm_2d.h 472 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_heads_bulk include/openswmm/engine/openswmm_2d.h 515 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_mass_balance include/openswmm/engine/openswmm_2d.h 737 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_max_depth include/openswmm/engine/openswmm_2d.h 659 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_net_source include/openswmm/engine/openswmm_2d.h 505 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_rain_volume_bulk include/openswmm/engine/openswmm_2d.h 534 gap +swmm_2d_get_rainfall include/openswmm/engine/openswmm_2d.h 481 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_rainfall_bulk include/openswmm/engine/openswmm_2d.h 526 gap +swmm_2d_get_rainfall_weights include/openswmm/engine/openswmm_2d.h 498 gap +swmm_2d_get_run_stats include/openswmm/engine/openswmm_2d.h 707 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_solver_last_step include/openswmm/engine/openswmm_2d.h 681 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_solver_steps include/openswmm/engine/openswmm_2d.h 675 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_stat_max_continuity_err include/openswmm/engine/openswmm_2d.h 725 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_stat_max_depths include/openswmm/engine/openswmm_2d.h 713 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_stat_max_velocities include/openswmm/engine/openswmm_2d.h 719 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_total_exchange_flow include/openswmm/engine/openswmm_2d.h 668 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_total_volume include/openswmm/engine/openswmm_2d.h 663 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_triangle_coupling_row include/openswmm/engine/openswmm_2d.h 407 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_triangle_tag include/openswmm/engine/openswmm_2d.h 292 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_vertex_coupling_area include/openswmm/engine/openswmm_2d.h 444 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_vertex_coupling_cd include/openswmm/engine/openswmm_2d.h 419 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_get_vertex_tag include/openswmm/engine/openswmm_2d.h 285 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_is_active include/openswmm/engine/openswmm_2d.h 71 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_loading_count include/openswmm/engine/openswmm_sq2d.h 94 gap +swmm_2d_loading_get include/openswmm/engine/openswmm_sq2d.h 103 gap +swmm_2d_loading_remove include/openswmm/engine/openswmm_sq2d.h 108 gap +swmm_2d_loading_set include/openswmm/engine/openswmm_sq2d.h 99 gap +swmm_2d_output_variable_count include/openswmm/engine/openswmm_2d.h 976 gap +swmm_2d_output_variable_mask include/openswmm/engine/openswmm_2d.h 981 gap +swmm_2d_output_variable_name include/openswmm/engine/openswmm_2d.h 978 gap +swmm_2d_output_variable_text include/openswmm/engine/openswmm_2d.h 983 gap +swmm_2d_prepare_for_edit include/openswmm/engine/openswmm_2d.h 83 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_quad_count include/openswmm/engine/openswmm_2d.h 182 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_reset_edge_conveyance include/openswmm/engine/openswmm_2d.h 625 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_dry_depth include/openswmm/engine/openswmm_2d.h 811 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_edge_bc_flow include/openswmm/engine/openswmm_2d.h 922 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_edge_bc_flow_tseries_name include/openswmm/engine/openswmm_2d.h 928 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_edge_bc_head include/openswmm/engine/openswmm_2d.h 869 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_edge_bc_rating_curve_name include/openswmm/engine/openswmm_2d.h 946 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_edge_bc_slope include/openswmm/engine/openswmm_2d.h 881 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_edge_bc_tseries_name include/openswmm/engine/openswmm_2d.h 892 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_edge_bc_type include/openswmm/engine/openswmm_2d.h 852 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_edge_conveyance include/openswmm/engine/openswmm_2d.h 611 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_triangle_init_depth include/openswmm/engine/openswmm_2d.h 250 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_triangle_init_velocity include/openswmm/engine/openswmm_2d.h 265 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_triangle_mannings include/openswmm/engine/openswmm_2d.h 234 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_triangle_tag include/openswmm/engine/openswmm_2d.h 278 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_vertex_coupled_node include/openswmm/engine/openswmm_2d.h 360 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_vertex_coupling_area include/openswmm/engine/openswmm_2d.h 456 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_vertex_coupling_cd include/openswmm/engine/openswmm_2d.h 429 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_vertex_tag include/openswmm/engine/openswmm_2d.h 272 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_vertex_z include/openswmm/engine/openswmm_2d.h 128 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_set_vertex_z_bulk include/openswmm/engine/openswmm_2d.h 153 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_triangle_count include/openswmm/engine/openswmm_2d.h 95 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_triangle_coupling_count include/openswmm/engine/openswmm_2d.h 327 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_triangle_coupling_rows include/openswmm/engine/openswmm_2d.h 397 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_triangle_get_area include/openswmm/engine/openswmm_2d.h 215 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_triangle_get_centroid include/openswmm/engine/openswmm_2d.h 220 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_triangle_get_coupled_node include/openswmm/engine/openswmm_2d.h 341 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_triangle_get_init_depth include/openswmm/engine/openswmm_2d.h 242 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_triangle_get_init_velocity include/openswmm/engine/openswmm_2d.h 255 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_triangle_get_mannings include/openswmm/engine/openswmm_2d.h 226 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_triangle_get_neighbours include/openswmm/engine/openswmm_2d.h 298 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_triangle_get_vertices include/openswmm/engine/openswmm_2d.h 161 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_vertex_count include/openswmm/engine/openswmm_2d.h 91 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_vertex_coupling_count include/openswmm/engine/openswmm_2d.h 323 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_vertex_get_coupled_node include/openswmm/engine/openswmm_2d.h 333 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_vertex_get_head include/openswmm/engine/openswmm_2d.h 633 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_vertex_get_heads_bulk include/openswmm/engine/openswmm_2d.h 638 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_vertex_get_render_depths_bulk include/openswmm/engine/openswmm_2d.h 650 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_vertex_get_xyz include/openswmm/engine/openswmm_2d.h 101 pyx_use python/openswmm/engine/_2d.pyx +swmm_2d_vertex_get_xyz_bulk include/openswmm/engine/openswmm_2d.h 107 pyx_use python/openswmm/engine/_2d.pyx +swmm_aquifer_add include/openswmm/engine/openswmm_subcatchments.h 856 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_aquifer_analyze_impact include/openswmm/engine/openswmm_edit.h 166 pyx_use python/openswmm/engine/_edit.pyx +swmm_aquifer_count include/openswmm/engine/openswmm_subcatchments.h 827 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_aquifer_delete include/openswmm/engine/openswmm_edit.h 347 pyx_use python/openswmm/engine/_edit.pyx +swmm_aquifer_get_evap_pattern include/openswmm/engine/openswmm_subcatchments.h 945 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_aquifer_get_param include/openswmm/engine/openswmm_subcatchments.h 910 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_aquifer_id include/openswmm/engine/openswmm_subcatchments.h 843 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_aquifer_index include/openswmm/engine/openswmm_subcatchments.h 835 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_aquifer_rename include/openswmm/engine/openswmm_subcatchments.h 873 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_aquifer_set_evap_pattern include/openswmm/engine/openswmm_subcatchments.h 961 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_aquifer_set_param include/openswmm/engine/openswmm_subcatchments.h 926 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_buildup_get include/openswmm/engine/openswmm_quality.h 120 pyx_use python/openswmm/engine/_quality.pyx +swmm_buildup_set include/openswmm/engine/openswmm_quality.h 100 pyx_use python/openswmm/engine/_quality.pyx +swmm_climate_get_adc_impervious include/openswmm/engine/openswmm_climate.h 266 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_adc_pervious include/openswmm/engine/openswmm_climate.h 273 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_adjust_conductivity include/openswmm/engine/openswmm_climate.h 302 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_adjust_evaporation include/openswmm/engine/openswmm_climate.h 290 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_adjust_rainfall include/openswmm/engine/openswmm_climate.h 296 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_adjust_temperature include/openswmm/engine/openswmm_climate.h 284 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_ati_weight include/openswmm/engine/openswmm_climate.h 249 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_dry_only include/openswmm/engine/openswmm_forcing.h 469 pyx_use python/openswmm/engine/_forcing.pyx +swmm_climate_get_elevation include/openswmm/engine/openswmm_climate.h 146 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_evap_monthly include/openswmm/engine/openswmm_climate.h 174 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_evap_rate include/openswmm/engine/openswmm_forcing.h 368 pyx_use python/openswmm/engine/_forcing.pyx +swmm_climate_get_evap_recovery include/openswmm/engine/openswmm_climate.h 191 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_evap_timeseries include/openswmm/engine/openswmm_climate.h 180 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_evap_type include/openswmm/engine/openswmm_climate.h 167 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_humidity_monthly include/openswmm/engine/openswmm_climate.h 230 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_humidity_timeseries include/openswmm/engine/openswmm_climate.h 235 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_humidity_type include/openswmm/engine/openswmm_climate.h 216 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_humidity_variable include/openswmm/engine/openswmm_climate.h 222 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_latitude include/openswmm/engine/openswmm_climate.h 151 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_longitude_correction include/openswmm/engine/openswmm_climate.h 158 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_neg_melt_ratio include/openswmm/engine/openswmm_climate.h 255 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_pan_coeff include/openswmm/engine/openswmm_climate.h 186 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_snow_temp include/openswmm/engine/openswmm_climate.h 244 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_temp_file_start include/openswmm/engine/openswmm_climate.h 134 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_temp_source include/openswmm/engine/openswmm_climate.h 123 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_temp_timeseries include/openswmm/engine/openswmm_climate.h 129 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_temp_units include/openswmm/engine/openswmm_climate.h 140 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_temperature include/openswmm/engine/openswmm_forcing.h 402 pyx_use python/openswmm/engine/_forcing.pyx +swmm_climate_get_wind_monthly include/openswmm/engine/openswmm_climate.h 207 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_get_wind_speed include/openswmm/engine/openswmm_forcing.h 429 pyx_use python/openswmm/engine/_forcing.pyx +swmm_climate_get_wind_type include/openswmm/engine/openswmm_climate.h 200 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_adc_impervious include/openswmm/engine/openswmm_climate.h 269 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_adc_pervious include/openswmm/engine/openswmm_climate.h 276 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_adjust_conductivity include/openswmm/engine/openswmm_climate.h 305 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_adjust_evaporation include/openswmm/engine/openswmm_climate.h 292 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_adjust_rainfall include/openswmm/engine/openswmm_climate.h 298 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_adjust_temperature include/openswmm/engine/openswmm_climate.h 286 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_ati_weight include/openswmm/engine/openswmm_climate.h 252 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_dry_only include/openswmm/engine/openswmm_forcing.h 459 pyx_use python/openswmm/engine/_forcing.pyx +swmm_climate_set_elevation include/openswmm/engine/openswmm_climate.h 148 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_evap_monthly include/openswmm/engine/openswmm_climate.h 177 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_evap_recovery include/openswmm/engine/openswmm_climate.h 193 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_evap_timeseries include/openswmm/engine/openswmm_climate.h 182 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_evap_type include/openswmm/engine/openswmm_climate.h 170 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_humidity_monthly include/openswmm/engine/openswmm_climate.h 232 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_humidity_timeseries include/openswmm/engine/openswmm_climate.h 237 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_humidity_type include/openswmm/engine/openswmm_climate.h 219 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_humidity_variable include/openswmm/engine/openswmm_climate.h 225 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_latitude include/openswmm/engine/openswmm_climate.h 154 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_longitude_correction include/openswmm/engine/openswmm_climate.h 160 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_neg_melt_ratio include/openswmm/engine/openswmm_climate.h 258 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_pan_coeff include/openswmm/engine/openswmm_climate.h 188 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_snow_temp include/openswmm/engine/openswmm_climate.h 246 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_temp_file_start include/openswmm/engine/openswmm_climate.h 136 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_temp_source include/openswmm/engine/openswmm_climate.h 126 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_temp_timeseries include/openswmm/engine/openswmm_climate.h 131 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_temp_units include/openswmm/engine/openswmm_climate.h 143 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_wind_monthly include/openswmm/engine/openswmm_climate.h 209 pyx_use python/openswmm/engine/_climate.pyx +swmm_climate_set_wind_type include/openswmm/engine/openswmm_climate.h 203 pyx_use python/openswmm/engine/_climate.pyx +swmm_conduit_split include/openswmm/engine/openswmm_edit.h 501 pyx_use python/openswmm/engine/_edit.pyx +swmm_conduit_split_inlet include/openswmm/engine/openswmm_edit.h 544 pyx_use python/openswmm/engine/_edit.pyx +swmm_control_add_rule include/openswmm/engine/openswmm_controls.h 70 pyx_use python/openswmm/engine/_controls.pyx +swmm_control_clear_rules include/openswmm/engine/openswmm_controls.h 112 pyx_use python/openswmm/engine/_controls.pyx +swmm_control_count include/openswmm/engine/openswmm_controls.h 77 pyx_use python/openswmm/engine/_controls.pyx +swmm_control_find_references include/openswmm/engine/openswmm_controls.h 144 pyx_use python/openswmm/engine/_controls.pyx +swmm_control_get_id include/openswmm/engine/openswmm_controls.h 105 pyx_use python/openswmm/engine/_controls.pyx +swmm_control_get_rule include/openswmm/engine/openswmm_controls.h 87 pyx_use python/openswmm/engine/_controls.pyx +swmm_control_remove_rule include/openswmm/engine/openswmm_controls.h 124 pyx_use python/openswmm/engine/_controls.pyx +swmm_control_set_link_setting include/openswmm/engine/openswmm_controls.h 205 pyx_use python/openswmm/engine/_controls.pyx +swmm_control_set_link_status include/openswmm/engine/openswmm_controls.h 217 pyx_use python/openswmm/engine/_controls.pyx +swmm_control_validate_rule include/openswmm/engine/openswmm_controls.h 185 pyx_use python/openswmm/engine/_controls.pyx +swmm_conversion_result_free include/openswmm/engine/openswmm_edit.h 423 pyx_use python/openswmm/engine/_edit.pyx +swmm_curve_add include/openswmm/engine/openswmm_tables.h 114 pyx_use python/openswmm/engine/_tables.pyx +swmm_datetime_add_seconds include/openswmm/engine/openswmm_datetime.h 175 pyx_use python/openswmm/engine/_datetime.pyx +swmm_datetime_decode_date include/openswmm/engine/openswmm_datetime.h 138 pyx_use python/openswmm/engine/_datetime.pyx +swmm_datetime_decode_time include/openswmm/engine/openswmm_datetime.h 155 pyx_use python/openswmm/engine/_datetime.pyx +swmm_datetime_encode_date include/openswmm/engine/openswmm_datetime.h 106 pyx_use python/openswmm/engine/_datetime.pyx +swmm_datetime_encode_time include/openswmm/engine/openswmm_datetime.h 121 pyx_use python/openswmm/engine/_datetime.pyx +swmm_datetime_time_diff include/openswmm/engine/openswmm_datetime.h 187 pyx_use python/openswmm/engine/_datetime.pyx +swmm_dwf_add include/openswmm/engine/openswmm_inflows.h 152 pyx_use python/openswmm/engine/_inflows.pyx +swmm_dwf_count include/openswmm/engine/openswmm_inflows.h 603 pyx_use python/openswmm/engine/_inflows.pyx +swmm_dwf_get include/openswmm/engine/openswmm_inflows.h 175 pyx_use python/openswmm/engine/_inflows.pyx +swmm_dwf_remove include/openswmm/engine/openswmm_inflows.h 190 pyx_use python/openswmm/engine/_inflows.pyx +swmm_dwf_set_baseline include/openswmm/engine/openswmm_inflows.h 198 pyx_use python/openswmm/engine/_inflows.pyx +swmm_engine_close include/openswmm/engine/openswmm_engine.h 236 pyx_use python/openswmm/engine/_solver.pyx +swmm_engine_create include/openswmm/engine/openswmm_engine.h 188 pyx_use python/openswmm/engine/_solver.pyx +swmm_engine_destroy include/openswmm/engine/openswmm_engine.h 239 pyx_use python/openswmm/engine/_solver.pyx +swmm_engine_end include/openswmm/engine/openswmm_engine.h 230 pyx_use python/openswmm/engine/_solver.pyx +swmm_engine_get_state include/openswmm/engine/openswmm_engine.h 242 pyx_use python/openswmm/engine/_solver.pyx +swmm_engine_initialize include/openswmm/engine/openswmm_engine.h 206 pyx_use python/openswmm/engine/_solver.pyx +swmm_engine_new include/openswmm/engine/openswmm_model.h 62 pyx_use python/openswmm/engine/_model.pyx +swmm_engine_open include/openswmm/engine/openswmm_engine.h 201 pyx_use python/openswmm/engine/_solver.pyx +swmm_engine_report include/openswmm/engine/openswmm_engine.h 233 pyx_use python/openswmm/engine/_solver.pyx +swmm_engine_run include/openswmm/engine/openswmm_engine.h 261 pyx_use python/openswmm/engine/_solver.pyx +swmm_engine_run_with_callback include/openswmm/engine/openswmm_engine.h 278 pyx_use python/openswmm/engine/_solver.pyx +swmm_engine_set_lenient_open include/openswmm/engine/openswmm_engine.h 314 pyx_use python/openswmm/engine/_solver.pyx +swmm_engine_start include/openswmm/engine/openswmm_engine.h 209 pyx_use python/openswmm/engine/_solver.pyx +swmm_engine_step include/openswmm/engine/openswmm_engine.h 212 pyx_use python/openswmm/engine/_solver.pyx +swmm_engine_stride include/openswmm/engine/openswmm_engine.h 227 pyx_use python/openswmm/engine/_solver.pyx +swmm_error_message include/openswmm/engine/openswmm_engine.h 302 existing_allowlist +swmm_events_add include/openswmm/engine/openswmm_engine.h 469 pyx_use python/openswmm/engine/_solver.pyx +swmm_events_clear include/openswmm/engine/openswmm_engine.h 477 pyx_use python/openswmm/engine/_solver.pyx +swmm_events_count include/openswmm/engine/openswmm_engine.h 453 pyx_use python/openswmm/engine/_solver.pyx +swmm_events_get include/openswmm/engine/openswmm_engine.h 457 pyx_use python/openswmm/engine/_solver.pyx +swmm_events_remove include/openswmm/engine/openswmm_engine.h 474 pyx_use python/openswmm/engine/_solver.pyx +swmm_events_set include/openswmm/engine/openswmm_engine.h 463 pyx_use python/openswmm/engine/_solver.pyx +swmm_ext_inflow_add include/openswmm/engine/openswmm_inflows.h 68 pyx_use python/openswmm/engine/_inflows.pyx +swmm_ext_inflow_count include/openswmm/engine/openswmm_inflows.h 596 pyx_use python/openswmm/engine/_inflows.pyx +swmm_ext_inflow_get include/openswmm/engine/openswmm_inflows.h 96 pyx_use python/openswmm/engine/_inflows.pyx +swmm_ext_inflow_remove include/openswmm/engine/openswmm_inflows.h 115 pyx_use python/openswmm/engine/_inflows.pyx +swmm_ext_inflow_set_baseline include/openswmm/engine/openswmm_inflows.h 130 pyx_use python/openswmm/engine/_inflows.pyx +swmm_ext_inflow_set_scale include/openswmm/engine/openswmm_inflows.h 123 pyx_use python/openswmm/engine/_inflows.pyx +swmm_file_path_get include/openswmm/engine/openswmm_model.h 398 pyx_use python/openswmm/engine/_model.pyx +swmm_file_path_set include/openswmm/engine/openswmm_model.h 419 pyx_use python/openswmm/engine/_model.pyx +swmm_files_get include/openswmm/engine/openswmm_model.h 307 pyx_use python/openswmm/engine/_model.pyx +swmm_files_set include/openswmm/engine/openswmm_model.h 322 pyx_use python/openswmm/engine/_model.pyx +swmm_finalize_model include/openswmm/engine/openswmm_model.h 91 pyx_use python/openswmm/engine/_model.pyx +swmm_forcing_clear include/openswmm/engine/openswmm_forcing.h 560 pyx_use python/openswmm/engine/_forcing.pyx +swmm_forcing_clear_all include/openswmm/engine/openswmm_forcing.h 569 pyx_use python/openswmm/engine/_forcing.pyx +swmm_forcing_climate_evap include/openswmm/engine/openswmm_forcing.h 448 pyx_use python/openswmm/engine/_forcing.pyx +swmm_forcing_climate_temperature include/openswmm/engine/openswmm_forcing.h 391 pyx_use python/openswmm/engine/_forcing.pyx +swmm_forcing_climate_wind include/openswmm/engine/openswmm_forcing.h 418 pyx_use python/openswmm/engine/_forcing.pyx +swmm_forcing_element_climate include/openswmm/engine/openswmm_forcing.h 547 gap +swmm_forcing_element_climate_get include/openswmm/engine/openswmm_forcing.h 556 gap +swmm_forcing_gage_rainfall include/openswmm/engine/openswmm_forcing.h 486 pyx_use python/openswmm/engine/_forcing.pyx +swmm_forcing_link_flow include/openswmm/engine/openswmm_forcing.h 224 pyx_use python/openswmm/engine/_forcing.pyx +swmm_forcing_link_quality include/openswmm/engine/openswmm_forcing.h 291 pyx_use python/openswmm/engine/_forcing.pyx +swmm_forcing_link_seepage include/openswmm/engine/openswmm_forcing.h 272 gap +swmm_forcing_link_setting include/openswmm/engine/openswmm_forcing.h 238 pyx_use python/openswmm/engine/_forcing.pyx +swmm_forcing_node_age include/openswmm/engine/openswmm_forcing.h 206 gap +swmm_forcing_node_head_boundary include/openswmm/engine/openswmm_forcing.h 161 pyx_use python/openswmm/engine/_forcing.pyx +swmm_forcing_node_lat_inflow include/openswmm/engine/openswmm_forcing.h 147 pyx_use python/openswmm/engine/_forcing.pyx +swmm_forcing_node_quality include/openswmm/engine/openswmm_forcing.h 176 pyx_use python/openswmm/engine/_forcing.pyx +swmm_forcing_node_temperature include/openswmm/engine/openswmm_forcing.h 193 gap +swmm_forcing_subcatch_evap include/openswmm/engine/openswmm_forcing.h 331 pyx_use python/openswmm/engine/_forcing.pyx +swmm_forcing_subcatch_rainfall include/openswmm/engine/openswmm_forcing.h 310 pyx_use python/openswmm/engine/_forcing.pyx +swmm_forcing_subcatch_snowfall include/openswmm/engine/openswmm_forcing.h 351 pyx_use python/openswmm/engine/_forcing.pyx +swmm_gage_add include/openswmm/engine/openswmm_gages.h 95 pyx_use python/openswmm/engine/_gages.pyx;python/openswmm/engine/_model.pyx +swmm_gage_analyze_impact include/openswmm/engine/openswmm_edit.h 146 pyx_use python/openswmm/engine/_edit.pyx +swmm_gage_count include/openswmm/engine/openswmm_gages.h 66 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_forcing.pyx;python/openswmm/engine/_gages.pyx;python/openswmm/engine/_spatial.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_gage_delete include/openswmm/engine/openswmm_edit.h 251 pyx_use python/openswmm/engine/_edit.pyx +swmm_gage_delete_many include/openswmm/engine/openswmm_edit.h 297 gap +swmm_gage_get_data_source include/openswmm/engine/openswmm_gages.h 258 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_get_file_column include/openswmm/engine/openswmm_gages.h 318 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_get_file_format include/openswmm/engine/openswmm_gages.h 330 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_get_rain_interval include/openswmm/engine/openswmm_gages.h 276 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_get_rain_type include/openswmm/engine/openswmm_gages.h 249 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_get_rain_units include/openswmm/engine/openswmm_gages.h 339 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_get_rainfall include/openswmm/engine/openswmm_gages.h 346 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_get_rainfall_bulk include/openswmm/engine/openswmm_gages.h 367 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_get_rainfall_series include/openswmm/engine/openswmm_gages.h 426 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_get_rainfall_series_count include/openswmm/engine/openswmm_gages.h 392 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_get_scale_factor include/openswmm/engine/openswmm_gages.h 267 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_get_snow_factor include/openswmm/engine/openswmm_gages.h 285 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_get_station_id include/openswmm/engine/openswmm_gages.h 307 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_get_timeseries include/openswmm/engine/openswmm_gages.h 296 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_id include/openswmm/engine/openswmm_gages.h 82 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_index include/openswmm/engine/openswmm_gages.h 74 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_forcing.pyx;python/openswmm/engine/_gages.pyx;python/openswmm/engine/_spatial.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_gage_reload_rain_files include/openswmm/engine/openswmm_gages.h 446 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_rename include/openswmm/engine/openswmm_gages.h 372 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_set_data_source include/openswmm/engine/openswmm_gages.h 126 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_set_file_column include/openswmm/engine/openswmm_gages.h 180 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_set_file_format include/openswmm/engine/openswmm_gages.h 203 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_set_filename include/openswmm/engine/openswmm_gages.h 154 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_set_rain_interval include/openswmm/engine/openswmm_gages.h 117 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_set_rain_type include/openswmm/engine/openswmm_gages.h 108 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_set_rain_units include/openswmm/engine/openswmm_gages.h 221 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_set_rainfall include/openswmm/engine/openswmm_gages.h 354 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_set_scale_factor include/openswmm/engine/openswmm_gages.h 236 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_set_snow_factor include/openswmm/engine/openswmm_gages.h 212 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_set_station_id include/openswmm/engine/openswmm_gages.h 164 pyx_use python/openswmm/engine/_gages.pyx +swmm_gage_set_timeseries include/openswmm/engine/openswmm_gages.h 135 pyx_use python/openswmm/engine/_gages.pyx +swmm_get_crs include/openswmm/engine/openswmm_model.h 560 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_get_current_time include/openswmm/engine/openswmm_engine.h 413 existing_allowlist python/openswmm/engine/_solver.pyx +swmm_get_effective_threads include/openswmm/engine/openswmm_engine.h 360 gap +swmm_get_end_time include/openswmm/engine/openswmm_engine.h 412 pyx_use python/openswmm/engine/_solver.pyx +swmm_get_error_at include/openswmm/engine/openswmm_engine.h 319 pyx_use python/openswmm/engine/_solver.pyx +swmm_get_error_count include/openswmm/engine/openswmm_engine.h 318 pyx_use python/openswmm/engine/_solver.pyx +swmm_get_event_count include/openswmm/engine/openswmm_engine.h 441 pyx_use python/openswmm/engine/_solver.pyx +swmm_get_flow_units include/openswmm/engine/openswmm_engine.h 426 pyx_use python/openswmm/engine/_solver.pyx +swmm_get_last_error include/openswmm/engine/openswmm_engine.h 300 existing_allowlist +swmm_get_last_error_msg include/openswmm/engine/openswmm_engine.h 301 existing_allowlist +swmm_get_max_courant include/openswmm/engine/openswmm_massbalance.h 151 pyx_use python/openswmm/engine/_massbalance.pyx +swmm_get_quality_continuity_error include/openswmm/engine/openswmm_massbalance.h 52 pyx_use python/openswmm/engine/_massbalance.pyx +swmm_get_quality_evap_loss include/openswmm/engine/openswmm_massbalance.h 161 pyx_use python/openswmm/engine/_massbalance.pyx +swmm_get_quality_seep_loss include/openswmm/engine/openswmm_massbalance.h 158 pyx_use python/openswmm/engine/_massbalance.pyx +swmm_get_routing_continuity_error include/openswmm/engine/openswmm_massbalance.h 49 pyx_use python/openswmm/engine/_massbalance.pyx +swmm_get_routing_stats include/openswmm/engine/openswmm_massbalance.h 145 pyx_use python/openswmm/engine/_massbalance.pyx +swmm_get_routing_step include/openswmm/engine/openswmm_engine.h 414 pyx_use python/openswmm/engine/_solver.pyx +swmm_get_routing_total include/openswmm/engine/openswmm_massbalance.h 138 pyx_use python/openswmm/engine/_massbalance.pyx +swmm_get_runoff_continuity_error include/openswmm/engine/openswmm_massbalance.h 46 pyx_use python/openswmm/engine/_massbalance.pyx +swmm_get_runoff_total include/openswmm/engine/openswmm_massbalance.h 129 pyx_use python/openswmm/engine/_massbalance.pyx +swmm_get_start_time include/openswmm/engine/openswmm_engine.h 411 pyx_use python/openswmm/engine/_solver.pyx +swmm_get_steady_state_skip include/openswmm/engine/openswmm_engine.h 480 pyx_use python/openswmm/engine/_solver.pyx +swmm_get_thread_info include/openswmm/engine/openswmm_engine.h 345 gap +swmm_get_transport_matrix include/openswmm/engine/openswmm_engine.h 401 gap +swmm_get_unit_system include/openswmm/engine/openswmm_engine.h 430 pyx_use python/openswmm/engine/_solver.pyx +swmm_get_warning_at include/openswmm/engine/openswmm_engine.h 321 pyx_use python/openswmm/engine/_solver.pyx +swmm_get_warning_count include/openswmm/engine/openswmm_engine.h 320 pyx_use python/openswmm/engine/_solver.pyx +swmm_gpkg_begin include/openswmm/engine/openswmm_geopackage.h 146 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_close include/openswmm/engine/openswmm_geopackage.h 123 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_commit include/openswmm/engine/openswmm_geopackage.h 153 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_create_observed_series include/openswmm/engine/openswmm_geopackage.h 310 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_gage_count include/openswmm/engine/openswmm_geopackage.h 216 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_is_registered include/openswmm/engine/openswmm_geopackage.h 184 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_last_error include/openswmm/engine/openswmm_geopackage.h 130 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_link_count include/openswmm/engine/openswmm_geopackage.h 214 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_node_count include/openswmm/engine/openswmm_geopackage.h 213 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_observed_series_count include/openswmm/engine/openswmm_geopackage.h 370 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_observed_value_count include/openswmm/engine/openswmm_geopackage.h 378 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_open include/openswmm/engine/openswmm_geopackage.h 114 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_query_double include/openswmm/engine/openswmm_geopackage.h 418 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_query_int include/openswmm/engine/openswmm_geopackage.h 409 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_read_observed_values include/openswmm/engine/openswmm_geopackage.h 392 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_read_result_ts include/openswmm/engine/openswmm_geopackage.h 241 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_read_summary include/openswmm/engine/openswmm_geopackage.h 283 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_register include/openswmm/engine/openswmm_geopackage.h 175 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_result_ts_count include/openswmm/engine/openswmm_geopackage.h 262 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_rollback include/openswmm/engine/openswmm_geopackage.h 160 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_simulation_count include/openswmm/engine/openswmm_geopackage.h 195 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_simulation_id include/openswmm/engine/openswmm_geopackage.h 206 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_subcatch_count include/openswmm/engine/openswmm_geopackage.h 215 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_topology_edge_count include/openswmm/engine/openswmm_geopackage.h 217 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_variable_count include/openswmm/engine/openswmm_geopackage.h 218 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_write_observed_value include/openswmm/engine/openswmm_geopackage.h 328 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gpkg_write_observed_values include/openswmm/engine/openswmm_geopackage.h 354 pyx_use python/openswmm/engine/_geopackage.pyx +swmm_gw2d_get_cell include/openswmm/engine/openswmm_gw2d.h 261 gap +swmm_gw2d_get_cell_bulk include/openswmm/engine/openswmm_gw2d.h 266 gap +swmm_gw2d_get_cell_conc include/openswmm/engine/openswmm_gw2d.h 144 gap +swmm_gw2d_get_column include/openswmm/engine/openswmm_gw2d.h 273 gap +swmm_gw2d_get_continuity_error include/openswmm/engine/openswmm_gw2d.h 290 gap +swmm_gw2d_get_dimensions include/openswmm/engine/openswmm_gw2d.h 257 gap +swmm_gw2d_get_ledger include/openswmm/engine/openswmm_gw2d.h 278 gap +swmm_gw2d_get_species_ledger include/openswmm/engine/openswmm_gw2d.h 149 gap +swmm_gw2d_get_tier_histogram include/openswmm/engine/openswmm_gw2d.h 295 gap +swmm_gw2d_is_active include/openswmm/engine/openswmm_gw2d.h 253 gap +swmm_gw2d_node_add include/openswmm/engine/openswmm_gw2d.h 224 gap +swmm_gw2d_node_count include/openswmm/engine/openswmm_gw2d.h 220 gap +swmm_gw2d_node_get include/openswmm/engine/openswmm_gw2d.h 228 gap +swmm_gw2d_node_get_flags include/openswmm/engine/openswmm_gw2d.h 240 gap +swmm_gw2d_node_remove include/openswmm/engine/openswmm_gw2d.h 232 gap +swmm_gw2d_node_set_exchange include/openswmm/engine/openswmm_gw2d.h 245 gap +swmm_gw2d_option_get include/openswmm/engine/openswmm_gw2d.h 160 gap +swmm_gw2d_option_set include/openswmm/engine/openswmm_gw2d.h 165 gap +swmm_gw2d_row_add include/openswmm/engine/openswmm_gw2d.h 187 gap +swmm_gw2d_row_count include/openswmm/engine/openswmm_gw2d.h 173 gap +swmm_gw2d_row_get include/openswmm/engine/openswmm_gw2d.h 195 gap +swmm_gw2d_row_get_property include/openswmm/engine/openswmm_gw2d.h 210 gap +swmm_gw2d_row_remove include/openswmm/engine/openswmm_gw2d.h 214 gap +swmm_gw2d_row_set_property include/openswmm/engine/openswmm_gw2d.h 206 gap +swmm_gw2d_species_count include/openswmm/engine/openswmm_gw2d.h 134 gap +swmm_gw2d_species_name include/openswmm/engine/openswmm_gw2d.h 138 gap +swmm_gw_boundary_quality_count include/openswmm/engine/openswmm_gw_transport.h 152 gap +swmm_gw_boundary_quality_get include/openswmm/engine/openswmm_gw_transport.h 153 gap +swmm_gw_boundary_quality_remove include/openswmm/engine/openswmm_gw_transport.h 161 gap +swmm_gw_boundary_quality_set include/openswmm/engine/openswmm_gw_transport.h 158 gap +swmm_gw_init_quality_count include/openswmm/engine/openswmm_gw_transport.h 130 gap +swmm_gw_init_quality_file_get include/openswmm/engine/openswmm_gw_transport.h 140 gap +swmm_gw_init_quality_file_set include/openswmm/engine/openswmm_gw_transport.h 142 gap +swmm_gw_init_quality_get include/openswmm/engine/openswmm_gw_transport.h 132 gap +swmm_gw_init_quality_remove include/openswmm/engine/openswmm_gw_transport.h 138 gap +swmm_gw_init_quality_set include/openswmm/engine/openswmm_gw_transport.h 135 gap +swmm_gw_params_count include/openswmm/engine/openswmm_gw_transport.h 104 gap +swmm_gw_params_get include/openswmm/engine/openswmm_gw_transport.h 106 gap +swmm_gw_params_remove include/openswmm/engine/openswmm_gw_transport.h 111 gap +swmm_gw_params_set include/openswmm/engine/openswmm_gw_transport.h 109 gap +swmm_gw_sorption_count include/openswmm/engine/openswmm_gw_transport.h 117 gap +swmm_gw_sorption_get include/openswmm/engine/openswmm_gw_transport.h 119 gap +swmm_gw_sorption_remove include/openswmm/engine/openswmm_gw_transport.h 124 gap +swmm_gw_sorption_set include/openswmm/engine/openswmm_gw_transport.h 122 gap +swmm_gw_source_count include/openswmm/engine/openswmm_gw_transport.h 167 gap +swmm_gw_source_get include/openswmm/engine/openswmm_gw_transport.h 168 gap +swmm_gw_source_remove include/openswmm/engine/openswmm_gw_transport.h 174 gap +swmm_gw_source_set include/openswmm/engine/openswmm_gw_transport.h 172 gap +swmm_gw_source_species_count include/openswmm/engine/openswmm_gw_transport.h 177 gap +swmm_gw_source_species_get include/openswmm/engine/openswmm_gw_transport.h 178 gap +swmm_gw_source_species_remove include/openswmm/engine/openswmm_gw_transport.h 184 gap +swmm_gw_source_species_set include/openswmm/engine/openswmm_gw_transport.h 182 gap +swmm_gw_transport_authored include/openswmm/engine/openswmm_gw_transport.h 84 gap +swmm_gw_transport_option_get include/openswmm/engine/openswmm_gw_transport.h 76 gap +swmm_gw_transport_option_set include/openswmm/engine/openswmm_gw_transport.h 80 gap +swmm_gwf_function_count include/openswmm/engine/openswmm_subcatchments.h 1269 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_gwf_function_name include/openswmm/engine/openswmm_subcatchments.h 1281 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_gwf_validate_expression include/openswmm/engine/openswmm_subcatchments.h 1233 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_gwf_variable_count include/openswmm/engine/openswmm_subcatchments.h 1241 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_gwf_variable_description include/openswmm/engine/openswmm_subcatchments.h 1262 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_gwf_variable_name include/openswmm/engine/openswmm_subcatchments.h 1251 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_heat_clear_cloud include/openswmm/engine/openswmm_heat.h 233 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_clear_source_temp include/openswmm/engine/openswmm_heat.h 292 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_get_cloud include/openswmm/engine/openswmm_heat.h 208 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_get_cloud_configured include/openswmm/engine/openswmm_heat.h 204 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_get_cloud_timeseries include/openswmm/engine/openswmm_heat.h 226 gap +swmm_heat_get_current_cloud include/openswmm/engine/openswmm_heat.h 236 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_get_current_shortwave include/openswmm/engine/openswmm_heat.h 180 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_get_effective_source_temp include/openswmm/engine/openswmm_heat.h 329 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_get_enabled include/openswmm/engine/openswmm_heat.h 124 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_get_module include/openswmm/engine/openswmm_heat.h 127 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_get_node_override include/openswmm/engine/openswmm_heat.h 300 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_get_radiative include/openswmm/engine/openswmm_heat.h 136 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_get_shortwave_mode include/openswmm/engine/openswmm_heat.h 151 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_get_shortwave_timeseries include/openswmm/engine/openswmm_heat.h 174 gap +swmm_heat_get_solar include/openswmm/engine/openswmm_heat.h 186 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_get_solar_sited include/openswmm/engine/openswmm_heat.h 199 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_get_source_configured include/openswmm/engine/openswmm_heat.h 284 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_get_source_temp include/openswmm/engine/openswmm_heat.h 270 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_node_override_count include/openswmm/engine/openswmm_heat.h 296 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_remove_node_override include/openswmm/engine/openswmm_heat.h 321 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_set_cloud include/openswmm/engine/openswmm_heat.h 214 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_set_cloud_timeseries include/openswmm/engine/openswmm_heat.h 219 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_set_module include/openswmm/engine/openswmm_heat.h 130 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_set_node_override include/openswmm/engine/openswmm_heat.h 315 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_set_radiative include/openswmm/engine/openswmm_heat.h 147 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_set_shortwave_mode include/openswmm/engine/openswmm_heat.h 158 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_set_shortwave_timeseries include/openswmm/engine/openswmm_heat.h 164 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_set_solar include/openswmm/engine/openswmm_heat.h 192 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_set_source_temp include/openswmm/engine/openswmm_heat.h 278 pyx_use python/openswmm/engine/_heat.pyx +swmm_heat_source_count include/openswmm/engine/openswmm_heat.h 264 pyx_use python/openswmm/engine/_heat.pyx +swmm_hotstart_apply include/openswmm/engine/openswmm_hotstart.h 250 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_close include/openswmm/engine/openswmm_hotstart.h 397 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_get_crs include/openswmm/engine/openswmm_hotstart.h 344 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_get_sim_time include/openswmm/engine/openswmm_hotstart.h 335 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_link_count include/openswmm/engine/openswmm_hotstart.h 358 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_node_count include/openswmm/engine/openswmm_hotstart.h 351 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_open include/openswmm/engine/openswmm_hotstart.h 227 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_save include/openswmm/engine/openswmm_hotstart.h 211 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_saves_add include/openswmm/engine/openswmm_hotstart.h 175 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_saves_clear include/openswmm/engine/openswmm_hotstart.h 191 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_saves_count include/openswmm/engine/openswmm_hotstart.h 114 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_saves_get_datetime include/openswmm/engine/openswmm_hotstart.h 135 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_saves_get_path include/openswmm/engine/openswmm_hotstart.h 124 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_saves_remove include/openswmm/engine/openswmm_hotstart.h 184 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_saves_set_datetime include/openswmm/engine/openswmm_hotstart.h 163 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_saves_set_path include/openswmm/engine/openswmm_hotstart.h 152 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_set_link_depth include/openswmm/engine/openswmm_hotstart.h 306 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_set_link_flow include/openswmm/engine/openswmm_hotstart.h 293 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_set_node_depth include/openswmm/engine/openswmm_hotstart.h 267 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_set_node_head include/openswmm/engine/openswmm_hotstart.h 280 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_set_subcatch_runoff include/openswmm/engine/openswmm_hotstart.h 319 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_warning include/openswmm/engine/openswmm_hotstart.h 382 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hotstart_warning_count include/openswmm/engine/openswmm_hotstart.h 373 pyx_use python/openswmm/engine/_hotstart.pyx +swmm_hydrograph_add include/openswmm/engine/openswmm_inflows.h 274 pyx_use python/openswmm/engine/_inflows.pyx +swmm_hydrograph_add_gage include/openswmm/engine/openswmm_inflows.h 316 pyx_use python/openswmm/engine/_inflows.pyx +swmm_hydrograph_analyze_impact include/openswmm/engine/openswmm_edit.h 202 pyx_use python/openswmm/engine/_edit.pyx +swmm_hydrograph_clear_group_months include/openswmm/engine/openswmm_inflows.h 462 pyx_use python/openswmm/engine/_inflows.pyx +swmm_hydrograph_count include/openswmm/engine/openswmm_inflows.h 306 pyx_use python/openswmm/engine/_inflows.pyx +swmm_hydrograph_delete include/openswmm/engine/openswmm_edit.h 395 pyx_use python/openswmm/engine/_edit.pyx +swmm_hydrograph_gage_count include/openswmm/engine/openswmm_inflows.h 339 pyx_use python/openswmm/engine/_inflows.pyx +swmm_hydrograph_get include/openswmm/engine/openswmm_inflows.h 296 pyx_use python/openswmm/engine/_inflows.pyx +swmm_hydrograph_get_gage include/openswmm/engine/openswmm_inflows.h 331 pyx_use python/openswmm/engine/_inflows.pyx +swmm_hydrograph_group_count include/openswmm/engine/openswmm_inflows.h 353 pyx_use python/openswmm/engine/_inflows.pyx +swmm_hydrograph_group_id include/openswmm/engine/openswmm_inflows.h 368 pyx_use python/openswmm/engine/_inflows.pyx +swmm_hydrograph_group_rename include/openswmm/engine/openswmm_inflows.h 486 pyx_use python/openswmm/engine/_inflows.pyx +swmm_hydrograph_remove_entry include/openswmm/engine/openswmm_inflows.h 441 pyx_use python/openswmm/engine/_inflows.pyx +swmm_hydrograph_remove_group include/openswmm/engine/openswmm_inflows.h 450 pyx_use python/openswmm/engine/_inflows.pyx +swmm_hydrograph_set_gage include/openswmm/engine/openswmm_inflows.h 473 pyx_use python/openswmm/engine/_inflows.pyx +swmm_hydrograph_set_ia include/openswmm/engine/openswmm_inflows.h 428 pyx_use python/openswmm/engine/_inflows.pyx +swmm_hydrograph_set_rtk include/openswmm/engine/openswmm_inflows.h 412 pyx_use python/openswmm/engine/_inflows.pyx +swmm_impact_report_free include/openswmm/engine/openswmm_edit.h 120 pyx_use python/openswmm/engine/_edit.pyx +swmm_infil2d_defaults_count include/openswmm/engine/openswmm_infil2d.h 220 pyx_use python/openswmm/engine/_2d.pyx +swmm_infil2d_get_cell include/openswmm/engine/openswmm_infil2d.h 320 pyx_use python/openswmm/engine/_2d.pyx +swmm_infil2d_get_cum_bulk include/openswmm/engine/openswmm_infil2d.h 412 pyx_use python/openswmm/engine/_2d.pyx +swmm_infil2d_get_default include/openswmm/engine/openswmm_infil2d.h 234 pyx_use python/openswmm/engine/_2d.pyx +swmm_infil2d_get_default_tag include/openswmm/engine/openswmm_infil2d.h 249 pyx_use python/openswmm/engine/_2d.pyx +swmm_infil2d_get_options include/openswmm/engine/openswmm_infil2d.h 178 pyx_use python/openswmm/engine/_2d.pyx +swmm_infil2d_get_rate_bulk include/openswmm/engine/openswmm_infil2d.h 392 pyx_use python/openswmm/engine/_2d.pyx +swmm_infil2d_get_total_volume include/openswmm/engine/openswmm_infil2d.h 428 pyx_use python/openswmm/engine/_2d.pyx +swmm_infil2d_remove_default include/openswmm/engine/openswmm_infil2d.h 290 pyx_use python/openswmm/engine/_2d.pyx +swmm_infil2d_set_cell include/openswmm/engine/openswmm_infil2d.h 343 pyx_use python/openswmm/engine/_2d.pyx +swmm_infil2d_set_cells include/openswmm/engine/openswmm_infil2d.h 368 pyx_use python/openswmm/engine/_2d.pyx +swmm_infil2d_set_default include/openswmm/engine/openswmm_infil2d.h 273 pyx_use python/openswmm/engine/_2d.pyx +swmm_infil2d_set_options include/openswmm/engine/openswmm_infil2d.h 203 pyx_use python/openswmm/engine/_2d.pyx +swmm_init_quality_count include/openswmm/engine/openswmm_initial_quality.h 59 pyx_use python/openswmm/engine/_initial_quality.pyx +swmm_init_quality_file_get include/openswmm/engine/openswmm_initial_quality.h 114 gap +swmm_init_quality_file_set include/openswmm/engine/openswmm_initial_quality.h 117 gap +swmm_init_quality_get include/openswmm/engine/openswmm_initial_quality.h 72 pyx_use python/openswmm/engine/_initial_quality.pyx +swmm_init_quality_is_file include/openswmm/engine/openswmm_initial_quality.h 120 gap +swmm_init_quality_remove include/openswmm/engine/openswmm_initial_quality.h 102 pyx_use python/openswmm/engine/_initial_quality.pyx +swmm_init_quality_set include/openswmm/engine/openswmm_initial_quality.h 91 pyx_use python/openswmm/engine/_initial_quality.pyx +swmm_inlet_add include/openswmm/engine/openswmm_infrastructure.h 407 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_analyze_impact include/openswmm/engine/openswmm_edit.h 182 pyx_use python/openswmm/engine/_edit.pyx +swmm_inlet_count include/openswmm/engine/openswmm_infrastructure.h 443 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_delete include/openswmm/engine/openswmm_edit.h 375 pyx_use python/openswmm/engine/_edit.pyx +swmm_inlet_get_comment include/openswmm/engine/openswmm_infrastructure.h 599 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_get_design include/openswmm/engine/openswmm_infrastructure.h 578 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_get_params include/openswmm/engine/openswmm_infrastructure.h 481 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_get_type include/openswmm/engine/openswmm_infrastructure.h 494 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_id include/openswmm/engine/openswmm_infrastructure.h 459 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_index include/openswmm/engine/openswmm_infrastructure.h 451 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_infrastructure.pyx +swmm_inlet_junction_fuse include/openswmm/engine/openswmm_edit.h 555 pyx_use python/openswmm/engine/_edit.pyx +swmm_inlet_rename include/openswmm/engine/openswmm_infrastructure.h 422 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_set_comment include/openswmm/engine/openswmm_infrastructure.h 600 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_set_design include/openswmm/engine/openswmm_infrastructure.h 594 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_set_params include/openswmm/engine/openswmm_infrastructure.h 435 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_usage_count include/openswmm/engine/openswmm_infrastructure.h 627 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_usage_find_link include/openswmm/engine/openswmm_infrastructure.h 630 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_usage_find_node include/openswmm/engine/openswmm_infrastructure.h 633 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_usage_get include/openswmm/engine/openswmm_infrastructure.h 639 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_usage_remove include/openswmm/engine/openswmm_infrastructure.h 662 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_inlet_usage_set include/openswmm/engine/openswmm_infrastructure.h 655 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_is_between_events include/openswmm/engine/openswmm_engine.h 437 pyx_use python/openswmm/engine/_solver.pyx +swmm_landuse_add include/openswmm/engine/openswmm_quality.h 56 pyx_use python/openswmm/engine/_quality.pyx +swmm_landuse_analyze_impact include/openswmm/engine/openswmm_edit.h 186 pyx_use python/openswmm/engine/_edit.pyx +swmm_landuse_count include/openswmm/engine/openswmm_quality.h 47 pyx_use python/openswmm/engine/_quality.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_landuse_delete include/openswmm/engine/openswmm_edit.h 382 pyx_use python/openswmm/engine/_edit.pyx +swmm_landuse_get_sweep_interval include/openswmm/engine/openswmm_quality.h 76 pyx_use python/openswmm/engine/_quality.pyx +swmm_landuse_get_sweep_removal include/openswmm/engine/openswmm_quality.h 82 pyx_use python/openswmm/engine/_quality.pyx +swmm_landuse_id include/openswmm/engine/openswmm_quality.h 53 pyx_use python/openswmm/engine/_quality.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_landuse_index include/openswmm/engine/openswmm_quality.h 50 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_quality.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_landuse_rename include/openswmm/engine/openswmm_quality.h 70 pyx_use python/openswmm/engine/_quality.pyx +swmm_landuse_set_sweep_interval include/openswmm/engine/openswmm_quality.h 73 pyx_use python/openswmm/engine/_quality.pyx +swmm_landuse_set_sweep_removal include/openswmm/engine/openswmm_quality.h 79 pyx_use python/openswmm/engine/_quality.pyx +swmm_lid_add include/openswmm/engine/openswmm_infrastructure.h 680 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_analyze_impact include/openswmm/engine/openswmm_edit.h 174 pyx_use python/openswmm/engine/_edit.pyx +swmm_lid_count include/openswmm/engine/openswmm_infrastructure.h 789 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_delete include/openswmm/engine/openswmm_edit.h 361 pyx_use python/openswmm/engine/_edit.pyx +swmm_lid_get_drain include/openswmm/engine/openswmm_infrastructure.h 831 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_get_drainmat include/openswmm/engine/openswmm_infrastructure.h 838 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_get_pavement include/openswmm/engine/openswmm_infrastructure.h 834 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_get_soil include/openswmm/engine/openswmm_infrastructure.h 825 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_get_storage include/openswmm/engine/openswmm_infrastructure.h 828 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_get_surface include/openswmm/engine/openswmm_infrastructure.h 822 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_get_type include/openswmm/engine/openswmm_infrastructure.h 815 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_id include/openswmm/engine/openswmm_infrastructure.h 805 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_index include/openswmm/engine/openswmm_infrastructure.h 797 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_infrastructure.pyx +swmm_lid_rename include/openswmm/engine/openswmm_infrastructure.h 698 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_set_drain include/openswmm/engine/openswmm_infrastructure.h 745 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_set_drainmat include/openswmm/engine/openswmm_infrastructure.h 782 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_set_pavement include/openswmm/engine/openswmm_infrastructure.h 765 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_set_soil include/openswmm/engine/openswmm_infrastructure.h 723 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_set_storage include/openswmm/engine/openswmm_infrastructure.h 734 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_set_surface include/openswmm/engine/openswmm_infrastructure.h 709 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_usage_add include/openswmm/engine/openswmm_infrastructure.h 860 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_usage_count include/openswmm/engine/openswmm_infrastructure.h 869 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_usage_get include/openswmm/engine/openswmm_infrastructure.h 886 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_lid_usage_remove include/openswmm/engine/openswmm_infrastructure.h 896 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_link_add include/openswmm/engine/openswmm_links.h 152 pyx_use python/openswmm/engine/_links.pyx;python/openswmm/engine/_model.pyx +swmm_link_analyze_impact include/openswmm/engine/openswmm_edit.h 138 pyx_use python/openswmm/engine/_edit.pyx +swmm_link_convert include/openswmm/engine/openswmm_edit.h 465 pyx_use python/openswmm/engine/_edit.pyx +swmm_link_count include/openswmm/engine/openswmm_links.h 114 pyx_use python/openswmm/engine/_controls.pyx;python/openswmm/engine/_edit.pyx;python/openswmm/engine/_forcing.pyx;python/openswmm/engine/_initial_quality.pyx;python/openswmm/engine/_links.pyx;python/openswmm/engine/_pollutants.pyx;python/openswmm/engine/_spatial.pyx;python/openswmm/engine/_statistics.pyx +swmm_link_create_xsect include/openswmm/engine/openswmm_xsect.h 231 pyx_use python/openswmm/engine/_xsect.pyx +swmm_link_delete include/openswmm/engine/openswmm_edit.h 235 pyx_use python/openswmm/engine/_edit.pyx +swmm_link_delete_many include/openswmm/engine/openswmm_edit.h 287 gap +swmm_link_get_barrels include/openswmm/engine/openswmm_links.h 892 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_capacities_bulk include/openswmm/engine/openswmm_links.h 1074 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_capacity include/openswmm/engine/openswmm_links.h 620 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_closed include/openswmm/engine/openswmm_links.h 681 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_control_setting include/openswmm/engine/openswmm_links.h 655 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_control_settings_bulk include/openswmm/engine/openswmm_links.h 1088 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_crest_height include/openswmm/engine/openswmm_links.h 747 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_culvert_code include/openswmm/engine/openswmm_links.h 874 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_depth include/openswmm/engine/openswmm_links.h 602 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_depths_bulk include/openswmm/engine/openswmm_links.h 1020 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_discharge_coeff include/openswmm/engine/openswmm_links.h 765 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_end_contractions include/openswmm/engine/openswmm_links.h 787 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_flap_gate include/openswmm/engine/openswmm_links.h 834 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_flow include/openswmm/engine/openswmm_links.h 584 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_flows_bulk include/openswmm/engine/openswmm_links.h 1011 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_from_node include/openswmm/engine/openswmm_links.h 195 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_hyd_power include/openswmm/engine/openswmm_links.h 1213 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_hyd_powers_bulk include/openswmm/engine/openswmm_links.h 1106 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_ids_bulk include/openswmm/engine/openswmm_links.h 1128 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_initial_flow include/openswmm/engine/openswmm_links.h 293 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_length include/openswmm/engine/openswmm_links.h 562 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_loss_coeff include/openswmm/engine/openswmm_links.h 813 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_max_flow include/openswmm/engine/openswmm_links.h 307 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_offset_dn include/openswmm/engine/openswmm_links.h 919 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_offset_up include/openswmm/engine/openswmm_links.h 910 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_orifice_open_close_rate include/openswmm/engine/openswmm_links.h 502 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_orifice_type include/openswmm/engine/openswmm_links.h 347 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_outlet_expon include/openswmm/engine/openswmm_links.h 461 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_outlet_rating_type include/openswmm/engine/openswmm_links.h 439 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_pump_curve include/openswmm/engine/openswmm_links.h 707 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_pump_init_state include/openswmm/engine/openswmm_links.h 725 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_pump_shutoff_depth include/openswmm/engine/openswmm_links.h 485 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_pump_startup_depth include/openswmm/engine/openswmm_links.h 473 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_pump_stats_bulk include/openswmm/engine/openswmm_links.h 1202 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_quality include/openswmm/engine/openswmm_links.h 997 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_quality_bulk include/openswmm/engine/openswmm_links.h 1039 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_roughness include/openswmm/engine/openswmm_links.h 571 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_seep_rate include/openswmm/engine/openswmm_links.h 852 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_slope include/openswmm/engine/openswmm_links.h 901 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_slot_volume include/openswmm/engine/openswmm_links.h 642 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_stat_max_filling include/openswmm/engine/openswmm_links.h 950 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_stat_max_flow include/openswmm/engine/openswmm_links.h 932 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_stat_max_velocity include/openswmm/engine/openswmm_links.h 941 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_stat_peak_slot_share include/openswmm/engine/openswmm_links.h 957 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_stat_pump_cycles include/openswmm/engine/openswmm_links.h 1138 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_stat_pump_on_time include/openswmm/engine/openswmm_links.h 1141 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_stat_pump_volume include/openswmm/engine/openswmm_links.h 1144 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_stat_slot_share include/openswmm/engine/openswmm_links.h 965 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_stat_surcharge_time include/openswmm/engine/openswmm_links.h 983 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_stat_vol_flow include/openswmm/engine/openswmm_links.h 974 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_tag include/openswmm/engine/openswmm_links.h 1225 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_target_setting include/openswmm/engine/openswmm_links.h 669 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_target_settings_bulk include/openswmm/engine/openswmm_links.h 1095 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_to_node include/openswmm/engine/openswmm_links.h 204 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_type include/openswmm/engine/openswmm_links.h 553 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_velocities_bulk include/openswmm/engine/openswmm_links.h 1066 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_velocity include/openswmm/engine/openswmm_links.h 611 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_volume include/openswmm/engine/openswmm_links.h 629 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_volumes_bulk include/openswmm/engine/openswmm_links.h 1081 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_weir_type include/openswmm/engine/openswmm_links.h 397 pyx_use python/openswmm/engine/_links.pyx +swmm_link_get_xsect include/openswmm/engine/openswmm_links.h 538 pyx_use python/openswmm/engine/_links.pyx +swmm_link_id include/openswmm/engine/openswmm_links.h 130 pyx_use python/openswmm/engine/_links.pyx +swmm_link_index include/openswmm/engine/openswmm_links.h 122 pyx_use python/openswmm/engine/_controls.pyx;python/openswmm/engine/_edit.pyx;python/openswmm/engine/_forcing.pyx;python/openswmm/engine/_initial_quality.pyx;python/openswmm/engine/_links.pyx;python/openswmm/engine/_pollutants.pyx;python/openswmm/engine/_spatial.pyx +swmm_link_pop_last include/openswmm/engine/openswmm_links.h 171 pyx_use python/openswmm/engine/_links.pyx;python/openswmm/engine/_model.pyx +swmm_link_rename include/openswmm/engine/openswmm_links.h 1218 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_barrels include/openswmm/engine/openswmm_links.h 883 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_closed include/openswmm/engine/openswmm_links.h 678 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_control_setting include/openswmm/engine/openswmm_links.h 652 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_crest_height include/openswmm/engine/openswmm_links.h 738 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_culvert_code include/openswmm/engine/openswmm_links.h 865 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_discharge_coeff include/openswmm/engine/openswmm_links.h 756 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_end_contractions include/openswmm/engine/openswmm_links.h 778 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_flap_gate include/openswmm/engine/openswmm_links.h 825 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_flow include/openswmm/engine/openswmm_links.h 593 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_flows_bulk include/openswmm/engine/openswmm_links.h 1029 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_initial_flow include/openswmm/engine/openswmm_links.h 269 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_length include/openswmm/engine/openswmm_links.h 233 pyx_use python/openswmm/engine/_links.pyx;python/openswmm/engine/_model.pyx +swmm_link_set_loss_coeff include/openswmm/engine/openswmm_links.h 802 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_max_flow include/openswmm/engine/openswmm_links.h 278 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_nodes include/openswmm/engine/openswmm_links.h 185 pyx_use python/openswmm/engine/_links.pyx;python/openswmm/engine/_model.pyx +swmm_link_set_offset_dn include/openswmm/engine/openswmm_links.h 260 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_offset_up include/openswmm/engine/openswmm_links.h 251 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_orifice_open_close_rate include/openswmm/engine/openswmm_links.h 499 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_orifice_type include/openswmm/engine/openswmm_links.h 335 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_outlet_expon include/openswmm/engine/openswmm_links.h 453 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_outlet_rating_type include/openswmm/engine/openswmm_links.h 431 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_pump_curve include/openswmm/engine/openswmm_links.h 698 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_pump_init_state include/openswmm/engine/openswmm_links.h 716 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_pump_shutoff_depth include/openswmm/engine/openswmm_links.h 482 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_pump_startup_depth include/openswmm/engine/openswmm_links.h 470 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_quality include/openswmm/engine/openswmm_pollutants.h 319 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_link_set_roughness include/openswmm/engine/openswmm_links.h 242 pyx_use python/openswmm/engine/_links.pyx;python/openswmm/engine/_model.pyx +swmm_link_set_seep_rate include/openswmm/engine/openswmm_links.h 843 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_tag include/openswmm/engine/openswmm_links.h 1229 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_target_setting include/openswmm/engine/openswmm_links.h 666 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_weir_type include/openswmm/engine/openswmm_links.h 385 pyx_use python/openswmm/engine/_links.pyx +swmm_link_set_xsect include/openswmm/engine/openswmm_links.h 523 pyx_use python/openswmm/engine/_links.pyx;python/openswmm/engine/_model.pyx +swmm_links_restore_authored_orientation include/openswmm/engine/openswmm_links.h 220 gap +swmm_model_write include/openswmm/engine/openswmm_model.h 109 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_model_write_compat include/openswmm/engine/openswmm_model.h 111 gap +swmm_model_write_with_plugin include/openswmm/engine/openswmm_model.h 190 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_node_add include/openswmm/engine/openswmm_nodes.h 105 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_nodes.pyx +swmm_node_analyze_impact include/openswmm/engine/openswmm_edit.h 134 pyx_use python/openswmm/engine/_edit.pyx +swmm_node_convert include/openswmm/engine/openswmm_edit.h 447 pyx_use python/openswmm/engine/_edit.pyx +swmm_node_count include/openswmm/engine/openswmm_nodes.h 64 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_forcing.pyx;python/openswmm/engine/_heat.pyx;python/openswmm/engine/_inflows.pyx;python/openswmm/engine/_initial_quality.pyx;python/openswmm/engine/_links.pyx;python/openswmm/engine/_nodes.pyx;python/openswmm/engine/_pollutants.pyx;python/openswmm/engine/_quality.pyx;python/openswmm/engine/_spatial.pyx;python/openswmm/engine/_statistics.pyx;python/openswmm/engine/_subcatchments.pyx;python/openswmm/engine/_water_age.pyx +swmm_node_delete include/openswmm/engine/openswmm_edit.h 225 pyx_use python/openswmm/engine/_edit.pyx +swmm_node_delete_many include/openswmm/engine/openswmm_edit.h 282 gap +swmm_node_get_crown_elev include/openswmm/engine/openswmm_nodes.h 831 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_degree include/openswmm/engine/openswmm_nodes.h 867 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_depth include/openswmm/engine/openswmm_nodes.h 348 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_depth_from_volume include/openswmm/engine/openswmm_nodes.h 1095 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_depths_bulk include/openswmm/engine/openswmm_nodes.h 920 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_divider_type include/openswmm/engine/openswmm_nodes.h 791 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_exfil_params include/openswmm/engine/openswmm_nodes.h 642 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_full_volume include/openswmm/engine/openswmm_nodes.h 840 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_head include/openswmm/engine/openswmm_nodes.h 366 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_heads_bulk include/openswmm/engine/openswmm_nodes.h 929 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_ids_bulk include/openswmm/engine/openswmm_nodes.h 1075 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_inflow include/openswmm/engine/openswmm_nodes.h 411 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_inflows_bulk include/openswmm/engine/openswmm_nodes.h 938 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_initial_depth include/openswmm/engine/openswmm_nodes.h 822 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_invert_elev include/openswmm/engine/openswmm_nodes.h 312 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_lateral_inflow include/openswmm/engine/openswmm_nodes.h 389 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_lateral_inflows_bulk include/openswmm/engine/openswmm_nodes.h 1034 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_losses include/openswmm/engine/openswmm_nodes.h 849 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_losses_bulk include/openswmm/engine/openswmm_nodes.h 1020 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_max_depth include/openswmm/engine/openswmm_nodes.h 321 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_outfall_flap_gate include/openswmm/engine/openswmm_nodes.h 754 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_outfall_param include/openswmm/engine/openswmm_nodes.h 703 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_outfall_route_to include/openswmm/engine/openswmm_nodes.h 1088 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_outfall_tidal include/openswmm/engine/openswmm_nodes.h 719 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_outfall_timeseries include/openswmm/engine/openswmm_nodes.h 733 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_outfall_type include/openswmm/engine/openswmm_nodes.h 667 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_outflow include/openswmm/engine/openswmm_nodes.h 858 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_outflows_bulk include/openswmm/engine/openswmm_nodes.h 1007 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_overflow include/openswmm/engine/openswmm_nodes.h 402 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_overflows_bulk include/openswmm/engine/openswmm_nodes.h 947 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_ponded_area include/openswmm/engine/openswmm_nodes.h 813 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_quality include/openswmm/engine/openswmm_nodes.h 473 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_quality_bulk include/openswmm/engine/openswmm_nodes.h 975 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_rim_depth include/openswmm/engine/openswmm_nodes.h 335 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_stat_max_depth include/openswmm/engine/openswmm_nodes.h 880 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_stat_max_overflow include/openswmm/engine/openswmm_nodes.h 889 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_stat_time_flooded include/openswmm/engine/openswmm_nodes.h 907 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_stat_vol_flooded include/openswmm/engine/openswmm_nodes.h 898 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_storage_curve include/openswmm/engine/openswmm_nodes.h 496 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_storage_functional include/openswmm/engine/openswmm_nodes.h 522 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_storage_geometry include/openswmm/engine/openswmm_nodes.h 597 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_storage_seep_rate include/openswmm/engine/openswmm_nodes.h 616 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_storage_shape include/openswmm/engine/openswmm_nodes.h 564 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_surcharge_depth include/openswmm/engine/openswmm_nodes.h 804 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_tag include/openswmm/engine/openswmm_nodes.h 1110 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_type include/openswmm/engine/openswmm_nodes.h 215 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_volume include/openswmm/engine/openswmm_nodes.h 375 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_get_volumes_bulk include/openswmm/engine/openswmm_nodes.h 994 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_id include/openswmm/engine/openswmm_nodes.h 80 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_index include/openswmm/engine/openswmm_nodes.h 72 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_forcing.pyx;python/openswmm/engine/_heat.pyx;python/openswmm/engine/_inflows.pyx;python/openswmm/engine/_initial_quality.pyx;python/openswmm/engine/_links.pyx;python/openswmm/engine/_nodes.pyx;python/openswmm/engine/_pollutants.pyx;python/openswmm/engine/_quality.pyx;python/openswmm/engine/_spatial.pyx;python/openswmm/engine/_subcatchments.pyx;python/openswmm/engine/_water_age.pyx +swmm_node_inlet_eligible include/openswmm/engine/openswmm_nodes.h 293 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_is_inlet include/openswmm/engine/openswmm_nodes.h 285 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_is_virtual include/openswmm/engine/openswmm_nodes.h 230 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_pop_last include/openswmm/engine/openswmm_nodes.h 128 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_nodes.pyx +swmm_node_rename include/openswmm/engine/openswmm_nodes.h 1101 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_depth include/openswmm/engine/openswmm_nodes.h 357 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_depths_bulk include/openswmm/engine/openswmm_nodes.h 956 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_divider_type include/openswmm/engine/openswmm_nodes.h 782 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_exfil_params include/openswmm/engine/openswmm_nodes.h 631 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_head_boundary include/openswmm/engine/openswmm_nodes.h 459 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_initial_depth include/openswmm/engine/openswmm_nodes.h 181 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_inlet include/openswmm/engine/openswmm_nodes.h 303 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_nodes.pyx +swmm_node_set_invert_elev include/openswmm/engine/openswmm_nodes.h 141 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_nodes.pyx +swmm_node_set_lat_inflows_bulk include/openswmm/engine/openswmm_nodes.h 965 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_lateral_inflow include/openswmm/engine/openswmm_nodes.h 429 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_max_depth include/openswmm/engine/openswmm_nodes.h 150 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_nodes.pyx +swmm_node_set_outfall_flap_gate include/openswmm/engine/openswmm_nodes.h 745 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_outfall_route_to include/openswmm/engine/openswmm_nodes.h 1085 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_outfall_stage include/openswmm/engine/openswmm_nodes.h 676 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_outfall_tidal include/openswmm/engine/openswmm_nodes.h 685 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_outfall_timeseries include/openswmm/engine/openswmm_nodes.h 694 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_outfall_type include/openswmm/engine/openswmm_nodes.h 658 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_pond_area include/openswmm/engine/openswmm_nodes.h 172 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_quality include/openswmm/engine/openswmm_pollutants.h 309 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_node_set_quality_mass_flux include/openswmm/engine/openswmm_nodes.h 445 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_rim_depth include/openswmm/engine/openswmm_nodes.h 202 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_storage_curve include/openswmm/engine/openswmm_nodes.h 487 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_storage_functional include/openswmm/engine/openswmm_nodes.h 511 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_storage_geometry include/openswmm/engine/openswmm_nodes.h 585 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_storage_seep_rate include/openswmm/engine/openswmm_nodes.h 607 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_storage_shape include/openswmm/engine/openswmm_nodes.h 555 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_surcharge_depth include/openswmm/engine/openswmm_nodes.h 159 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_tag include/openswmm/engine/openswmm_nodes.h 1116 pyx_use python/openswmm/engine/_nodes.pyx +swmm_node_set_virtual include/openswmm/engine/openswmm_nodes.h 252 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_nodes.pyx +swmm_node_virtual_eligible include/openswmm/engine/openswmm_nodes.h 271 pyx_use python/openswmm/engine/_nodes.pyx +swmm_options_get include/openswmm/engine/openswmm_model.h 501 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_options_get_end_date include/openswmm/engine/openswmm_model.h 591 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_options_get_ext include/openswmm/engine/openswmm_model.h 531 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_options_get_report_start include/openswmm/engine/openswmm_model.h 607 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_options_get_start_date include/openswmm/engine/openswmm_model.h 575 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_options_set include/openswmm/engine/openswmm_model.h 516 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_options_set_end_date include/openswmm/engine/openswmm_model.h 599 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_options_set_ext include/openswmm/engine/openswmm_model.h 546 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_options_set_report_start include/openswmm/engine/openswmm_model.h 615 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_options_set_start_date include/openswmm/engine/openswmm_model.h 583 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_output_close include/openswmm/engine/openswmm_output.h 203 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_error_code include/openswmm/engine/openswmm_output.h 618 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_flow_units include/openswmm/engine/openswmm_output.h 222 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_link_attribute include/openswmm/engine/openswmm_output.h 510 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_link_count include/openswmm/engine/openswmm_output.h 243 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_link_id include/openswmm/engine/openswmm_output.h 310 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_link_result include/openswmm/engine/openswmm_output.h 371 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_link_series include/openswmm/engine/openswmm_output.h 441 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_node_attribute include/openswmm/engine/openswmm_output.h 494 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_node_count include/openswmm/engine/openswmm_output.h 236 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_node_id include/openswmm/engine/openswmm_output.h 301 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_node_result include/openswmm/engine/openswmm_output.h 357 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_node_series include/openswmm/engine/openswmm_output.h 423 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_node_stat_max_depth include/openswmm/engine/openswmm_output.h 569 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_node_stat_max_overflow include/openswmm/engine/openswmm_output.h 580 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_node_stat_time_flooded include/openswmm/engine/openswmm_output.h 605 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_node_stat_vol_flooded include/openswmm/engine/openswmm_output.h 592 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_period_count include/openswmm/engine/openswmm_output.h 257 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_period_time include/openswmm/engine/openswmm_output.h 533 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_pollut_count include/openswmm/engine/openswmm_output.h 250 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_pollut_id include/openswmm/engine/openswmm_output.h 327 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_report_step include/openswmm/engine/openswmm_output.h 280 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_start_date include/openswmm/engine/openswmm_output.h 272 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_subcatch_attribute include/openswmm/engine/openswmm_output.h 478 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_subcatch_count include/openswmm/engine/openswmm_output.h 229 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_subcatch_id include/openswmm/engine/openswmm_output.h 292 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_subcatch_result include/openswmm/engine/openswmm_output.h 343 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_subcatch_series include/openswmm/engine/openswmm_output.h 405 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_system_result include/openswmm/engine/openswmm_output.h 385 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_system_series include/openswmm/engine/openswmm_output.h 458 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_get_version include/openswmm/engine/openswmm_output.h 214 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_is_live include/openswmm/engine/openswmm_output.h 196 gap +swmm_output_open include/openswmm/engine/openswmm_output.h 160 pyx_use python/openswmm/engine/_output_reader.pyx +swmm_output_open_live include/openswmm/engine/openswmm_output.h 176 gap +swmm_output_refresh include/openswmm/engine/openswmm_output.h 190 gap +swmm_pattern_add include/openswmm/engine/openswmm_tables.h 235 pyx_use python/openswmm/engine/_tables.pyx +swmm_pattern_analyze_impact include/openswmm/engine/openswmm_edit.h 162 pyx_use python/openswmm/engine/_edit.pyx +swmm_pattern_count include/openswmm/engine/openswmm_tables.h 252 pyx_use python/openswmm/engine/_tables.pyx +swmm_pattern_delete include/openswmm/engine/openswmm_edit.h 340 pyx_use python/openswmm/engine/_edit.pyx +swmm_pattern_get_factor include/openswmm/engine/openswmm_tables.h 301 pyx_use python/openswmm/engine/_tables.pyx +swmm_pattern_get_factor_count include/openswmm/engine/openswmm_tables.h 291 pyx_use python/openswmm/engine/_tables.pyx +swmm_pattern_get_type include/openswmm/engine/openswmm_tables.h 282 pyx_use python/openswmm/engine/_tables.pyx +swmm_pattern_id include/openswmm/engine/openswmm_tables.h 268 pyx_use python/openswmm/engine/_tables.pyx +swmm_pattern_index include/openswmm/engine/openswmm_tables.h 260 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_tables.pyx +swmm_pattern_remove include/openswmm/engine/openswmm_tables.h 322 pyx_use python/openswmm/engine/_tables.pyx +swmm_pattern_rename include/openswmm/engine/openswmm_tables.h 335 pyx_use python/openswmm/engine/_tables.pyx +swmm_pattern_set_factors include/openswmm/engine/openswmm_tables.h 245 pyx_use python/openswmm/engine/_tables.pyx +swmm_plugin_get include/openswmm/engine/openswmm_model.h 233 pyx_use python/openswmm/engine/_model.pyx +swmm_plugin_remove include/openswmm/engine/openswmm_model.h 270 pyx_use python/openswmm/engine/_model.pyx +swmm_plugin_set include/openswmm/engine/openswmm_model.h 256 pyx_use python/openswmm/engine/_model.pyx +swmm_plugins_count include/openswmm/engine/openswmm_model.h 213 pyx_use python/openswmm/engine/_model.pyx +swmm_pollutant_add include/openswmm/engine/openswmm_pollutants.h 79 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_analyze_impact include/openswmm/engine/openswmm_edit.h 158 pyx_use python/openswmm/engine/_edit.pyx +swmm_pollutant_count include/openswmm/engine/openswmm_pollutants.h 50 pyx_use python/openswmm/engine/_forcing.pyx;python/openswmm/engine/_links.pyx;python/openswmm/engine/_massbalance.pyx;python/openswmm/engine/_nodes.pyx;python/openswmm/engine/_pollutants.pyx;python/openswmm/engine/_quality.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_pollutant_delete include/openswmm/engine/openswmm_edit.h 329 pyx_use python/openswmm/engine/_edit.pyx +swmm_pollutant_get_co_pollutant include/openswmm/engine/openswmm_pollutants.h 277 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_get_dwf_conc include/openswmm/engine/openswmm_pollutants.h 239 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_get_gw_conc include/openswmm/engine/openswmm_pollutants.h 192 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_get_init_conc include/openswmm/engine/openswmm_pollutants.h 201 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_get_kdecay include/openswmm/engine/openswmm_pollutants.h 174 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_get_mwt include/openswmm/engine/openswmm_pollutants.h 257 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_get_rain_conc include/openswmm/engine/openswmm_pollutants.h 183 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_get_rdii_conc include/openswmm/engine/openswmm_pollutants.h 219 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_get_snow_only include/openswmm/engine/openswmm_pollutants.h 295 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_get_units include/openswmm/engine/openswmm_pollutants.h 161 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_id include/openswmm/engine/openswmm_pollutants.h 66 pyx_use python/openswmm/engine/_pollutants.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_pollutant_index include/openswmm/engine/openswmm_pollutants.h 58 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_forcing.pyx;python/openswmm/engine/_links.pyx;python/openswmm/engine/_massbalance.pyx;python/openswmm/engine/_nodes.pyx;python/openswmm/engine/_pollutants.pyx;python/openswmm/engine/_quality.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_pollutant_rename include/openswmm/engine/openswmm_pollutants.h 94 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_set_co_pollutant include/openswmm/engine/openswmm_pollutants.h 267 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_set_dwf_conc include/openswmm/engine/openswmm_pollutants.h 230 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_set_gw_conc include/openswmm/engine/openswmm_pollutants.h 143 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_set_init_conc include/openswmm/engine/openswmm_pollutants.h 152 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_set_kdecay include/openswmm/engine/openswmm_pollutants.h 125 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_set_mwt include/openswmm/engine/openswmm_pollutants.h 248 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_set_rain_conc include/openswmm/engine/openswmm_pollutants.h 134 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_set_rdii_conc include/openswmm/engine/openswmm_pollutants.h 210 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_set_snow_only include/openswmm/engine/openswmm_pollutants.h 286 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_pollutant_set_units include/openswmm/engine/openswmm_pollutants.h 116 pyx_use python/openswmm/engine/_pollutants.pyx +swmm_process_component_count include/openswmm/engine/openswmm_process_components.h 46 pyx_use python/openswmm/engine/_process_components.pyx +swmm_process_component_find include/openswmm/engine/openswmm_process_components.h 60 pyx_use python/openswmm/engine/_process_components.pyx +swmm_process_component_get include/openswmm/engine/openswmm_process_components.h 55 pyx_use python/openswmm/engine/_process_components.pyx +swmm_process_component_known_count include/openswmm/engine/openswmm_process_components.h 83 gap +swmm_process_component_known_get include/openswmm/engine/openswmm_process_components.h 93 gap +swmm_process_component_register include/openswmm/engine/openswmm_process_components.h 67 pyx_use python/openswmm/engine/_process_components.pyx +swmm_process_component_remove include/openswmm/engine/openswmm_process_components.h 71 pyx_use python/openswmm/engine/_process_components.pyx +swmm_rdii_add include/openswmm/engine/openswmm_inflows.h 216 pyx_use python/openswmm/engine/_inflows.pyx +swmm_rdii_count include/openswmm/engine/openswmm_inflows.h 610 pyx_use python/openswmm/engine/_inflows.pyx +swmm_rdii_decay_add include/openswmm/engine/openswmm_inflows.h 549 pyx_use python/openswmm/engine/_inflows.pyx +swmm_rdii_decay_count include/openswmm/engine/openswmm_inflows.h 585 pyx_use python/openswmm/engine/_inflows.pyx +swmm_rdii_decay_get include/openswmm/engine/openswmm_inflows.h 574 pyx_use python/openswmm/engine/_inflows.pyx +swmm_rdii_decay_remove include/openswmm/engine/openswmm_inflows.h 514 pyx_use python/openswmm/engine/_inflows.pyx +swmm_rdii_decay_set include/openswmm/engine/openswmm_inflows.h 501 pyx_use python/openswmm/engine/_inflows.pyx +swmm_rdii_get include/openswmm/engine/openswmm_inflows.h 229 pyx_use python/openswmm/engine/_inflows.pyx +swmm_rdii_remove include/openswmm/engine/openswmm_inflows.h 239 pyx_use python/openswmm/engine/_inflows.pyx +swmm_reaction_coeff_add include/openswmm/engine/openswmm_reactions.h 137 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_coeff_count include/openswmm/engine/openswmm_reactions.h 92 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_coeff_get include/openswmm/engine/openswmm_reactions.h 95 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_coeff_remove include/openswmm/engine/openswmm_reactions.h 145 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_coeff_set_value include/openswmm/engine/openswmm_reactions.h 141 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_expr_get include/openswmm/engine/openswmm_reactions.h 109 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_expr_set include/openswmm/engine/openswmm_reactions.h 162 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_function_count include/openswmm/engine/openswmm_reactions.h 236 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_function_get include/openswmm/engine/openswmm_reactions.h 239 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_hydvar_count include/openswmm/engine/openswmm_reactions.h 229 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_hydvar_get include/openswmm/engine/openswmm_reactions.h 232 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_init_elem_count include/openswmm/engine/openswmm_reactions.h 175 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_init_elem_get include/openswmm/engine/openswmm_reactions.h 176 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_init_elem_remove include/openswmm/engine/openswmm_reactions.h 182 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_init_elem_set include/openswmm/engine/openswmm_reactions.h 180 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_init_global_get include/openswmm/engine/openswmm_reactions.h 171 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_init_global_set include/openswmm/engine/openswmm_reactions.h 173 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_option_get include/openswmm/engine/openswmm_reactions.h 117 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_option_set include/openswmm/engine/openswmm_reactions.h 166 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_species_add include/openswmm/engine/openswmm_reactions.h 129 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_species_count include/openswmm/engine/openswmm_reactions.h 84 pyx_use python/openswmm/engine/_quality.pyx;python/openswmm/engine/_reactions.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_reaction_species_get include/openswmm/engine/openswmm_reactions.h 87 pyx_use python/openswmm/engine/_quality.pyx;python/openswmm/engine/_reactions.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_reaction_species_remove include/openswmm/engine/openswmm_reactions.h 134 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_term_add include/openswmm/engine/openswmm_reactions.h 148 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_term_count include/openswmm/engine/openswmm_reactions.h 99 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_term_get include/openswmm/engine/openswmm_reactions.h 102 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_term_remove include/openswmm/engine/openswmm_reactions.h 156 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_term_set_expr include/openswmm/engine/openswmm_reactions.h 152 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reaction_validate_expression include/openswmm/engine/openswmm_reactions.h 78 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reactions_apply_text include/openswmm/engine/openswmm_reactions.h 215 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reactions_check_text include/openswmm/engine/openswmm_reactions.h 205 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reactions_save include/openswmm/engine/openswmm_reactions.h 222 pyx_use python/openswmm/engine/_reactions.pyx +swmm_reactions_serialize include/openswmm/engine/openswmm_reactions.h 196 pyx_use python/openswmm/engine/_reactions.pyx +swmm_runoff_iface_close include/openswmm/engine/openswmm_engine.h 562 pyx_use python/openswmm/engine/_solver.pyx +swmm_runoff_iface_open_read include/openswmm/engine/openswmm_engine.h 525 pyx_use python/openswmm/engine/_solver.pyx +swmm_runoff_iface_open_write include/openswmm/engine/openswmm_engine.h 514 pyx_use python/openswmm/engine/_solver.pyx +swmm_runoff_iface_read_step include/openswmm/engine/openswmm_engine.h 554 pyx_use python/openswmm/engine/_solver.pyx +swmm_runoff_iface_save_step include/openswmm/engine/openswmm_engine.h 540 pyx_use python/openswmm/engine/_solver.pyx +swmm_set_progress_callback include/openswmm/engine/openswmm_engine.h 287 pyx_use python/openswmm/engine/_solver.pyx +swmm_set_steady_state_skip include/openswmm/engine/openswmm_engine.h 481 pyx_use python/openswmm/engine/_solver.pyx +swmm_set_step_begin_callback include/openswmm/engine/openswmm_engine.h 291 pyx_use python/openswmm/engine/_solver.pyx +swmm_set_step_end_callback include/openswmm/engine/openswmm_engine.h 293 pyx_use python/openswmm/engine/_solver.pyx +swmm_set_warning_callback include/openswmm/engine/openswmm_engine.h 289 pyx_use python/openswmm/engine/_solver.pyx +swmm_snowpack_add include/openswmm/engine/openswmm_subcatchments.h 1003 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_snowpack_analyze_impact include/openswmm/engine/openswmm_edit.h 170 pyx_use python/openswmm/engine/_edit.pyx +swmm_snowpack_count include/openswmm/engine/openswmm_subcatchments.h 972 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_snowpack_delete include/openswmm/engine/openswmm_edit.h 354 pyx_use python/openswmm/engine/_edit.pyx +swmm_snowpack_get_impervious include/openswmm/engine/openswmm_subcatchments.h 1062 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_snowpack_get_pervious include/openswmm/engine/openswmm_subcatchments.h 1071 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_snowpack_get_plowable include/openswmm/engine/openswmm_subcatchments.h 1053 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_snowpack_get_removal include/openswmm/engine/openswmm_subcatchments.h 1090 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_snowpack_get_removal_subcatch include/openswmm/engine/openswmm_subcatchments.h 1097 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_snowpack_id include/openswmm/engine/openswmm_subcatchments.h 988 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_snowpack_index include/openswmm/engine/openswmm_subcatchments.h 980 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_snowpack_rename include/openswmm/engine/openswmm_subcatchments.h 1022 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_snowpack_set_impervious include/openswmm/engine/openswmm_subcatchments.h 1058 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_snowpack_set_pervious include/openswmm/engine/openswmm_subcatchments.h 1067 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_snowpack_set_plowable include/openswmm/engine/openswmm_subcatchments.h 1049 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_snowpack_set_removal include/openswmm/engine/openswmm_subcatchments.h 1086 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_snowpack_set_removal_subcatch include/openswmm/engine/openswmm_subcatchments.h 1095 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_spatial_get_crs include/openswmm/engine/openswmm_spatial.h 65 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_get_gage_coord include/openswmm/engine/openswmm_spatial.h 266 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_get_link_coord include/openswmm/engine/openswmm_spatial.h 133 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_get_link_vertex_count include/openswmm/engine/openswmm_spatial.h 166 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_get_link_vertices include/openswmm/engine/openswmm_spatial.h 183 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_get_node_coord include/openswmm/engine/openswmm_spatial.h 89 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_get_node_coords_bulk include/openswmm/engine/openswmm_spatial.h 99 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_get_subcatch_coord include/openswmm/engine/openswmm_spatial.h 207 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_get_subcatch_polygon include/openswmm/engine/openswmm_spatial.h 242 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_get_subcatch_polygon_count include/openswmm/engine/openswmm_spatial.h 231 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_set_crs include/openswmm/engine/openswmm_spatial.h 56 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_set_gage_coord include/openswmm/engine/openswmm_spatial.h 256 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_set_link_coord include/openswmm/engine/openswmm_spatial.h 123 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_set_link_vertices include/openswmm/engine/openswmm_spatial.h 152 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_set_node_coord include/openswmm/engine/openswmm_spatial.h 79 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_set_node_coords_bulk include/openswmm/engine/openswmm_spatial.h 109 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_set_subcatch_coord include/openswmm/engine/openswmm_spatial.h 197 pyx_use python/openswmm/engine/_spatial.pyx +swmm_spatial_set_subcatch_polygon include/openswmm/engine/openswmm_spatial.h 222 pyx_use python/openswmm/engine/_spatial.pyx +swmm_stat_link_max_filling include/openswmm/engine/openswmm_statistics.h 68 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_link_max_filling_bulk include/openswmm/engine/openswmm_statistics.h 159 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_link_max_flow include/openswmm/engine/openswmm_statistics.h 62 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_link_max_flow_bulk include/openswmm/engine/openswmm_statistics.h 97 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_link_max_velocity include/openswmm/engine/openswmm_statistics.h 65 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_link_max_velocity_bulk include/openswmm/engine/openswmm_statistics.h 151 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_link_surcharge_time include/openswmm/engine/openswmm_statistics.h 74 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_link_surcharge_time_bulk include/openswmm/engine/openswmm_statistics.h 175 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_link_vol_flow include/openswmm/engine/openswmm_statistics.h 71 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_link_vol_flow_bulk include/openswmm/engine/openswmm_statistics.h 167 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_node_max_depth include/openswmm/engine/openswmm_statistics.h 46 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_node_max_depth_bulk include/openswmm/engine/openswmm_statistics.h 94 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_node_max_overflow include/openswmm/engine/openswmm_statistics.h 49 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_node_max_overflow_bulk include/openswmm/engine/openswmm_statistics.h 115 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_node_time_flooded include/openswmm/engine/openswmm_statistics.h 55 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_node_time_flooded_bulk include/openswmm/engine/openswmm_statistics.h 129 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_node_vol_flooded include/openswmm/engine/openswmm_statistics.h 52 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_node_vol_flooded_bulk include/openswmm/engine/openswmm_statistics.h 122 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_subcatch_max_runoff include/openswmm/engine/openswmm_statistics.h 87 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_subcatch_max_runoff_bulk include/openswmm/engine/openswmm_statistics.h 136 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_subcatch_precip include/openswmm/engine/openswmm_statistics.h 81 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_subcatch_runoff_vol include/openswmm/engine/openswmm_statistics.h 84 pyx_use python/openswmm/engine/_statistics.pyx +swmm_stat_subcatch_runoff_vol_bulk include/openswmm/engine/openswmm_statistics.h 100 pyx_use python/openswmm/engine/_statistics.pyx +swmm_street_add include/openswmm/engine/openswmm_infrastructure.h 304 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_street_analyze_impact include/openswmm/engine/openswmm_edit.h 178 pyx_use python/openswmm/engine/_edit.pyx +swmm_street_count include/openswmm/engine/openswmm_infrastructure.h 352 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_street_delete include/openswmm/engine/openswmm_edit.h 368 pyx_use python/openswmm/engine/_edit.pyx +swmm_street_get_params include/openswmm/engine/openswmm_infrastructure.h 391 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_street_id include/openswmm/engine/openswmm_infrastructure.h 368 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_street_index include/openswmm/engine/openswmm_infrastructure.h 360 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_infrastructure.pyx +swmm_street_rename include/openswmm/engine/openswmm_infrastructure.h 323 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_street_set_params include/openswmm/engine/openswmm_infrastructure.h 342 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_subcatch_add include/openswmm/engine/openswmm_subcatchments.h 85 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_analyze_impact include/openswmm/engine/openswmm_edit.h 142 pyx_use python/openswmm/engine/_edit.pyx +swmm_subcatch_count include/openswmm/engine/openswmm_subcatchments.h 50 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_forcing.pyx;python/openswmm/engine/_infrastructure.pyx;python/openswmm/engine/_spatial.pyx;python/openswmm/engine/_statistics.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_delete include/openswmm/engine/openswmm_edit.h 245 pyx_use python/openswmm/engine/_edit.pyx +swmm_subcatch_delete_many include/openswmm/engine/openswmm_edit.h 292 gap +swmm_subcatch_get_aquifer include/openswmm/engine/openswmm_subcatchments.h 583 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_area include/openswmm/engine/openswmm_subcatchments.h 244 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_coverage include/openswmm/engine/openswmm_subcatchments.h 475 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_coverages include/openswmm/engine/openswmm_subcatchments.h 490 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_ds_imperv include/openswmm/engine/openswmm_subcatchments.h 317 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_ds_perv include/openswmm/engine/openswmm_subcatchments.h 326 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_evap include/openswmm/engine/openswmm_subcatchments.h 568 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_evap_bulk include/openswmm/engine/openswmm_subcatchments.h 762 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_gage include/openswmm/engine/openswmm_subcatchments.h 335 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_groundwater include/openswmm/engine/openswmm_subcatchments.h 541 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_gw_node include/openswmm/engine/openswmm_subcatchments.h 589 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_gw_params include/openswmm/engine/openswmm_subcatchments.h 607 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_gw_state include/openswmm/engine/openswmm_subcatchments.h 643 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_gwf_expression include/openswmm/engine/openswmm_subcatchments.h 1187 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_ids_bulk include/openswmm/engine/openswmm_subcatchments.h 796 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_imperv_pct include/openswmm/engine/openswmm_subcatchments.h 253 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_infil include/openswmm/engine/openswmm_subcatchments.h 689 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_infil_bulk include/openswmm/engine/openswmm_subcatchments.h 770 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_infil_curve_number include/openswmm/engine/openswmm_subcatchments.h 413 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_infil_green_ampt include/openswmm/engine/openswmm_subcatchments.h 400 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_infil_horton include/openswmm/engine/openswmm_subcatchments.h 387 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_infil_model include/openswmm/engine/openswmm_subcatchments.h 375 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_initial_loading include/openswmm/engine/openswmm_subcatchments.h 519 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_n_imperv include/openswmm/engine/openswmm_subcatchments.h 299 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_n_perv include/openswmm/engine/openswmm_subcatchments.h 308 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_outlet include/openswmm/engine/openswmm_subcatchments.h 272 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_outlet_subcatch include/openswmm/engine/openswmm_subcatchments.h 358 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_ponded_quality include/openswmm/engine/openswmm_subcatchments.h 806 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_quality include/openswmm/engine/openswmm_subcatchments.h 714 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_quality_bulk include/openswmm/engine/openswmm_subcatchments.h 738 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_rain_scale_factor include/openswmm/engine/openswmm_subcatchments.h 1130 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_rainfall include/openswmm/engine/openswmm_subcatchments.h 550 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_rainfall_bulk include/openswmm/engine/openswmm_subcatchments.h 754 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_runoff include/openswmm/engine/openswmm_subcatchments.h 532 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_runoff_bulk include/openswmm/engine/openswmm_subcatchments.h 728 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_slope include/openswmm/engine/openswmm_subcatchments.h 290 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_snow_depth include/openswmm/engine/openswmm_subcatchments.h 559 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_snow_depth_bulk include/openswmm/engine/openswmm_subcatchments.h 780 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_snow_scale_factor include/openswmm/engine/openswmm_subcatchments.h 1153 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_snow_state include/openswmm/engine/openswmm_subcatchments.h 678 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_stat_max_runoff include/openswmm/engine/openswmm_subcatchments.h 445 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_stat_precip include/openswmm/engine/openswmm_subcatchments.h 427 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_stat_runoff_vol include/openswmm/engine/openswmm_subcatchments.h 436 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_tag include/openswmm/engine/openswmm_subcatchments.h 1104 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_width include/openswmm/engine/openswmm_subcatchments.h 281 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_get_zero_imperv_pct include/openswmm/engine/openswmm_subcatchments.h 263 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_id include/openswmm/engine/openswmm_subcatchments.h 66 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_index include/openswmm/engine/openswmm_subcatchments.h 58 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_forcing.pyx;python/openswmm/engine/_infrastructure.pyx;python/openswmm/engine/_spatial.pyx;python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_rename include/openswmm/engine/openswmm_subcatchments.h 816 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_aquifer include/openswmm/engine/openswmm_subcatchments.h 580 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_area include/openswmm/engine/openswmm_subcatchments.h 107 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_coverage include/openswmm/engine/openswmm_subcatchments.h 465 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_ds_imperv include/openswmm/engine/openswmm_subcatchments.h 172 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_ds_perv include/openswmm/engine/openswmm_subcatchments.h 181 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_gage include/openswmm/engine/openswmm_subcatchments.h 190 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_gw_node include/openswmm/engine/openswmm_subcatchments.h 586 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_gw_params include/openswmm/engine/openswmm_subcatchments.h 601 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_gw_state include/openswmm/engine/openswmm_subcatchments.h 631 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_gwf_expression include/openswmm/engine/openswmm_subcatchments.h 1207 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_imperv_pct include/openswmm/engine/openswmm_subcatchments.h 134 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_infil_curve_number include/openswmm/engine/openswmm_subcatchments.h 215 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_infil_green_ampt include/openswmm/engine/openswmm_subcatchments.h 200 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_infil_horton include/openswmm/engine/openswmm_subcatchments.h 195 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_infil_model include/openswmm/engine/openswmm_subcatchments.h 231 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_initial_loading include/openswmm/engine/openswmm_subcatchments.h 509 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_n_imperv include/openswmm/engine/openswmm_subcatchments.h 154 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_n_perv include/openswmm/engine/openswmm_subcatchments.h 163 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_outlet include/openswmm/engine/openswmm_subcatchments.h 98 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_outlet_subcatch include/openswmm/engine/openswmm_subcatchments.h 349 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_ponded_quality include/openswmm/engine/openswmm_subcatchments.h 810 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_rain_scale_factor include/openswmm/engine/openswmm_subcatchments.h 1126 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_rainfall include/openswmm/engine/openswmm_subcatchments.h 700 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_slope include/openswmm/engine/openswmm_subcatchments.h 125 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_snow_scale_factor include/openswmm/engine/openswmm_subcatchments.h 1149 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_snow_state include/openswmm/engine/openswmm_subcatchments.h 662 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_tag include/openswmm/engine/openswmm_subcatchments.h 1108 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_width include/openswmm/engine/openswmm_subcatchments.h 116 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_subcatch_set_zero_imperv_pct include/openswmm/engine/openswmm_subcatchments.h 145 pyx_use python/openswmm/engine/_subcatchments.pyx +swmm_table_add_point include/openswmm/engine/openswmm_tables.h 132 pyx_use python/openswmm/engine/_tables.pyx +swmm_table_analyze_impact include/openswmm/engine/openswmm_edit.h 150 pyx_use python/openswmm/engine/_edit.pyx +swmm_table_clear include/openswmm/engine/openswmm_tables.h 160 pyx_use python/openswmm/engine/_tables.pyx +swmm_table_count include/openswmm/engine/openswmm_tables.h 50 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_tables.pyx +swmm_table_delete include/openswmm/engine/openswmm_edit.h 308 pyx_use python/openswmm/engine/_edit.pyx +swmm_table_get_point include/openswmm/engine/openswmm_tables.h 152 pyx_use python/openswmm/engine/_tables.pyx +swmm_table_get_point_count include/openswmm/engine/openswmm_tables.h 141 pyx_use python/openswmm/engine/_tables.pyx +swmm_table_get_type include/openswmm/engine/openswmm_tables.h 89 pyx_use python/openswmm/engine/_tables.pyx +swmm_table_id include/openswmm/engine/openswmm_tables.h 66 pyx_use python/openswmm/engine/_tables.pyx +swmm_table_index include/openswmm/engine/openswmm_tables.h 58 pyx_use python/openswmm/engine/_edit.pyx;python/openswmm/engine/_tables.pyx +swmm_table_lookup include/openswmm/engine/openswmm_tables.h 219 pyx_use python/openswmm/engine/_tables.pyx +swmm_timeseries_add include/openswmm/engine/openswmm_tables.h 101 pyx_use python/openswmm/engine/_tables.pyx +swmm_timeseries_get_relative_info include/openswmm/engine/openswmm_tables.h 180 gap +swmm_timeseries_set_relative_info include/openswmm/engine/openswmm_tables.h 200 gap +swmm_title_add_line include/openswmm/engine/openswmm_model.h 460 pyx_use python/openswmm/engine/_model.pyx +swmm_title_clear include/openswmm/engine/openswmm_model.h 480 pyx_use python/openswmm/engine/_model.pyx +swmm_title_get_count include/openswmm/engine/openswmm_model.h 435 pyx_use python/openswmm/engine/_model.pyx +swmm_title_get_line include/openswmm/engine/openswmm_model.h 446 pyx_use python/openswmm/engine/_model.pyx +swmm_title_set include/openswmm/engine/openswmm_model.h 472 pyx_use python/openswmm/engine/_model.pyx +swmm_transect_add include/openswmm/engine/openswmm_infrastructure.h 51 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_add_station include/openswmm/engine/openswmm_infrastructure.h 76 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_analyze_impact include/openswmm/engine/openswmm_edit.h 154 pyx_use python/openswmm/engine/_edit.pyx +swmm_transect_clear_stations include/openswmm/engine/openswmm_infrastructure.h 265 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_count include/openswmm/engine/openswmm_infrastructure.h 83 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_delete include/openswmm/engine/openswmm_edit.h 317 pyx_use python/openswmm/engine/_edit.pyx +swmm_transect_get_bank_stations include/openswmm/engine/openswmm_infrastructure.h 144 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_get_comments include/openswmm/engine/openswmm_infrastructure.h 233 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_get_encroachment_stations include/openswmm/engine/openswmm_infrastructure.h 174 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_get_modifiers include/openswmm/engine/openswmm_infrastructure.h 208 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_get_roughness include/openswmm/engine/openswmm_infrastructure.h 118 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_get_station include/openswmm/engine/openswmm_infrastructure.h 252 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_get_station_count include/openswmm/engine/openswmm_infrastructure.h 241 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_id include/openswmm/engine/openswmm_infrastructure.h 99 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_index include/openswmm/engine/openswmm_infrastructure.h 91 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_remove include/openswmm/engine/openswmm_infrastructure.h 292 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_rename include/openswmm/engine/openswmm_infrastructure.h 278 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_set_bank_stations include/openswmm/engine/openswmm_infrastructure.h 133 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_set_comments include/openswmm/engine/openswmm_infrastructure.h 218 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_set_encroachment_stations include/openswmm/engine/openswmm_infrastructure.h 163 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_set_modifiers include/openswmm/engine/openswmm_infrastructure.h 196 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transect_set_roughness include/openswmm/engine/openswmm_infrastructure.h 62 pyx_use python/openswmm/engine/_infrastructure.pyx +swmm_transport_boundary_count include/openswmm/engine/openswmm_transport.h 101 gap +swmm_transport_class_name include/openswmm/engine/openswmm_engine.h 405 gap +swmm_transport_conduit_disp_count include/openswmm/engine/openswmm_transport.h 91 gap +swmm_transport_domain_name include/openswmm/engine/openswmm_engine.h 404 gap +swmm_transport_get_boundary include/openswmm/engine/openswmm_transport.h 114 gap +swmm_transport_get_conduit_disp include/openswmm/engine/openswmm_transport.h 95 gap +swmm_transport_get_configured include/openswmm/engine/openswmm_transport.h 59 gap +swmm_transport_get_dispersion_mode include/openswmm/engine/openswmm_transport.h 63 gap +swmm_transport_get_dispersion_value include/openswmm/engine/openswmm_transport.h 75 gap +swmm_transport_get_source include/openswmm/engine/openswmm_transport.h 129 gap +swmm_transport_get_target_dx include/openswmm/engine/openswmm_transport.h 83 gap +swmm_transport_set_dispersion_mode include/openswmm/engine/openswmm_transport.h 71 gap +swmm_transport_set_dispersion_value include/openswmm/engine/openswmm_transport.h 79 gap +swmm_transport_set_target_dx include/openswmm/engine/openswmm_transport.h 87 gap +swmm_transport_source_count include/openswmm/engine/openswmm_transport.h 121 gap +swmm_treatment_clear include/openswmm/engine/openswmm_quality.h 197 pyx_use python/openswmm/engine/_quality.pyx +swmm_treatment_get include/openswmm/engine/openswmm_quality.h 186 pyx_use python/openswmm/engine/_quality.pyx +swmm_treatment_set include/openswmm/engine/openswmm_quality.h 174 pyx_use python/openswmm/engine/_quality.pyx +swmm_treatment_validate_expression include/openswmm/engine/openswmm_quality.h 223 pyx_use python/openswmm/engine/_quality.pyx +swmm_userflag_def_count include/openswmm/engine/openswmm_model.h 679 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_userflag_def_get include/openswmm/engine/openswmm_model.h 692 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_userflag_define include/openswmm/engine/openswmm_model.h 712 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_userflag_get_bool include/openswmm/engine/openswmm_model.h 631 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_userflag_get_int include/openswmm/engine/openswmm_model.h 640 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_userflag_get_real include/openswmm/engine/openswmm_model.h 649 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_userflag_set_bool include/openswmm/engine/openswmm_model.h 652 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_userflag_set_int include/openswmm/engine/openswmm_model.h 655 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_userflag_set_real include/openswmm/engine/openswmm_model.h 658 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_userflag_undefine include/openswmm/engine/openswmm_model.h 725 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_userflag_value_clear include/openswmm/engine/openswmm_model.h 778 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_userflag_value_get include/openswmm/engine/openswmm_model.h 738 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_userflag_value_set include/openswmm/engine/openswmm_model.h 761 pyx_use python/openswmm/engine/_model.pyx;python/openswmm/engine/_solver.pyx +swmm_validate_model include/openswmm/engine/openswmm_model.h 78 pyx_use python/openswmm/engine/_model.pyx +swmm_virtual_junction_fuse include/openswmm/engine/openswmm_edit.h 525 pyx_use python/openswmm/engine/_edit.pyx +swmm_washoff_get include/openswmm/engine/openswmm_quality.h 158 pyx_use python/openswmm/engine/_quality.pyx +swmm_washoff_set include/openswmm/engine/openswmm_quality.h 141 pyx_use python/openswmm/engine/_quality.pyx +swmm_water_age_get_enabled include/openswmm/engine/openswmm_water_age.h 68 pyx_use python/openswmm/engine/_water_age.pyx +swmm_water_age_get_global_source include/openswmm/engine/openswmm_water_age.h 72 pyx_use python/openswmm/engine/_water_age.pyx +swmm_water_age_get_override include/openswmm/engine/openswmm_water_age.h 93 pyx_use python/openswmm/engine/_water_age.pyx +swmm_water_age_override_count include/openswmm/engine/openswmm_water_age.h 86 pyx_use python/openswmm/engine/_water_age.pyx +swmm_water_age_remove_override include/openswmm/engine/openswmm_water_age.h 108 pyx_use python/openswmm/engine/_water_age.pyx +swmm_water_age_save include/openswmm/engine/openswmm_water_age.h 117 pyx_use python/openswmm/engine/_water_age.pyx +swmm_water_age_set_global_source include/openswmm/engine/openswmm_water_age.h 81 pyx_use python/openswmm/engine/_water_age.pyx +swmm_water_age_set_override include/openswmm/engine/openswmm_water_age.h 103 pyx_use python/openswmm/engine/_water_age.pyx +swmm_xsect_area_of_depth include/openswmm/engine/openswmm_xsect.h 301 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_area_of_depth_array include/openswmm/engine/openswmm_xsect.h 440 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_area_of_sectfactor include/openswmm/engine/openswmm_xsect.h 374 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_area_of_sectfactor_array include/openswmm/engine/openswmm_xsect.h 464 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_create include/openswmm/engine/openswmm_xsect.h 121 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_create_custom include/openswmm/engine/openswmm_xsect.h 175 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_create_irregular include/openswmm/engine/openswmm_xsect.h 152 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_create_street include/openswmm/engine/openswmm_xsect.h 203 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_critical_depth include/openswmm/engine/openswmm_xsect.h 397 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_critical_depth_array include/openswmm/engine/openswmm_xsect.h 471 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_depth_of_area include/openswmm/engine/openswmm_xsect.h 337 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_depth_of_area_array include/openswmm/engine/openswmm_xsect.h 452 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_dsda include/openswmm/engine/openswmm_xsect.h 386 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_dsda_array include/openswmm/engine/openswmm_xsect.h 468 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_free include/openswmm/engine/openswmm_xsect.h 70 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_full_properties include/openswmm/engine/openswmm_xsect.h 415 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_get_shape include/openswmm/engine/openswmm_xsect.h 254 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_get_units include/openswmm/engine/openswmm_xsect.h 271 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_hydrad_of_area include/openswmm/engine/openswmm_xsect.h 349 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_hydrad_of_area_array include/openswmm/engine/openswmm_xsect.h 456 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_hydrad_of_depth include/openswmm/engine/openswmm_xsect.h 325 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_hydrad_of_depth_array include/openswmm/engine/openswmm_xsect.h 448 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_is_open include/openswmm/engine/openswmm_xsect.h 427 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_sectfactor_of_area include/openswmm/engine/openswmm_xsect.h 361 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_sectfactor_of_area_array include/openswmm/engine/openswmm_xsect.h 460 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_shape_name include/openswmm/engine/openswmm_xsect.h 285 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_width_of_depth include/openswmm/engine/openswmm_xsect.h 313 pyx_use python/openswmm/engine/_xsect.pyx +swmm_xsect_width_of_depth_array include/openswmm/engine/openswmm_xsect.h 444 pyx_use python/openswmm/engine/_xsect.pyx diff --git a/docs/analysis/RAINFALL_2D_TEMPORAL_OUTPUT_AUDIT_2026-09-30.md b/docs/analysis/RAINFALL_2D_TEMPORAL_OUTPUT_AUDIT_2026-09-30.md new file mode 100644 index 000000000..7cfa98c3b --- /dev/null +++ b/docs/analysis/RAINFALL_2D_TEMPORAL_OUTPUT_AUDIT_2026-09-30.md @@ -0,0 +1,74 @@ +# 2D rainfall: input formats, integration and saved output + +## Scope + +Audit the current engine's gage-to-cell rainfall conversion and its actual HDF5 output, independently of the GUI plot renderer. The affected project was not supplied; this is controlled model verification, not a diagnosis of that project's individual dry cells. + +## Input and output contracts + +- `INTENSITY` records are rates in the project's rain units: in/hr for US flow units, mm/hr for SI flow units. +- `VOLUME` records are **rainfall depths per gage recording interval** (inches or millimetres), not cubic metres. Conversion is `depth / interval_seconds * 3600`. +- `CUMULATIVE` records are cumulative depths. Consecutive differences give interval depths; a decreasing counter starts a new accumulation and the new reading is that interval's depth. Differencing happens before spatial interpolation. +- Each gage uses its own recording interval. Its converted rate is constant over that interval; gaps are dry. The runoff clock books changes, and each 2D synchronization window receives the integral of those rates divided by the window duration. The solver's smaller internal steps use that window mean. +- Spatial weights map gage values to each cell. Natural neighbour uses its existing inverse-distance fallback outside the gage hull. Nearest neighbour selects the closest located gage, including when its rainfall is zero. +- `Mesh2_face_rainfall` stores a sampled rate in **m/s**, not a report-period average. Multiply by 3,600,000 for mm/hr. +- `Mesh2_face_rain_cum` stores cumulative **volume in m³ per cell**. Divide by `Mesh2_face_area` for depth in metres; multiply by 1,000 for millimetres. Differences between cumulative records give the volume over a reporting interval. + +For cell c, area A and static spatial weights w, the expected accumulated volume is: + +`V_c(t) = A_c * sum_g(w_cg * integral(start, t, rain_g(t) dt))`. + +A storm entirely between report instants can leave every sampled rate zero while cumulative volume increases correctly. Also, the last scheduled frame can precede simulation end: the final `mass_balance_2d/rainfall_in` includes the trailing interval, while that last frame stops at its own reporting time. `swmm_engine_end()` flushes a final partial 2D batch before writing final mass balance. + +## Confirmed defects and correction + +The old saved cumulative volume came directly from the completed 2D window, even when that window ended after the frame timestamp. A controlled 7-second routing case reported the accumulated volume through 21 seconds at the 17-second frame, 23.53% too high at that early frame. Across 36 cases / 1,080 cell records, maximum absolute saved-volume error was 0.0012072649572649578 m³. Final domain rainfall totals were already correct: maximum error against the independent input integral was 9.03e-17 m³. + +The correction preserves the gage integral at a report boundary before the runoff clock passes it, then maps only the portion up to that boundary onto the cells. Added and overridden cell rainfall is included over the corresponding portion of the window. Runtime source rates, integration, and mass-balance totals are unchanged. Storage is bounded to one additional gage row and one cell row; the extra spatial mapping occurs at report cadence. This reconstructs source accumulation at the requested time: the hydraulic solver still applies a window-mean source, and reported depth/head still use linear state interpolation. Inside a window crossing a rain change, those interpolated hydraulic states need not imply exactly the same partial volume as the piecewise gage integral. Completed-window and final rainfall conservation are unchanged. + +The saved rate also used the runoff cursor's current rain record. If a routing step crossed a report instant and then a rain-record change, the cursor could already point beyond the requested report time. The 2D report path now looks up the source interval directly, reusing the existing format conversion, scaling, co-gage and monthly-adjustment handling. The 1D legacy report path is preserved. Both paths retain SWMM's +1 second report-gage lookup convention, so a rate within one second of a boundary can show the next interval; cumulative volume integrates to the nominal report boundary without this look-ahead. + +The HDF5 `time` coordinate contained absolute SWMM dates but was labelled `days since simulation start`. The label is corrected to `days since 1899-12-30 00:00:00`. Numeric timestamps are unchanged and retain SWMM's 1 ms report-date offset. The GUI already interprets these numbers as absolute SWMM dates. + +## Regression matrix + +`Output2DWriter.RainFormatsAndMismatchedClocks` writes and reopens real output files. Its expected values come from independently integrated depth records, not the production conversion or integration functions. + +The matrix combines both interpolation modes, both unit systems and all three rain formats with these clock configurations: + +| Routing maximum (s) | 2D maximum substep (s) | Base report interval (s) | Requested runoff interval (s) | Start offset (s) | Additional coverage | +|---:|---:|---:|---:|---:|---| +| 7 | 2 | 17 | 37 | 0 | Unaligned report and rain boundaries | +| 31 | 7 | 60 | 120 | 17 | Partial first rain interval and final simulation interval | +| 2.5 | 0.7 | 19 | 13 | 0 | Fractional routing and solver timesteps | +| 7 | 2 | 17 | 37 | 0 | 29 s synchronization, report start at 68 s, 34 s 2D reports, FLOAT32, persistent added rainfall | +| 31 | 7 | 53 | 120 | 17 | Routing step crosses a rain change after the report instant | + +Three simultaneously active gages have 60-, 120- and 180-second recording intervals, distinct wet/dry patterns and cumulative counter resets. The engine correctly reduces a requested runoff step longer than the shortest recording interval. Other cases use FLOAT64. All authored report intervals exceed the routing maximum; sub-routing-step reporting is outside this matrix. + +The assertions cover each saved rate, each saved cumulative cell volume, frame count, timestamps, unit attributes, final rainfall mass balance and consistency of the live cell-volume sum with the live ledger. Existing pulse, dry-cell, spatial interpolation and gage-file tests provide complementary coverage. + +## Validation artifacts + +Reviewable generated models, reports and HDF5 files: `tests/output/2d_output_options/rain_clocks_*`. + +Audit logs and the independent Python/HDF5 checker: `tests/output/rainfall_temporal_audit_2026-09-30/`. + +## Results + +- All **60 model runs / 1,440 saved cell records** passed the analytic comparisons. +- All **45 tests** passed across `test_engine_gage_rain_series` (10), `test_engine_2d_rainfall_interp` (19) and `test_engine_2d_output_options` (16). The between-report pulse test now requires zero cumulative rain at the pulse's start instead of allowing an entire routing step's premature rainfall. +- The independent Python/HDF5 audit found a maximum FLOAT64 cumulative-volume error of **3.13e-17 m³** and maximum rate error of **1.70e-21 m/s**. For FLOAT32 these were **8.91e-10 m³** and **1.08e-13 m/s**, consistent with storage rounding. +- Maximum final domain rainfall error was **9.03e-17 m³**. Final rainfall totals on the original 36 baseline cases changed by **exactly zero** after the output corrections. +- All 60 files have the corrected time-axis units. Both US and SI models retain SI rainfall output units. + +Validation used the rebuilt Release engine and the CPU explicit 2D marcher with one local-timestep tier. These changes are in the engine working tree; no GUI package was rebuilt or installed during this audit. Existing results need a simulation rerun with the updated engine to obtain corrected saved values. The input matrix uses model time series; external rain-file loading is covered separately by the passing gage-series suite, rather than every external file format being crossed with every clock configuration. + +Reproduce the automated matrix and regression checks: + +```sh +cmake --build build/darwin --target test_engine_2d_output_options test_engine_2d_rainfall_interp test_engine_gage_rain_series -j6 +ctest --test-dir build/darwin -R '^test_engine_(2d_output_options|2d_rainfall_interp|gage_rain_series)$' --output-on-failure +``` + +The independent file-audit summaries are `before.json` and `after.json`; the test logs are `before.log`, `after.log`, and `regression.log` in the audit artifact directory above. diff --git a/docs/custom/css/manual.css b/docs/custom/css/manual.css new file mode 100644 index 000000000..e3da02ccc --- /dev/null +++ b/docs/custom/css/manual.css @@ -0,0 +1,103 @@ +/* Manual-specific styling on top of doxygen-awesome. + * + * Status badges: emitted by the Doxyfile alias + * \status{Implemented} -> Implemented + * The four hex values are copied from docs/figures/status.py; the lint + * (check_status_badges) fails if they drift apart, so change them there first. + */ +.status-badge { + display: inline-block; + padding: 0 0.55em; + border-radius: 1em; + font-size: 0.78em; + font-weight: 600; + line-height: 1.7; + letter-spacing: 0.01em; + color: #ffffff; + white-space: nowrap; + vertical-align: baseline; +} +.status-badge[data-status="Implemented"] { background: #1baf7a; } +.status-badge[data-status="Experimental"] { background: #eda100; } +.status-badge[data-status="Planned"] { background: #898781; } +.status-badge[data-status="Retired"] { background: #e87ba4; text-decoration: line-through; } + +html.dark-mode .status-badge[data-status="Implemented"] { background: #148a5f; } +html.dark-mode .status-badge[data-status="Experimental"] { background: #b97e00; } +html.dark-mode .status-badge[data-status="Planned"] { background: #6a6863; } +html.dark-mode .status-badge[data-status="Retired"] { background: #b85c7f; } + +/* A table whose header names a Status column reads better with the badge + * centred and the column kept narrow. */ +table.markdownTable td .status-badge { margin: 0 auto; } + +/* Interactive diagrams (docs/custom/js/manual-interactive.js). + * + * The link blocks that carry the click targets are data for the script, not + * content: hidden here so a page without JavaScript shows the plain figure. */ +div.fig-hotspots, div.workflow-links { display: none; } + +.mi-wrap { position: relative; margin: 0.6em 0 0.3em; } +.mi-viewport { + position: relative; + overflow: hidden; + border: 1px solid var(--separator-color, #dedede); + border-radius: 6px; + background: var(--page-background-color, #ffffff); + cursor: grab; + touch-action: pan-y; + user-select: none; + -webkit-user-select: none; +} +.mi-viewport.is-active { + border-color: var(--primary-color, #1982d2); + box-shadow: 0 0 0 2px rgba(25, 130, 210, 0.22); + touch-action: none; +} +.mi-viewport.is-dragging { cursor: grabbing; } +.mi-canvas { position: relative; display: inline-block; max-width: 100%; transform-origin: 0 0; will-change: transform; } +.mi-canvas > img { display: block; max-width: 100%; height: auto; } +.mi-canvas > pre.mermaid { margin: 0; padding: 0.4em; background: transparent; border: 0; text-align: center; } +.mi-canvas > pre.mermaid > svg { max-width: 100%; height: auto; } +.mi-overlay { position: absolute; left: 0; top: 0; right: 0; bottom: 0; } +.mi-hs { + position: absolute; + display: block; + border: 1px solid transparent; + border-radius: 4px; + box-sizing: border-box; + cursor: pointer; +} +.mi-hs:hover, .mi-hs:focus-visible { + border-color: var(--primary-color, #1982d2); + background: rgba(25, 130, 210, 0.13); + outline: none; +} +.mi-toolbar { + position: absolute; + top: 6px; + right: 6px; + display: flex; + gap: 4px; + z-index: 5; + opacity: 0.45; + transition: opacity 0.15s; +} +.mi-wrap:hover .mi-toolbar, .mi-viewport.is-active ~ .mi-toolbar { opacity: 1; } +.mi-btn { + min-width: 26px; + height: 26px; + padding: 0 6px; + border: 1px solid var(--separator-color, #cfcfcf); + border-radius: 5px; + background: var(--page-background-color, #ffffff); + color: var(--page-foreground-color, #222222); + font: 600 14px/24px sans-serif; + text-align: center; + text-decoration: none; + cursor: pointer; +} +.mi-btn:hover { border-color: var(--primary-color, #1982d2); color: var(--primary-color, #1982d2); } +.mi-hint { font-size: 0.78em; color: var(--page-secondary-foreground-color, #6a6a6a); margin-top: 0.3em; } +html.dark-mode .mi-viewport.is-active { box-shadow: 0 0 0 2px rgba(120, 180, 240, 0.28); } +html.dark-mode .mi-hs:hover, html.dark-mode .mi-hs:focus-visible { background: rgba(120, 180, 240, 0.18); } diff --git a/docs/custom/html/header.html b/docs/custom/html/header.html index 13a72f9e2..d59d3e9ca 100644 --- a/docs/custom/html/header.html +++ b/docs/custom/html/header.html @@ -43,14 +43,19 @@ + in the manual markdown. Theme follows the doxygen-awesome dark-mode class. + Rendering is started by manual-interactive.js, which first injects the click + targets a block's