An open API service indexing awesome lists of open source software.

https://github.com/jlillywh/shrine

Open-source Python library for integrated water-resources simulation: hydrology, reservoirs, flow networks, scenarios, and mass balance via shrine.simulation.
https://github.com/jlillywh/shrine

hydrology open-source python python3 reservoir simulation water-resources watershed

Last synced: 18 days ago
JSON representation

Open-source Python library for integrated water-resources simulation: hydrology, reservoirs, flow networks, scenarios, and mass balance via shrine.simulation.

Awesome Lists containing this project

README

          

[![PyPI version](https://img.shields.io/pypi/v/shrine?logo=pypi&logoColor=white&label=version&v=0.3.0)](https://pypi.org/project/shrine/)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![codecov](https://codecov.io/gh/jlillywh/SHRINE/graph/badge.svg?branch=master)](https://codecov.io/gh/jlillywh/SHRINE)

# SHRINE

**Simulation of Hydrology, Reservoirs, and Integrated Network Environments**

SHRINE is an open-source water-resources modeling library, written in Python. It is meant for the same work you already do with watershed hydrology, reservoir and conveyance systems, and operating rules—set up a model, run it through a study period, compare alternatives, and check that water is accounted for.

Under the hood, SHRINE ties together rainfall–runoff and routing, storage and networks, flow allocation, and mass-balance checks. New studies should use the **simulation framework** ([below](#simulation-framework-shrinesimulation)): a shared calendar, named inputs, scenario files, and tabular outputs you can review in Excel or your own scripts. You do not need to be a software developer to start; if you are comfortable with Python notebooks or willing to follow the examples, you can run a complete model from this repo.

This project is the modeling core for a planning-grade web application I am building—and it is published here so other civil engineers and water resources modelers can experiment with it, adapt it, or borrow pieces for their own studies.

Naming details: [docs/project-name.md](docs/project-name.md).

## Simulation framework (`shrine.simulation`)

The framework is the **only supported path** for new model runs. Import from the package root:

```python
import shrine.simulation as sim
# or
from shrine.simulation import Model, RunController, Clock, WatershedElement
```

**Package versions:** `shrine.__version__` (distribution) and `shrine.simulation.__api_version__` (stable simulation API surface, currently **1.1**). Stability and deprecation rules: [docs/api-stability.md](docs/api-stability.md). Release history: [CHANGELOG.md](CHANGELOG.md); SemVer and maintainer checklist: [docs/releases.md](docs/releases.md). Submodules outside `__all__` are internal unless noted in [extending-elements.md](docs/extending-elements.md).

### Public API

| Category | Symbols |
|----------|---------|
| **Run** | `Model`, `RegisteredElement`, `RunController`, `RunResult`, `RunSession`, `StepResult`, `ElementScheduler` |
| **Time** | `Clock`, `RunContext`, `TimestepContext` |
| **Elements** | `Simulatable`, `WatershedElement`, `CatchmentElement`, `ReservoirElement`, `ReservoirNetworkElement`, `IrrigatedFieldElement`, `ClimateRecorderElement`, `StorageLike` |
| **Plugins** | `list_element_plugins`, `load_element_plugin`, `create_element_from_plugin` (`shrine.elements` entry points) |
| **Inputs** | `InputManager`, `InputProvider`, `ConstantInput`, `MonthlyLookupInput`, `StochasticInput` |
| **Flow / balance** | `FlowSolver`, `NetworkXFlowSolver`, `FlowSolveResult`, `MassBalanceCheck`, `MassBalanceReport`, `MassBalanceTerm` |
| **Outputs / scenarios** | `Recorder`, `ScenarioConfig`, `load_scenario_file`, `run_scenario`, `run_scenarios`, `load_and_run` |
| **Metadata / RNG** | `build_run_metadata`, `enrich_run_metadata`, `RunTimer`, `make_rng` |
| **Deprecation** | `warn_api_deprecated` |
| **Errors** | `SimulationError`, `SimulationPhase` |

Capabilities:

- **`Model`** — register elements (watersheds, reservoirs, custom types) with a shared `Clock`
- **`RunController`** — validate → initialize → timestep loop → finalize
- **`InputManager`** — constant, monthly, and stochastic inputs bound by name
- **`Recorder`** — wide `pandas` DataFrame outputs per run
- **Scenarios** — YAML/JSON clock, inputs, and parameter overrides
- **Flow solve** — NetworkX max-flow on graphs owned by `Watershed` adapters
- **Mass balance** — per-timestep verification with `SimulationError` diagnostics

Requirements and phased delivery: [docs/simulation-framework-requirements.md](docs/simulation-framework-requirements.md). Layer diagram: [docs/architecture.md](docs/architecture.md).

## Hydrology (`src/hydrology/`)

Legacy and evolving **rainfall–runoff** and **watershed network** code lives under `src/hydrology/`. New runs should use **`shrine.simulation`** adapters (`CatchmentElement`, `WatershedElement`) with a `RunoffMethod` or custom `RunoffModel` on each catchment. Contracts and adapters: [docs/hydrology-contracts.md](docs/hydrology-contracts.md). Muskingum routing, snow models, and future kinematic wave work: [docs/river-reservoir-roadmap.md](docs/river-reservoir-roadmap.md).

### Rainfall–runoff models

| Model | `RunoffMethod` | Module | Framework-ready | Notes |
|-------|----------------|--------|-----------------|-------|
| **Rational** (simple loss) | `SIMPLE` | `catchment.Rational` | Yes | Default; fast sanity checks and reference scenarios |
| **AWBM** (Australian Water Balance Model) | `AWBM` | `awbm.Awbm` | Yes | Boughton (2004); verified via fixture + mass balance — [docs/awbm-verification.md](docs/awbm-verification.md) |
| **Snow-17** (temperature-index snow) | `SNOW17` | `snow17.Snow17RunoffModel` | Yes | Independent reference twin + fixture — [docs/snow17-verification.md](docs/snow17-verification.md) |
| **AWBM + Snow-17** | `AWBM_SNOW17` | `snow17.AWBM_Snow17_RunoffModel` | Yes | Snowpack → AWBM soil; [docs/awbm-snow17.md](docs/awbm-snow17.md) |
| **GR4J** | `GR4J` | `gr4j.Gr4jRunoffModel` | Yes | airGR `frun_gr4j` port — [docs/gr4j-cemaneige-verification.md](docs/gr4j-cemaneige-verification.md) |
| **GR4J + CemaNeige** | `GR4J_CEMANNEIGE` | `gr4j_cemaneige.GR4J_CemaneigeRunoffModel` | Yes | INRAE snow + runoff — [docs/gr4j-cemaneige.md](docs/gr4j-cemaneige.md) |
| **Sacramento** soil moisture accounting | — | `sacramento.Sacramento` | No | Legacy class; deferred in favor of GR4J path |

Use with the framework:

```python
from hydrology.enums import RunoffMethod
from shrine.simulation import CatchmentElement, Model, RunController

model.register_catchment(
"c1",
CatchmentElement(
element_id="c1",
area=1_000_000.0,
runoff_method=RunoffMethod.AWBM,
temperature_key="temperature", # Snow-17 / AWBM+Snow-17
),
)
```

Custom physics: implement `RunoffModel` in `hydrology/protocols.py` and pass an instance to `Catchment(..., runoff_method=model)`.

### Networks and routing

| Component | Module | Role |
|-----------|--------|------|
| **Catchment** | `catchment` | Single hillslope / subcatchment runoff (`area` × depth rate → volume) |
| **Watershed** | `watershed` | NetworkX graph of catchments, junctions, sink; capacity updates + max-flow or Muskingum routing via `WatershedElement` |
| **Muskingum reach** | `muskingum`, `reach_routing`, `routed_flow` | Lagged channel routing with internal substeps (`ReachRoutingConfig`); `Watershed.set_muskingum_reach` |
| **Graph payloads** | `graph_nodes` | Typed `CatchmentNode`, `JunctionNode`, `SinkNode`, `SourceNode` on graph nodes |
| **Junction** | `junction` | Legacy junction helpers |

GML test data: `src/hydrology/test_data/`. Examples: [examples/catchment_run.py](examples/catchment_run.py), [examples/watershed_run.py](examples/watershed_run.py).

**Muskingum routing (R1.5.3)** — optional lagged reaches with an internal routing time step inside each daily run step:

```python
from hydrology import Watershed
from shrine.simulation import WatershedElement

ws = Watershed()
ws.add_junction("J1", "sink")
ws.link_catchment("C1", "J1")
ws.set_muskingum_reach("J1", "sink", K=2.0, X=0.2, n_substeps=24)

element = WatershedElement(ws, element_id="ws1", reach_routing="muskingum")
```

**Prerun flow slots (R1.5.4)** — warm Muskingum state before the first recorded step (RiverWare *Initialize Flow Slots for Routing*):

```python
from hydrology import PrerunConfig

# Manual warm start on the watershed graph
ws.update_capacity("C1", catchment_supply)
ws.prerun_routing(inflow_rates={"C1": catchment_supply}) # n_timesteps from max K

# Or via WatershedElement (uses first timestep supplies when inflow_rate is omitted)
element = WatershedElement(
ws,
reach_routing="muskingum",
prerun=PrerunConfig(n_timesteps=12),
)
```

Default `reach_routing="instant"` keeps zero-lag max-flow. Tests: `tests/hydrology/test_muskingum.py`, `tests/hydrology/test_routed_flow.py`, `tests/hydrology/test_prerun_routing.py`, `tests/hydrology/test_inflow_hydrograph.py`. See [reservoir-roadmap §8](docs/reservoir-roadmap.md#8-temporal-discretization-riverware-alignment).

**Intra-step inflow hydrographs (R1.5.5)** — non-uniform inflow within a daily run step for pool and reach substeps:

```python
from water_manage.reservoir import SubstepConfig, ReservoirModel, default_test_curve, default_test_spillway

# Reservoir pool substeps — storm front-loaded into the day
model = ReservoirModel(
curve=default_test_curve(),
storage=174.0,
spillway=default_test_spillway(),
hydraulic_substeps=SubstepConfig(n_substeps=24, inflow_pattern="front_loaded"),
)

# Muskingum reach — triangular inflow within the day
ws.set_muskingum_reach(
"J1",
"sink",
K=2.0,
X=0.2,
n_substeps=24,
inflow_pattern="triangular",
)
```

Patterns: ``uniform`` (default), ``front_loaded``, ``back_loaded``, ``triangular``, or ``custom`` with explicit ``inflow_weights``.

More hydrology (kinematic wave, etc.) is tracked on the [river–reservoir roadmap](docs/river-reservoir-roadmap.md). **Reservoir operations** (level-pool routing, outlet rating curves, rule curves, hydropower) have a dedicated checklist: [docs/reservoir-roadmap.md](docs/reservoir-roadmap.md). When you add a model, extend the table above and link any verification doc or test module.

## Reservoir operations (`src/water_manage/reservoir/`)

Composable **level-pool** reservoir model for `ReservoirElement` (roadmap phases R0–R5). Legacy monolithic `Reservoir` remains in `reservoir/legacy.py` for backward compatibility.

| Component | Module | Status |
|-----------|--------|--------|
| **StageStorageCurve** | `stage_storage` | Elevation ↔ storage ↔ area lookup |
| **LevelPoolRouter** | `level_pool` | Daily mass balance `S' = S + I − Q` |
| **ReservoirModel** | `model` | `StorageElement` backend; optional outlet + spillway |
| **ReservoirNetwork** | `reservoir_network` | Coupled pool → reach → pool networks (`ReservoirNetworkElement`) |
| **OutletWorks** | `outlet_works` | Head–discharge rating; intake invert; gate opening |
| **Spillway** | `spillway` | Broad-crested weir; sharp/ogee stubs |
| **Hydraulic substeps** | `hydraulic_substeps` | Optional intra-day pool updates with configurable inflow hydrograph; see [reservoir-roadmap §8](docs/reservoir-roadmap.md#8-temporal-discretization-riverware-alignment) |
| **RuleCurve / OperationalRules** | `rule_curve`, `operational_rules` | Seasonal target pool; hold-target release |
| **Hydropower** | `hydropower` | Fixed-η turbine plant |

Cross-check vs RiverWare / HEC-ResSim: [docs/reservoir-riverware-verification.md](docs/reservoir-riverware-verification.md) (R5.4). Tutorial: [examples/reservoir_rule_curve.py](examples/reservoir_rule_curve.py) (R5.3).

Reference scenario: `scenarios/reference/simple_level_pool_reservoir.yaml` (REF-RES-LP). Multi-reservoir network reference: `scenarios/reference/twin_reservoir_network.yaml` (REF-RES-NET-01).

**Multi-reservoir network (R5.1)** — instant reaches between pools (zero lag within a run step). **R5.2** adds Muskingum lag on inter-reservoir edges. Tests: `tests/water_manage/test_reservoir_network.py`, `tests/simulation/test_reservoir_network_adapter.py`.

```python
from hydrology.reach_routing import PrerunConfig, ReachRoutingConfig
from shrine.simulation import (
Clock,
ConstantInput,
InputManager,
Model,
ReservoirNetworkElement,
RunController,
)
from water_manage.reservoir import ReservoirModel, ReservoirNetwork, default_test_curve

network = ReservoirNetwork()
network.add_reservoir("upper", ReservoirModel(curve=default_test_curve(), storage=500.0))
network.add_reservoir("lower", ReservoirModel(curve=default_test_curve(), storage=50.0))
network.link_muskingum_reach(
"upper",
"lower",
K=2.0,
X=0.2,
routing=ReachRoutingConfig(n_substeps=24),
)

model = Model(clock=Clock("1/1/2019", "1/3/2019"))
model.register_reservoir_network(
"system",
ReservoirNetworkElement(network, element_id="system", prerun=PrerunConfig(n_timesteps=12)),
)

inputs = InputManager()
inputs.bind("upper.inflow", ConstantInput(8.0))
inputs.bind("upper.release", ConstantInput(8.0))
inputs.bind("lower.release", ConstantInput(0.0))

result = RunController(model, input_manager=inputs).run()
assert result.outputs["system.lower.routed_inflow"].iloc[0] > 0.0
```

Instant reaches (R5.1 only) use ``network.link_reach("upper", "lower")`` instead of ``link_muskingum_reach``.

```python
from water_manage.reservoir import (
OutletRatingCurve,
OutletWorks,
ReservoirModel,
default_test_curve,
default_test_spillway,
)
from shrine.simulation import ReservoirElement, Model, RunController, ConstantInput, InputManager

backend = ReservoirModel(
curve=default_test_curve(),
storage=50.0,
outlet=OutletWorks(
invert_elevation=3.75,
rating=OutletRatingCurve.power_law(coefficient=3.2, exponent=1.5),
),
spillway=default_test_spillway(),
)
model = Model()
model.register("res1", ReservoirElement(backend, element_id="res1"))
inputs = InputManager()
inputs.bind("inflow", ConstantInput(10.0))
inputs.bind("release", ConstantInput(5.0))
RunController(model, input_manager=inputs).run()
```

## Agronomic irrigated fields (`src/hydrology/agro/`)

Daily **FAO-56** crop evapotranspiration, root-zone soil water balance, and **irrigation demand** for irrigated land — separate from rainfall–runoff catchments and from reservoir **release allocation** (`ReleaseDemand`). Physics lives in **`hydrology.agro`**; the simulation adapter is **`IrrigatedFieldElement`**.

| Component | Module | Role |
|-----------|--------|------|
| **ET0** | `et0`, `weather` | Penman–Monteith reference ET (explicit inputs only) |
| **Crop ET** | `crop`, `etc`, `etc_dual`, `catalog` | Stage calendars, single/dual Kc, built-in crop presets |
| **Soil** | `soil`, `soil_catalog` | TAW/RAW, layered profiles, soil presets |
| **Balance** | `balance` | Daily depletion, stress `Ks`, net/gross irrigation |
| **Field config** | `field`, `irrigation` | `IrrigatedFieldConfig` — crop, soil, method, area, site |
| **GIS import** | `gis_adapter`, `soil_class_lookup`, `crop_landuse_lookup` | Parcel rows → field configs (no GeoPandas in core) |
| **Batch scenarios** | `scenario_batch` | Many fields + one shared climate CSV → scenario YAML |

**Irrigation demand** is recorded as **`gross_irrigation`** (mm/day gross applied depth). Coupling to a reservoir or other supply:

| Pattern | Status | How |
|---------|--------|-----|
| **Open-loop** | Documented | Bind the same schedule to field `irrigation_applied` and reservoir `release` (user converts mm → m³/day). |
| **Closed-loop** | Shipped | Register supply **before** the field; set `supply_element_id` on `IrrigatedFieldElement` (or scenario override). Field reads supply outflow same timestep. |

Units bridge: `release_m³/day = gross_irrigation_mm/day × area_m² / 1000`. Full patterns: [docs/agro-irrigation-supply.md](docs/agro-irrigation-supply.md).

Reference scenarios: [`irrigated_field_fao56.yaml`](scenarios/reference/irrigated_field_fao56.yaml) (**REF-AGRO-01**), [`irrigated_field_elements.yaml`](scenarios/reference/irrigated_field_elements.yaml) (**REF-AGRO-01-ELEMENTS** — declarative `elements`), [`reservoir_irrigated_field.yaml`](scenarios/reference/reservoir_irrigated_field.yaml) (**REF-AGRO-02**). GIS and batch YAML: [docs/agro-gis-field-adapter.md](docs/agro-gis-field-adapter.md). Roadmap: [docs/agro-fao56-roadmap.md](docs/agro-fao56-roadmap.md) (Phases A0–C1).

```python
from hydrology.agro import build_irrigated_fields_from_dicts, fields_from_rows
from shrine.simulation import (
Clock,
IrrigatedFieldElement,
Model,
ReservoirElement,
load_scenario_file,
run_scenario,
)
from water_manage.reservoir import ReservoirModel, default_test_curve

# Programmatic field (or fields_from_rows for GIS attribute tables)
field = build_irrigated_fields_from_dicts(
[
{
"field_id": "north_40",
"area_m2": 40_000.0,
"crop_preset_id": "corn",
"soil_preset_id": "silty",
"irrigation_preset_id": "drip",
"location": {"latitude": 40.5, "elevation_m": 350.0},
}
]
)[0]

model = Model(clock=Clock("6/1/2019", "6/10/2019"))
model.register(
"supply",
ReservoirElement(
ReservoirModel(curve=default_test_curve(), storage=500.0),
element_id="supply",
),
)
model.register_irrigated_field(
"north_40",
IrrigatedFieldElement(field, supply_element_id="supply"),
)

result = run_scenario(model, load_scenario_file("scenarios/reference/reservoir_irrigated_field.yaml"))
```

Examples: [examples/build_irrigated_fields.py](examples/build_irrigated_fields.py), [examples/build_agro_batch_scenario.py](examples/build_agro_batch_scenario.py), [examples/fields_from_geodataframe.py](examples/fields_from_geodataframe.py) (optional GeoPandas). Tests: `tests/hydrology/agro/`, `tests/simulation/test_irrigated_field*.py`.

## Project layout

| Area | Description |
|------|-------------|
| `src/shrine/simulation/` | Framework: clock, model, run controller, inputs, recorder, scenarios |
| `src/hydrology/` | Catchments, watersheds, networks, **FAO-56 agronomic** (`agro/`) |
| `src/water_manage/` | Storage, flow networks, **composable reservoir model** (`reservoir/`) |
| `src/inputs/`, `src/results/` | Tables, time series, `TimeHistory`, charts |
| `examples/` | Runnable demos (climate, watershed, scenarios, stepping) |
| `shrine-element-cookiecutter/` | Cookiecutter template for third-party `shrine.elements` plugins ([guide](docs/cookiecutter-element.md)) |
| `tests/simulation/` | Framework unit and acceptance tests |
| `docs/` | Guides (see below) |

Library code is under `src/`; run `pip install -e ".[dev]"` before tests or scripts (see [docs/testing.md](docs/testing.md)).

## Prerequisites

- Python **3.10+**
- WSL/Linux or Windows with WSL recommended for `./scripts/run_tests.sh`

## Install

**PyPI:** `pip install shrine` — see [docs/install.md](docs/install.md) (extras, PEP 668, wheel vs clone). **Development:** clone + editable install below.

### From PyPI

```bash
pip install shrine
pip install "shrine[dev,viz,hydrology]"
```

The PyPI wheel includes Python packages and `examples/`; **clone the repo** for bundled `scenarios/` and `./scripts/run_tests.sh`.

### From source *(development)*

```bash
git clone https://github.com/jlillywh/SHRINE.git
cd SHRINE
bash scripts/bootstrap_venv.sh # .venv + pip install -e ".[dev]"
```

Contributors (tests + plotting + NWIS demo):

```bash
.venv/bin/python3 -m pip install -e ".[dev,viz,hydrology]"
```

On Ubuntu/WSL, use `.venv/bin/python3` and `.venv/bin/pip` — system `pip install` is blocked (PEP 668). See [docs/install.md](docs/install.md).

### Optional dependency extras

Defined in `pyproject.toml` under `[project.optional-dependencies]`. Same extra names for source and PyPI.

| Extra | Purpose | Source | PyPI |
|-------|---------|--------|------|
| *(none)* | Core runtime | `pip install -e .` | `pip install shrine` |
| `dev` | pytest, mypy, ruff, pre-commit | `pip install -e ".[dev]"` | `pip install "shrine[dev]"` |
| `docs` | MkDocs site | `pip install -e ".[docs]"` | `pip install "shrine[docs]"` |
| `viz` | matplotlib | `pip install -e ".[viz]"` | `pip install "shrine[viz]"` |
| `hydrology` | NWIS demo (`examples/nwis_streamflow.py`) | `pip install -e ".[hydrology]"` | `pip install "shrine[hydrology]"` |

**Recommended for contributors:**

```bash
pip install -e ".[dev,viz,hydrology]"
```

**Framework-only CI** (matches `./scripts/run_tests.sh`):

```bash
pip install -e ".[dev]"
```

## Run tests

```bash
./scripts/run_tests.sh
```

Or manually:

```bash
pytest tests/ -v
pytest tests/simulation --cov=shrine.simulation --cov-report=term-missing
```

See [docs/testing.md](docs/testing.md) for layout and troubleshooting (WSL sync, venv, etc.).

## Secrets and credentials

Do **not** commit API keys or `.env` files. Use `GOOGLE_MAPS_API_KEY` for the optional Maps demo, or a local gitignored `data_external/apikey.txt` (see `data_external/apikey.txt.example`). Full guidance: **[docs/secrets-and-repo-hygiene.md](docs/secrets-and-repo-hygiene.md)**. Optional local hook (with venv activated): `pip install -e ".[dev]" && pre-commit install` — gitleaks on commit; CI runs the same scan on push/PR.

## Examples

From the repo root with the venv activated:

```bash
# Climate inputs via framework (no Excel)
python examples/climate_loop.py

# Two catchments → junction → flow solve
python examples/watershed_run.py

# Single catchment, rational runoff (no network)
python examples/catchment_run.py

# Rule-curve reservoir tutorial (R5.3)
python examples/reservoir_rule_curve.py

# Scenario file (JSON/YAML)
python examples/run_from_scenario.py scenarios/baseline_watershed.json

# Tutorial: watershed + monthly scenario + plot (roadmap 3.3)
python examples/tutorial_watershed.py --no-show --output tutorial_plot.png

# Single-timestep debugging
python examples/step_debug.py

# Minimal custom element
python examples/custom_element.py

# USGS NWIS fetch (optional: pip install -e ".[hydrology]")
python examples/nwis_streamflow.py

# Irrigated fields from parcel attributes; batch scenario YAML (FAO-56 C0)
python examples/build_irrigated_fields.py
python examples/build_agro_batch_scenario.py

# Reference regression cases (includes REF-AGRO-01/01-ELEMENTS/02)
python examples/run_reference_scenario.py irrigated_field_fao56
python examples/run_reference_scenario.py irrigated_field_elements
```

Bundled scenarios: `scenarios/baseline_watershed.json`, `scenarios/wet_year.yaml`, `scenarios/reference/` (watershed, reservoir, **irrigated-field** references).

## Documentation

**Online docs:** [https://jlillywh.github.io/SHRINE/](https://jlillywh.github.io/SHRINE/) (GitHub Pages — enable *Settings → Pages → GitHub Actions* after the first `Docs` workflow run on `master`).

Build locally:

```bash
pip install -e ".[docs]"
mkdocs serve # http://127.0.0.1:8000
# or: ./scripts/build_docs.sh
```

| Guide | Topic |
|-------|--------|
| [Architecture](https://jlillywh.github.io/SHRINE/architecture/) | **Framework vs domain vs adapters** (diagrams) |
| [Comparison with other tools](https://jlillywh.github.io/SHRINE/comparison/) | SHRINE vs PySWMM, WEAP, ResSim, Spotpy, … (honest scope) |
| [docs/project-name.md](docs/project-name.md) | **SHRINE** naming and acronym |
| [docs/modernization-roadmap.md](docs/modernization-roadmap.md) | Strategic checklist: pythonic OOP, OSS excellence |
| [docs/api-stability.md](docs/api-stability.md) | SemVer, deprecation cycle, public API policy |
| [CHANGELOG.md](CHANGELOG.md) | Release history ([Keep a Changelog](https://keepachangelog.com/)) |
| [GOVERNANCE.md](GOVERNANCE.md) | Maintainer, release manager, lazy consensus |
| [SECURITY.md](SECURITY.md) | Vulnerability reporting and supported versions |
| [docs/releases.md](docs/releases.md) | Versioning policy and maintainer release checklist |
| [docs/simulation-framework-requirements.md](docs/simulation-framework-requirements.md) | Architecture decisions and requirements |
| [docs/extending-elements.md](docs/extending-elements.md) | Adding new `Simulatable` elements |
| [docs/cookiecutter-element.md](docs/cookiecutter-element.md) | Cookiecutter template for plugin packages |
| [docs/scenarios.md](docs/scenarios.md) | Scenario YAML/JSON |
| [docs/step-debugging.md](docs/step-debugging.md) | `RunController.step()` API |
| [docs/results-recording.md](docs/results-recording.md) | `Recorder` and `TimeHistory` |
| [docs/install.md](docs/install.md) | **Install** — source vs PyPI, extras, PEP 668, wheel vs clone |
| [docs/testing.md](docs/testing.md) | Test suite and CI-style local runs |
| [docs/hydrology-contracts.md](docs/hydrology-contracts.md) | RunoffModel, catchments, graph node contracts |
| [docs/awbm-verification.md](docs/awbm-verification.md) | AWBM verification (fixture, mass balance, eWater SRG) |
| [docs/snow17-verification.md](docs/snow17-verification.md) | Snow-17 verification (reference twin, GitHub NWS ports) |
| [docs/awbm-snow17.md](docs/awbm-snow17.md) | Combined AWBM + Snow-17 usage and units |
| [docs/snow-dominated-verification.md](docs/snow-dominated-verification.md) | Seasonal snow-dominated regression case |
| [docs/river-reservoir-roadmap.md](docs/river-reservoir-roadmap.md) | Snow, Muskingum routing, reservoir networks — hydrology roadmap |
| [docs/reservoir-roadmap.md](docs/reservoir-roadmap.md) | Level-pool reservoir, outlets, rules, hydropower (R0–R5) |
| [docs/agro-fao56-roadmap.md](docs/agro-fao56-roadmap.md) | FAO-56 irrigated fields — ET0, crop/soil balance, irrigation demand (Phases A0–C0) |
| [docs/agro-irrigation-supply.md](docs/agro-irrigation-supply.md) | Field irrigation demand ↔ reservoir release (open- and closed-loop) |
| [docs/agro-gis-field-adapter.md](docs/agro-gis-field-adapter.md) | GIS parcel rows → field configs; batch scenario YAML from shared climate CSV |
| [docs/agro-weather-forcing.md](docs/agro-weather-forcing.md) | Penman–Monteith weather inputs and explicit-only ET0 policy |
| [docs/secrets-and-repo-hygiene.md](docs/secrets-and-repo-hygiene.md) | API keys, `.env`, history purge if a secret was committed |

## Quick API sketch

```python
from shrine.simulation import (
Clock,
ConstantInput,
InputManager,
Model,
RunController,
WatershedElement,
)
from hydrology.watershed import Watershed

ws = Watershed()
ws.add_junction("J1", "sink")
ws.link_catchment("C1", "J1")

model = Model(clock=Clock("1/1/2019", "1/15/2019"))
model.register_watershed("basin", WatershedElement(ws, element_id="basin"))

inputs = InputManager()
inputs.bind("precipitation", ConstantInput(10.0))
inputs.bind("evaporation", ConstantInput(1.0))

result = RunController(model, input_manager=inputs, seed=42).run()
print(result.outputs.head())
```

Legacy scripts (e.g. `global_attributes/test_model.py`) remain for reference; prefer `examples/climate_loop.py` and the framework APIs for new work.

## Community

| Need | Where |
|------|-------|
| **Questions**, ideas, show-and-tell | [GitHub Discussions](https://github.com/jlillywh/SHRINE/discussions) |
| **Bugs** and **feature requests** | [Issues](https://github.com/jlillywh/SHRINE/issues/new/choose) |
| **Good first issues** | [Issues labeled `good first issue`](https://github.com/jlillywh/SHRINE/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) |
| **Help wanted** | [Issues labeled `help wanted`](https://github.com/jlillywh/SHRINE/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) |
| **Security** vulnerabilities | [SECURITY.md](SECURITY.md) (private reporting — do not open a public issue) |

When you [start a discussion](https://github.com/jlillywh/SHRINE/discussions/new/choose), choose a category:

- **Q&A** — how-to questions, troubleshooting, API usage
- **Ideas** — feature proposals before they become issues
- **Show and tell** — scenarios, plugins, teaching examples, integrations

**Announcements** are for maintainer updates (for example the welcome thread). Please follow the [Code of Conduct](CODE_OF_CONDUCT.md).

## Contributing

We welcome pull requests. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, tests, and PR workflow. For questions, use [Discussions](https://github.com/jlillywh/SHRINE/discussions) (table above).

## License

MIT License — see [LICENSE](LICENSE).