Metadata-Version: 2.4
Name: aihydro-lsh
Version: 0.2.1
Summary: Global large-sample hydrology — CAMELS-like catchment attributes for any watershed (recipe layer over aihydro-data + aihydro-watershed)
Author-email: Mohammad Galib <mgalib@purdue.edu>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/AI-Hydro/aihydro-lsh
Project-URL: Repository, https://github.com/AI-Hydro/aihydro-lsh
Keywords: hydrology,CAMELS,large-sample,catchment-attributes,global,watershed,geospatial,agentic
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Hydrology
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: aihydro-core<0.3,>=0.2.3
Requires-Dist: aihydro-data<0.3,>=0.2.3
Requires-Dist: aihydro-watershed<0.2,>=0.1.3
Requires-Dist: numpy>=1.24.0
Requires-Dist: pandas>=2.0.0
Requires-Dist: geopandas>=0.13.0
Requires-Dist: shapely>=2.0.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: validate
Requires-Dist: camels-attrs>=1.0.4; extra == "validate"
Provides-Extra: mcp
Requires-Dist: fastmcp>=2.0.0; extra == "mcp"
Provides-Extra: trends
Requires-Dist: pymannkendall>=1.4.3; extra == "trends"
Provides-Extra: dev
Requires-Dist: pytest>=7.4.0; extra == "dev"
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Requires-Dist: build>=1.0.0; extra == "dev"
Requires-Dist: import-linter>=2.0; extra == "dev"
Provides-Extra: all
Requires-Dist: aihydro-lsh[mcp,trends,validate]; extra == "all"

# aihydro-lsh

**Global large-sample hydrology — CAMELS-like catchment attributes for globally routed watersheds, with source-specific caveats.**

A recipe layer over [`aihydro-data`](../aihydro-data/) and [`aihydro-watershed`](../aihydro-watershed/) that computes **101 static scalar attributes** (8 families) plus **daily meteorological forcing timeseries** and **temporal-tier extensions** (trends, annual dynamics, drought/flood events) for basin geometries through region-aware product routing.  The [`camels-attrs`](../camels-attrs/) CONUS reference implementation is used to validate recipes against USGS gauge data; use `aihydro_lsh.reliability.reliability_matrix()` for the family/region caveats that qualify global interpretation.

## Quick start

```python
import geopandas as gpd
from shapely.geometry import box
import aihydro_lsh

# Any watershed polygon (EPSG:4326)
geom = gpd.GeoDataFrame(geometry=[box(-79.5, 38.5, -77.5, 39.5)], crs="EPSG:4326")
result = aihydro_lsh.attributes(geom, "1990-01-01", "2020-12-31")

print(result.attrs["p_mean"])          # mean daily precip (mm/day)
print(result.attrs["elev_mean"])       # mean elevation (m)
print(result.attrs["geol_porosity"])   # GLHYMPS porosity
df = result.to_dataframe()             # single-row DataFrame, 101 columns

# Full provenance report (agent-relay verbatim — package is the authority)
print(result.to_report())

# Fully JSON-serializable dict (NaN → null)
d = result.to_json()
print(d["meta"]["content_hash"])       # SHA-256 fingerprint of attrs

# USGS gauge — auto-delineate then extract (outlet coords wired automatically)
from aihydro_watershed.delineation.router import delineate_from_point
ws = delineate_from_point(39.27, -77.54)          # Potomac Point of Rocks
result = aihydro_lsh.attributes(ws, "1990-01-01", "2020-12-31",
                                 gauge_id="01638500")
# ws is a HydroResult — outlet_lat/outlet_lon extracted automatically for geomorphic attrs

# Daily forcing timeseries (for ML workflows — LSTM, HBV training)
df = aihydro_lsh.forcing(geom, "1990-01-01", "2020-12-31")
print(df.columns.tolist())
# ['prcp_mm', 'tmin_C', 'tmax_C', 'tavg_C', 'pet_mm', 'srad_Wm2', 'wind_ms', 'sph_kgkg']

# Combined: scalar attrs + daily forcing in one call
result = aihydro_lsh.attributes(geom, "1990-01-01", "2020-12-31", include_forcing=True)
print(result.forcing)          # pd.DataFrame with DatetimeIndex

# Temporal extensions: Mann-Kendall trend scalars (T2.5)
result = aihydro_lsh.attributes(geom, "1990-01-01", "2020-12-31", include_trends=True)
print(result.trends)           # pd.DataFrame — 16 slope/p-value scalars

# Temporal extensions: annual dynamic panels (T3)
result = aihydro_lsh.attributes(geom, "1990-01-01", "2020-12-31", include_dynamic=True)
print(result.dynamic)          # pd.DataFrame (year × 22 variables)

# Temporal extensions: drought and flood event inventory (T3)
result = aihydro_lsh.attributes(geom, "1990-01-01", "2020-12-31", include_events=True)
print(result.events["drought"])    # DataFrame of SPI-drought episodes
print(result.events["flood"])      # DataFrame of POT-flood exceedances

# Everything at once
result = aihydro_lsh.attributes(
    geom, "1990-01-01", "2020-12-31",
    include_forcing=True,
    include_trends=True,
    include_dynamic=True,
    include_events=True,
)

# Batch extraction (list of pour-points → single DataFrame)
df, errors = aihydro_lsh.batch_attributes(
    [{"id": "01638500", "lat": 39.27, "lon": -77.54},
     {"id": "02108000", "lat": 35.53, "lon": -79.09}],
    start="1990-01-01", end="2020-12-31",
)
df.to_csv("camels_global.csv", index=False)
```

