{"id":50539303,"url":"https://github.com/jlillywh/shrine","last_synced_at":"2026-06-03T19:01:29.211Z","repository":{"id":107253419,"uuid":"159980392","full_name":"jlillywh/SHRINE","owner":"jlillywh","description":"Open-source Python library for integrated water-resources simulation: hydrology, reservoirs, flow networks, scenarios, and mass balance via shrine.simulation.","archived":false,"fork":false,"pushed_at":"2026-06-03T13:44:37.000Z","size":18722,"stargazers_count":6,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-06-03T15:20:49.251Z","etag":null,"topics":["hydrology","open-source","python","python3","reservoir","simulation","water-resources","watershed"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/shrine/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jlillywh.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2018-12-01T19:37:35.000Z","updated_at":"2026-06-03T13:55:06.000Z","dependencies_parsed_at":"2025-02-22T23:33:44.078Z","dependency_job_id":null,"html_url":"https://github.com/jlillywh/SHRINE","commit_stats":{"total_commits":205,"total_committers":1,"mean_commits":205.0,"dds":0.0,"last_synced_commit":"1a2031d651df6ce4a9cfcf3f4aaa97ead201c86d"},"previous_names":["jlillywh/shrine"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jlillywh/SHRINE","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jlillywh%2FSHRINE","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jlillywh%2FSHRINE/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jlillywh%2FSHRINE/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jlillywh%2FSHRINE/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jlillywh","download_url":"https://codeload.github.com/jlillywh/SHRINE/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jlillywh%2FSHRINE/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33876333,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hydrology","open-source","python","python3","reservoir","simulation","water-resources","watershed"],"created_at":"2026-06-03T19:01:28.272Z","updated_at":"2026-06-03T19:01:29.204Z","avatar_url":"https://github.com/jlillywh.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![PyPI version](https://img.shields.io/pypi/v/shrine?logo=pypi\u0026logoColor=white\u0026label=version\u0026v=0.3.0)](https://pypi.org/project/shrine/)\n[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)\n[![codecov](https://codecov.io/gh/jlillywh/SHRINE/graph/badge.svg?branch=master)](https://codecov.io/gh/jlillywh/SHRINE)\n\n# SHRINE\n\n**Simulation of Hydrology, Reservoirs, and Integrated Network Environments**\n\nSHRINE 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.\n\nUnder 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.\n\nThis 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.\n\nNaming details: [docs/project-name.md](docs/project-name.md).\n\n## Simulation framework (`shrine.simulation`)\n\nThe framework is the **only supported path** for new model runs. Import from the package root:\n\n```python\nimport shrine.simulation as sim\n# or\nfrom shrine.simulation import Model, RunController, Clock, WatershedElement\n```\n\n**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).\n\n### Public API\n\n| Category | Symbols |\n|----------|---------|\n| **Run** | `Model`, `RegisteredElement`, `RunController`, `RunResult`, `RunSession`, `StepResult`, `ElementScheduler` |\n| **Time** | `Clock`, `RunContext`, `TimestepContext` |\n| **Elements** | `Simulatable`, `WatershedElement`, `CatchmentElement`, `ReservoirElement`, `ReservoirNetworkElement`, `IrrigatedFieldElement`, `ClimateRecorderElement`, `StorageLike` |\n| **Plugins** | `list_element_plugins`, `load_element_plugin`, `create_element_from_plugin` (`shrine.elements` entry points) |\n| **Inputs** | `InputManager`, `InputProvider`, `ConstantInput`, `MonthlyLookupInput`, `StochasticInput` |\n| **Flow / balance** | `FlowSolver`, `NetworkXFlowSolver`, `FlowSolveResult`, `MassBalanceCheck`, `MassBalanceReport`, `MassBalanceTerm` |\n| **Outputs / scenarios** | `Recorder`, `ScenarioConfig`, `load_scenario_file`, `run_scenario`, `run_scenarios`, `load_and_run` |\n| **Metadata / RNG** | `build_run_metadata`, `enrich_run_metadata`, `RunTimer`, `make_rng` |\n| **Deprecation** | `warn_api_deprecated` |\n| **Errors** | `SimulationError`, `SimulationPhase` |\n\nCapabilities:\n\n- **`Model`** — register elements (watersheds, reservoirs, custom types) with a shared `Clock`\n- **`RunController`** — validate → initialize → timestep loop → finalize\n- **`InputManager`** — constant, monthly, and stochastic inputs bound by name\n- **`Recorder`** — wide `pandas` DataFrame outputs per run\n- **Scenarios** — YAML/JSON clock, inputs, and parameter overrides\n- **Flow solve** — NetworkX max-flow on graphs owned by `Watershed` adapters\n- **Mass balance** — per-timestep verification with `SimulationError` diagnostics\n\nRequirements and phased delivery: [docs/simulation-framework-requirements.md](docs/simulation-framework-requirements.md). Layer diagram: [docs/architecture.md](docs/architecture.md).\n\n## Hydrology (`src/hydrology/`)\n\nLegacy 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).\n\n### Rainfall–runoff models\n\n| Model | `RunoffMethod` | Module | Framework-ready | Notes |\n|-------|----------------|--------|-----------------|-------|\n| **Rational** (simple loss) | `SIMPLE` | `catchment.Rational` | Yes | Default; fast sanity checks and reference scenarios |\n| **AWBM** (Australian Water Balance Model) | `AWBM` | `awbm.Awbm` | Yes | Boughton (2004); verified via fixture + mass balance — [docs/awbm-verification.md](docs/awbm-verification.md) |\n| **Snow-17** (temperature-index snow) | `SNOW17` | `snow17.Snow17RunoffModel` | Yes | Independent reference twin + fixture — [docs/snow17-verification.md](docs/snow17-verification.md) |\n| **AWBM + Snow-17** | `AWBM_SNOW17` | `snow17.AWBM_Snow17_RunoffModel` | Yes | Snowpack → AWBM soil; [docs/awbm-snow17.md](docs/awbm-snow17.md) |\n| **GR4J** | `GR4J` | `gr4j.Gr4jRunoffModel` | Yes | airGR `frun_gr4j` port — [docs/gr4j-cemaneige-verification.md](docs/gr4j-cemaneige-verification.md) |\n| **GR4J + CemaNeige** | `GR4J_CEMANNEIGE` | `gr4j_cemaneige.GR4J_CemaneigeRunoffModel` | Yes | INRAE snow + runoff — [docs/gr4j-cemaneige.md](docs/gr4j-cemaneige.md) |\n| **Sacramento** soil moisture accounting | — | `sacramento.Sacramento` | No | Legacy class; deferred in favor of GR4J path |\n\nUse with the framework:\n\n```python\nfrom hydrology.enums import RunoffMethod\nfrom shrine.simulation import CatchmentElement, Model, RunController\n\nmodel.register_catchment(\n    \"c1\",\n    CatchmentElement(\n        element_id=\"c1\",\n        area=1_000_000.0,\n        runoff_method=RunoffMethod.AWBM,\n        temperature_key=\"temperature\",  # Snow-17 / AWBM+Snow-17\n    ),\n)\n```\n\nCustom physics: implement `RunoffModel` in `hydrology/protocols.py` and pass an instance to `Catchment(..., runoff_method=model)`.\n\n### Networks and routing\n\n| Component | Module | Role |\n|-----------|--------|------|\n| **Catchment** | `catchment` | Single hillslope / subcatchment runoff (`area` × depth rate → volume) |\n| **Watershed** | `watershed` | NetworkX graph of catchments, junctions, sink; capacity updates + max-flow or Muskingum routing via `WatershedElement` |\n| **Muskingum reach** | `muskingum`, `reach_routing`, `routed_flow` | Lagged channel routing with internal substeps (`ReachRoutingConfig`); `Watershed.set_muskingum_reach` |\n| **Graph payloads** | `graph_nodes` | Typed `CatchmentNode`, `JunctionNode`, `SinkNode`, `SourceNode` on graph nodes |\n| **Junction** | `junction` | Legacy junction helpers |\n\nGML test data: `src/hydrology/test_data/`. Examples: [examples/catchment_run.py](examples/catchment_run.py), [examples/watershed_run.py](examples/watershed_run.py).\n\n**Muskingum routing (R1.5.3)** — optional lagged reaches with an internal routing time step inside each daily run step:\n\n```python\nfrom hydrology import Watershed\nfrom shrine.simulation import WatershedElement\n\nws = Watershed()\nws.add_junction(\"J1\", \"sink\")\nws.link_catchment(\"C1\", \"J1\")\nws.set_muskingum_reach(\"J1\", \"sink\", K=2.0, X=0.2, n_substeps=24)\n\nelement = WatershedElement(ws, element_id=\"ws1\", reach_routing=\"muskingum\")\n```\n\n**Prerun flow slots (R1.5.4)** — warm Muskingum state before the first recorded step (RiverWare *Initialize Flow Slots for Routing*):\n\n```python\nfrom hydrology import PrerunConfig\n\n# Manual warm start on the watershed graph\nws.update_capacity(\"C1\", catchment_supply)\nws.prerun_routing(inflow_rates={\"C1\": catchment_supply})  # n_timesteps from max K\n\n# Or via WatershedElement (uses first timestep supplies when inflow_rate is omitted)\nelement = WatershedElement(\n    ws,\n    reach_routing=\"muskingum\",\n    prerun=PrerunConfig(n_timesteps=12),\n)\n```\n\nDefault `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).\n\n**Intra-step inflow hydrographs (R1.5.5)** — non-uniform inflow within a daily run step for pool and reach substeps:\n\n```python\nfrom water_manage.reservoir import SubstepConfig, ReservoirModel, default_test_curve, default_test_spillway\n\n# Reservoir pool substeps — storm front-loaded into the day\nmodel = ReservoirModel(\n    curve=default_test_curve(),\n    storage=174.0,\n    spillway=default_test_spillway(),\n    hydraulic_substeps=SubstepConfig(n_substeps=24, inflow_pattern=\"front_loaded\"),\n)\n\n# Muskingum reach — triangular inflow within the day\nws.set_muskingum_reach(\n    \"J1\",\n    \"sink\",\n    K=2.0,\n    X=0.2,\n    n_substeps=24,\n    inflow_pattern=\"triangular\",\n)\n```\n\nPatterns: ``uniform`` (default), ``front_loaded``, ``back_loaded``, ``triangular``, or ``custom`` with explicit ``inflow_weights``.\n\nMore 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.\n\n## Reservoir operations (`src/water_manage/reservoir/`)\n\nComposable **level-pool** reservoir model for `ReservoirElement` (roadmap phases R0–R5). Legacy monolithic `Reservoir` remains in `reservoir/legacy.py` for backward compatibility.\n\n| Component | Module | Status |\n|-----------|--------|--------|\n| **StageStorageCurve** | `stage_storage` | Elevation ↔ storage ↔ area lookup |\n| **LevelPoolRouter** | `level_pool` | Daily mass balance `S' = S + I − Q` |\n| **ReservoirModel** | `model` | `StorageElement` backend; optional outlet + spillway |\n| **ReservoirNetwork** | `reservoir_network` | Coupled pool → reach → pool networks (`ReservoirNetworkElement`) |\n| **OutletWorks** | `outlet_works` | Head–discharge rating; intake invert; gate opening |\n| **Spillway** | `spillway` | Broad-crested weir; sharp/ogee stubs |\n| **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) |\n| **RuleCurve / OperationalRules** | `rule_curve`, `operational_rules` | Seasonal target pool; hold-target release |\n| **Hydropower** | `hydropower` | Fixed-η turbine plant |\n\nCross-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).\n\nReference 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).\n\n**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`.\n\n```python\nfrom hydrology.reach_routing import PrerunConfig, ReachRoutingConfig\nfrom shrine.simulation import (\n    Clock,\n    ConstantInput,\n    InputManager,\n    Model,\n    ReservoirNetworkElement,\n    RunController,\n)\nfrom water_manage.reservoir import ReservoirModel, ReservoirNetwork, default_test_curve\n\nnetwork = ReservoirNetwork()\nnetwork.add_reservoir(\"upper\", ReservoirModel(curve=default_test_curve(), storage=500.0))\nnetwork.add_reservoir(\"lower\", ReservoirModel(curve=default_test_curve(), storage=50.0))\nnetwork.link_muskingum_reach(\n    \"upper\",\n    \"lower\",\n    K=2.0,\n    X=0.2,\n    routing=ReachRoutingConfig(n_substeps=24),\n)\n\nmodel = Model(clock=Clock(\"1/1/2019\", \"1/3/2019\"))\nmodel.register_reservoir_network(\n    \"system\",\n    ReservoirNetworkElement(network, element_id=\"system\", prerun=PrerunConfig(n_timesteps=12)),\n)\n\ninputs = InputManager()\ninputs.bind(\"upper.inflow\", ConstantInput(8.0))\ninputs.bind(\"upper.release\", ConstantInput(8.0))\ninputs.bind(\"lower.release\", ConstantInput(0.0))\n\nresult = RunController(model, input_manager=inputs).run()\nassert result.outputs[\"system.lower.routed_inflow\"].iloc[0] \u003e 0.0\n```\n\nInstant reaches (R5.1 only) use ``network.link_reach(\"upper\", \"lower\")`` instead of ``link_muskingum_reach``.\n\n```python\nfrom water_manage.reservoir import (\n    OutletRatingCurve,\n    OutletWorks,\n    ReservoirModel,\n    default_test_curve,\n    default_test_spillway,\n)\nfrom shrine.simulation import ReservoirElement, Model, RunController, ConstantInput, InputManager\n\nbackend = ReservoirModel(\n    curve=default_test_curve(),\n    storage=50.0,\n    outlet=OutletWorks(\n        invert_elevation=3.75,\n        rating=OutletRatingCurve.power_law(coefficient=3.2, exponent=1.5),\n    ),\n    spillway=default_test_spillway(),\n)\nmodel = Model()\nmodel.register(\"res1\", ReservoirElement(backend, element_id=\"res1\"))\ninputs = InputManager()\ninputs.bind(\"inflow\", ConstantInput(10.0))\ninputs.bind(\"release\", ConstantInput(5.0))\nRunController(model, input_manager=inputs).run()\n```\n\n## Agronomic irrigated fields (`src/hydrology/agro/`)\n\nDaily **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`**.\n\n| Component | Module | Role |\n|-----------|--------|------|\n| **ET0** | `et0`, `weather` | Penman–Monteith reference ET (explicit inputs only) |\n| **Crop ET** | `crop`, `etc`, `etc_dual`, `catalog` | Stage calendars, single/dual Kc, built-in crop presets |\n| **Soil** | `soil`, `soil_catalog` | TAW/RAW, layered profiles, soil presets |\n| **Balance** | `balance` | Daily depletion, stress `Ks`, net/gross irrigation |\n| **Field config** | `field`, `irrigation` | `IrrigatedFieldConfig` — crop, soil, method, area, site |\n| **GIS import** | `gis_adapter`, `soil_class_lookup`, `crop_landuse_lookup` | Parcel rows → field configs (no GeoPandas in core) |\n| **Batch scenarios** | `scenario_batch` | Many fields + one shared climate CSV → scenario YAML |\n\n**Irrigation demand** is recorded as **`gross_irrigation`** (mm/day gross applied depth). Coupling to a reservoir or other supply:\n\n| Pattern | Status | How |\n|---------|--------|-----|\n| **Open-loop** | Documented | Bind the same schedule to field `irrigation_applied` and reservoir `release` (user converts mm → m³/day). |\n| **Closed-loop** | Shipped | Register supply **before** the field; set `supply_element_id` on `IrrigatedFieldElement` (or scenario override). Field reads supply outflow same timestep. |\n\nUnits bridge: `release_m³/day = gross_irrigation_mm/day × area_m² / 1000`. Full patterns: [docs/agro-irrigation-supply.md](docs/agro-irrigation-supply.md).\n\nReference 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).\n\n```python\nfrom hydrology.agro import build_irrigated_fields_from_dicts, fields_from_rows\nfrom shrine.simulation import (\n    Clock,\n    IrrigatedFieldElement,\n    Model,\n    ReservoirElement,\n    load_scenario_file,\n    run_scenario,\n)\nfrom water_manage.reservoir import ReservoirModel, default_test_curve\n\n# Programmatic field (or fields_from_rows for GIS attribute tables)\nfield = build_irrigated_fields_from_dicts(\n    [\n        {\n            \"field_id\": \"north_40\",\n            \"area_m2\": 40_000.0,\n            \"crop_preset_id\": \"corn\",\n            \"soil_preset_id\": \"silty\",\n            \"irrigation_preset_id\": \"drip\",\n            \"location\": {\"latitude\": 40.5, \"elevation_m\": 350.0},\n        }\n    ]\n)[0]\n\nmodel = Model(clock=Clock(\"6/1/2019\", \"6/10/2019\"))\nmodel.register(\n    \"supply\",\n    ReservoirElement(\n        ReservoirModel(curve=default_test_curve(), storage=500.0),\n        element_id=\"supply\",\n    ),\n)\nmodel.register_irrigated_field(\n    \"north_40\",\n    IrrigatedFieldElement(field, supply_element_id=\"supply\"),\n)\n\nresult = run_scenario(model, load_scenario_file(\"scenarios/reference/reservoir_irrigated_field.yaml\"))\n```\n\nExamples: [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`.\n\n## Project layout\n\n| Area | Description |\n|------|-------------|\n| `src/shrine/simulation/` | Framework: clock, model, run controller, inputs, recorder, scenarios |\n| `src/hydrology/` | Catchments, watersheds, networks, **FAO-56 agronomic** (`agro/`) |\n| `src/water_manage/` | Storage, flow networks, **composable reservoir model** (`reservoir/`) |\n| `src/inputs/`, `src/results/` | Tables, time series, `TimeHistory`, charts |\n| `examples/` | Runnable demos (climate, watershed, scenarios, stepping) |\n| `shrine-element-cookiecutter/` | Cookiecutter template for third-party `shrine.elements` plugins ([guide](docs/cookiecutter-element.md)) |\n| `tests/simulation/` | Framework unit and acceptance tests |\n| `docs/` | Guides (see below) |\n\nLibrary code is under `src/`; run `pip install -e \".[dev]\"` before tests or scripts (see [docs/testing.md](docs/testing.md)).\n\n## Prerequisites\n\n- Python **3.10+**\n- WSL/Linux or Windows with WSL recommended for `./scripts/run_tests.sh`\n\n## Install\n\n**PyPI:** `pip install shrine` — see [docs/install.md](docs/install.md) (extras, PEP 668, wheel vs clone). **Development:** clone + editable install below.\n\n### From PyPI\n\n```bash\npip install shrine\npip install \"shrine[dev,viz,hydrology]\"\n```\n\nThe PyPI wheel includes Python packages and `examples/`; **clone the repo** for bundled `scenarios/` and `./scripts/run_tests.sh`.\n\n### From source *(development)*\n\n```bash\ngit clone https://github.com/jlillywh/SHRINE.git\ncd SHRINE\nbash scripts/bootstrap_venv.sh    # .venv + pip install -e \".[dev]\"\n```\n\nContributors (tests + plotting + NWIS demo):\n\n```bash\n.venv/bin/python3 -m pip install -e \".[dev,viz,hydrology]\"\n```\n\nOn Ubuntu/WSL, use `.venv/bin/python3` and `.venv/bin/pip` — system `pip install` is blocked (PEP 668). See [docs/install.md](docs/install.md).\n\n### Optional dependency extras\n\nDefined in `pyproject.toml` under `[project.optional-dependencies]`. Same extra names for source and PyPI.\n\n| Extra | Purpose | Source | PyPI |\n|-------|---------|--------|------|\n| *(none)* | Core runtime | `pip install -e .` | `pip install shrine` |\n| `dev` | pytest, mypy, ruff, pre-commit | `pip install -e \".[dev]\"` | `pip install \"shrine[dev]\"` |\n| `docs` | MkDocs site | `pip install -e \".[docs]\"` | `pip install \"shrine[docs]\"` |\n| `viz` | matplotlib | `pip install -e \".[viz]\"` | `pip install \"shrine[viz]\"` |\n| `hydrology` | NWIS demo (`examples/nwis_streamflow.py`) | `pip install -e \".[hydrology]\"` | `pip install \"shrine[hydrology]\"` |\n\n**Recommended for contributors:**\n\n```bash\npip install -e \".[dev,viz,hydrology]\"\n```\n\n**Framework-only CI** (matches `./scripts/run_tests.sh`):\n\n```bash\npip install -e \".[dev]\"\n```\n\n## Run tests\n\n```bash\n./scripts/run_tests.sh\n```\n\nOr manually:\n\n```bash\npytest tests/ -v\npytest tests/simulation --cov=shrine.simulation --cov-report=term-missing\n```\n\nSee [docs/testing.md](docs/testing.md) for layout and troubleshooting (WSL sync, venv, etc.).\n\n## Secrets and credentials\n\nDo **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]\" \u0026\u0026 pre-commit install` — gitleaks on commit; CI runs the same scan on push/PR.\n\n## Examples\n\nFrom the repo root with the venv activated:\n\n```bash\n# Climate inputs via framework (no Excel)\npython examples/climate_loop.py\n\n# Two catchments → junction → flow solve\npython examples/watershed_run.py\n\n# Single catchment, rational runoff (no network)\npython examples/catchment_run.py\n\n# Rule-curve reservoir tutorial (R5.3)\npython examples/reservoir_rule_curve.py\n\n# Scenario file (JSON/YAML)\npython examples/run_from_scenario.py scenarios/baseline_watershed.json\n\n# Tutorial: watershed + monthly scenario + plot (roadmap 3.3)\npython examples/tutorial_watershed.py --no-show --output tutorial_plot.png\n\n# Single-timestep debugging\npython examples/step_debug.py\n\n# Minimal custom element\npython examples/custom_element.py\n\n# USGS NWIS fetch (optional: pip install -e \".[hydrology]\")\npython examples/nwis_streamflow.py\n\n# Irrigated fields from parcel attributes; batch scenario YAML (FAO-56 C0)\npython examples/build_irrigated_fields.py\npython examples/build_agro_batch_scenario.py\n\n# Reference regression cases (includes REF-AGRO-01/01-ELEMENTS/02)\npython examples/run_reference_scenario.py irrigated_field_fao56\npython examples/run_reference_scenario.py irrigated_field_elements\n```\n\nBundled scenarios: `scenarios/baseline_watershed.json`, `scenarios/wet_year.yaml`, `scenarios/reference/` (watershed, reservoir, **irrigated-field** references).\n\n## Documentation\n\n**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`).\n\nBuild locally:\n\n```bash\npip install -e \".[docs]\"\nmkdocs serve          # http://127.0.0.1:8000\n# or: ./scripts/build_docs.sh\n```\n\n| Guide | Topic |\n|-------|--------|\n| [Architecture](https://jlillywh.github.io/SHRINE/architecture/) | **Framework vs domain vs adapters** (diagrams) |\n| [Comparison with other tools](https://jlillywh.github.io/SHRINE/comparison/) | SHRINE vs PySWMM, WEAP, ResSim, Spotpy, … (honest scope) |\n| [docs/project-name.md](docs/project-name.md) | **SHRINE** naming and acronym |\n| [docs/modernization-roadmap.md](docs/modernization-roadmap.md) | Strategic checklist: pythonic OOP, OSS excellence |\n| [docs/api-stability.md](docs/api-stability.md) | SemVer, deprecation cycle, public API policy |\n| [CHANGELOG.md](CHANGELOG.md) | Release history ([Keep a Changelog](https://keepachangelog.com/)) |\n| [GOVERNANCE.md](GOVERNANCE.md) | Maintainer, release manager, lazy consensus |\n| [SECURITY.md](SECURITY.md) | Vulnerability reporting and supported versions |\n| [docs/releases.md](docs/releases.md) | Versioning policy and maintainer release checklist |\n| [docs/simulation-framework-requirements.md](docs/simulation-framework-requirements.md) | Architecture decisions and requirements |\n| [docs/extending-elements.md](docs/extending-elements.md) | Adding new `Simulatable` elements |\n| [docs/cookiecutter-element.md](docs/cookiecutter-element.md) | Cookiecutter template for plugin packages |\n| [docs/scenarios.md](docs/scenarios.md) | Scenario YAML/JSON |\n| [docs/step-debugging.md](docs/step-debugging.md) | `RunController.step()` API |\n| [docs/results-recording.md](docs/results-recording.md) | `Recorder` and `TimeHistory` |\n| [docs/install.md](docs/install.md) | **Install** — source vs PyPI, extras, PEP 668, wheel vs clone |\n| [docs/testing.md](docs/testing.md) | Test suite and CI-style local runs |\n| [docs/hydrology-contracts.md](docs/hydrology-contracts.md) | RunoffModel, catchments, graph node contracts |\n| [docs/awbm-verification.md](docs/awbm-verification.md) | AWBM verification (fixture, mass balance, eWater SRG) |\n| [docs/snow17-verification.md](docs/snow17-verification.md) | Snow-17 verification (reference twin, GitHub NWS ports) |\n| [docs/awbm-snow17.md](docs/awbm-snow17.md) | Combined AWBM + Snow-17 usage and units |\n| [docs/snow-dominated-verification.md](docs/snow-dominated-verification.md) | Seasonal snow-dominated regression case |\n| [docs/river-reservoir-roadmap.md](docs/river-reservoir-roadmap.md) | Snow, Muskingum routing, reservoir networks — hydrology roadmap |\n| [docs/reservoir-roadmap.md](docs/reservoir-roadmap.md) | Level-pool reservoir, outlets, rules, hydropower (R0–R5) |\n| [docs/agro-fao56-roadmap.md](docs/agro-fao56-roadmap.md) | FAO-56 irrigated fields — ET0, crop/soil balance, irrigation demand (Phases A0–C0) |\n| [docs/agro-irrigation-supply.md](docs/agro-irrigation-supply.md) | Field irrigation demand ↔ reservoir release (open- and closed-loop) |\n| [docs/agro-gis-field-adapter.md](docs/agro-gis-field-adapter.md) | GIS parcel rows → field configs; batch scenario YAML from shared climate CSV |\n| [docs/agro-weather-forcing.md](docs/agro-weather-forcing.md) | Penman–Monteith weather inputs and explicit-only ET0 policy |\n| [docs/secrets-and-repo-hygiene.md](docs/secrets-and-repo-hygiene.md) | API keys, `.env`, history purge if a secret was committed |\n\n## Quick API sketch\n\n```python\nfrom shrine.simulation import (\n    Clock,\n    ConstantInput,\n    InputManager,\n    Model,\n    RunController,\n    WatershedElement,\n)\nfrom hydrology.watershed import Watershed\n\nws = Watershed()\nws.add_junction(\"J1\", \"sink\")\nws.link_catchment(\"C1\", \"J1\")\n\nmodel = Model(clock=Clock(\"1/1/2019\", \"1/15/2019\"))\nmodel.register_watershed(\"basin\", WatershedElement(ws, element_id=\"basin\"))\n\ninputs = InputManager()\ninputs.bind(\"precipitation\", ConstantInput(10.0))\ninputs.bind(\"evaporation\", ConstantInput(1.0))\n\nresult = RunController(model, input_manager=inputs, seed=42).run()\nprint(result.outputs.head())\n```\n\nLegacy scripts (e.g. `global_attributes/test_model.py`) remain for reference; prefer `examples/climate_loop.py` and the framework APIs for new work.\n\n## Community\n\n| Need | Where |\n|------|-------|\n| **Questions**, ideas, show-and-tell | [GitHub Discussions](https://github.com/jlillywh/SHRINE/discussions) |\n| **Bugs** and **feature requests** | [Issues](https://github.com/jlillywh/SHRINE/issues/new/choose) |\n| **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) |\n| **Help wanted** | [Issues labeled `help wanted`](https://github.com/jlillywh/SHRINE/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) |\n| **Security** vulnerabilities | [SECURITY.md](SECURITY.md) (private reporting — do not open a public issue) |\n\nWhen you [start a discussion](https://github.com/jlillywh/SHRINE/discussions/new/choose), choose a category:\n\n- **Q\u0026A** — how-to questions, troubleshooting, API usage\n- **Ideas** — feature proposals before they become issues\n- **Show and tell** — scenarios, plugins, teaching examples, integrations\n\n**Announcements** are for maintainer updates (for example the welcome thread). Please follow the [Code of Conduct](CODE_OF_CONDUCT.md).\n\n## Contributing\n\nWe 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).\n\n## License\n\nMIT License — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjlillywh%2Fshrine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjlillywh%2Fshrine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjlillywh%2Fshrine/lists"}