## Installation

```bash
pip install -e .                         # editable (ecosystem tree)
pip install -e .[validate]               # + camels-attrs CONUS reference
pip install -e .[mcp]                    # + MCP server tools
```

## Attribute families (101 static scalars, 8 families + 1 timeseries family)

| Family | Attrs | Data source | Notes |
|---|---|---|---|
| **watershed** | 5 | GDF geometry | area, perimeter, compactness, elongation, form_factor |
| **topography** | 7 | GLO30 / DEM3DEP / SRTM | elev stats, slope stats, area |
| **climate** | 14 | GRIDMET / ERA5-Land / CHIRPS | aridity, seasonality, snow frac, high/low precip stats |
| **soil** | 9 | POLARIS / SoilGrids | texture, Ksat; porosity/AWC/FC from Saxton-Rawls PTF |
| **geomorphic** | 28 | 3DEP/GLO30 + NLCD/ESA WorldCover | shape, relief, stream network, CN, sinks, land cover % |
| **vegetation** | 13 | MODIS LAI/NDVI + ESA WorldCover | LAI/GVF seasonal stats, LC fractions, root depth |
| **geology** | 9 | pygeoglim (GLiM + GLHYMPS) | lithology classes, carbonate frac, permeability, porosity; global path is permission/tile-gated |
| **hydrology** | 17 | NWIS (CONUS) / GEOGLOWS (global) | q_mean, BFI, FDC slope, half-flow date, … |
| **forcing** | timeseries | GRIDMET (CONUS) / ERA5-Land (global) | Daily: prcp_mm, tmin_C, tmax_C, tavg_C, pet_mm [+ srad, wind, sph] |

`geomorphic` runs after `soil` so `clay_frac` is available for NRCS Curve Number soil group estimation.
`forcing` is opt-in (not in the default `RECIPE_ORDER`) — request via `include_forcing=True` or `families=["forcing"]`.

## Temporal-tier extensions

The static families above are **T1** (permanent) and **T2** (window-mean over the caller's date range).  Three optional tiers extend the attribute landscape:

| Tier | Flag | Output | Description |
|---|---|---|---|
| **T2.5 — Trends** | `include_trends=True` | `result.trends` — up to 16 slope/p-value scalars | Mann–Kendall p-value and Sen slope for eight derived series: precipitation mean, temperature mean, PET mean, snow fraction, LAI maximum, GVF mean, discharge mean and runoff ratio. At least ten valid annual values are required. P-values do not yet adjust for serial dependence or multiple comparisons. |
| **T3 — Dynamic** | `include_dynamic=True` | `result.dynamic` — year × 22-column schema | Annual climate, vegetation and streamflow summaries. Three annual land-cover columns are currently unavailable and filled with NaN. Other columns can also be missing by source/year; this is a schema, not 22 measured values in every year. Annual `q5` is high flow (5% exceedance) and `q95` is low flow. |
| **T3 — Events** | `include_events=True` | `result.events["drought"]` + `result.events["flood"]` | SPI-based drought episodes (McKee et al. 1993) and peaks-over-threshold flood exceedances (Coles 2001). Each row is a discrete event with start/end date, duration, severity/magnitude. |

The temporal extensions are **opt-in** so standard `attributes()` calls remain fast.  All three can be combined with `include_forcing=True` in a single call.

### Geometry input forms

`attributes()` accepts any of:
- `HydroResult` from `delineate_from_point()` / `delineate_watershed()` — outlet coords auto-wired
- `GeoDataFrame` (EPSG:4326, single or multi-row)
- Shapely Polygon / MultiPolygon
- `(lat, lon)` tuple — wrapped as 0.001° buffer (use full delineation for accurate basin-mean attrs)
- `(minx, miny, maxx, maxy)` bbox tuple

### Provenance + reproducibility

Every `AttributeResult` carries:
- **`.provenance`** — per-family `AttrProvenance` (source, product, region, resolution_m, is_observed)
- **`.meta`** — `ResultMeta` with package versions, bbox, timing, and a SHA-256 content hash
- **`.to_report()`** — human-readable report; agents relay this verbatim (package is the authority)
- **`.to_json()`** — fully JSON-serializable dict (NaN → null)

The `is_observed` flag distinguishes gauge data (NWIS, `is_observed=True`) from modelled or
reanalysis products (GEOGLOWS, ERA5-Land, `is_observed=False`) — critical for uncertainty-aware
interpretation by agents. For hydrology specifically, gauge-less/global streamflow signatures
carry `quality_flags` including `modelled_streamflow`; `AttributeResult.to_report()` emits a
mandatory **MODELLED STREAMFLOW CAVEAT** so BFI/FDC/flood-frequency/timing metrics are not
mistaken for observed catchment properties.

## Layering

```
aihydro-core  ←  aihydro-data  ←  aihydro-lsh
                  aihydro-watershed ↗
```

`aihydro-lsh` may depend DOWN on core/data/watershed, never UP on aihydro-tools.
Enforced by the AST guard in `tests/test_layering.py` and import-linter.

## MCP tools (C5)

When `aihydro-lsh[mcp]` is installed alongside `aihydro-tools`, ten tools are
auto-registered on the shared server:

| Tool | Description |
|---|---|
| `lsh_attributes` | All 101 static scalar attrs for a polygon or pour-point |
| `lsh_forcing` | Daily basin-averaged meteorological timeseries (LSTM/HBV) |
| `lsh_dynamic_attributes` | Annual dynamic panels (T3) — year × 22 variables |
| `lsh_events` | Drought (SPI) and flood (POT) event inventories (T3) |
| `lsh_families` | List families + attribute keys |
| `lsh_attribute_info` | Unit, source, family, formula, CAMELS equivalent, and caveat for one attribute |
| `lsh_batch` | Multi-basin extraction from JSON array |
| `lsh_compare_to_ref` | lsh vs camels-attrs side-by-side (CONUS) |
| `lsh_recipe_status` | Check which recipes can run (auth/deps) |
| `lsh_reliability_matrix` | Family/region availability status and interpretation caveats |

For comparative studies, keep product chains explicit. The helper
`aihydro_lsh.batch.summarize_product_homogeneity(results)` reports whether each
attribute family used one product across a result set or mixed products such as
CHIRPS and ERA5-Land. Mixed products are valid but should be reported as a
source of uncertainty rather than interpreted as purely hydrologic contrast.

## Reliability matrix

`aihydro_lsh.reliability.reliability_matrix()` returns a code-readable status
for each attribute family by region (`reliable`, `available_with_caveats`, or
`not_available`). It is intentionally static and import-light: it does not prove
live network/data availability, but it prevents binary “global” claims from
dropping caveats such as modelled streamflow, product-switching uncertainty, and
permission/tile availability for geology. Geology provenance includes
`global_tiles_status=...` from `pygeoglim.global_tiles_status()` where available.

## CONUS parity validation (C3)

```bash
# CI gate — offline, reads the pre-generated fixture, no network required
pytest tests/test_conus_parity.py -k Offline --tb=short

# Regenerate the fixture (network + GEE + pygeoglim HF token required;
# several minutes per gauge)
python scripts/gen_conus_parity_fixture.py

# Live spot-check against a fresh live run instead of the cached fixture
pytest tests/test_conus_parity.py -m live --tb=short
```

The parity suite is parameterized by `tests/fixtures/conus_parity_panel.json`
— inspect that file for the current basin panel and how each gauge was
selected (stratified by aridity x snow fraction x relief using
`pygeohydro.get_camels()`'s real 671-station CAMELS metadata). It checks:
- Topography: ±10 % of camels-attrs reference
- Climate: ±10 %
- Geomorphic: shape ratios within geodesic tolerance
- Geology: ±5 % (same pygeoglim backend)
- NaN guard: ≤5 NaN numeric attrs for a healthy extraction (geomorphic sinks/CN may NaN on global path)

**What this does and does not establish:** this validates that aihydro-lsh
reproduces camels-attrs on CONUS — it does not validate the global attribute
claim (no non-CONUS ground truth exists in this chain, and gauge-less
hydrology signatures are computed from modelled streamflow with no observed
reference to check against anywhere). See `papers/DECISIONS.md` PD-4 for the
claim-scoping decision this follows, and
`scripts/gen_parity_tolerance_table.py` for the per-attribute divergence
table generated from the fixture.

## Citations

Attribute-level citations are returned in `AttributeResult.citations`.
Key references:
- Saxton & Rawls (2006) — soil PTF
- Hartmann & Moosdorf (2012) — GLiM lithology
- Gleeson et al. (2014) — GLHYMPS permeability
- Gutman & Ignatov (1998) — NDVI→GVF conversion
- Jackson et al. (1996) — rooting-depth profiles
- Addor et al. (2017) — CAMELS attribute definitions
- Mann (1945); Kendall (1975) — Mann-Kendall trend test
- Sen (1968) — Sen's slope estimator
- McKee, Doesken & Kleist (1993) — Standardised Precipitation Index (SPI)
- Coles (2001) — peaks-over-threshold extreme value analysis
