An AI-powered water-quality intelligence platform, in two layers:
-
Hydrosense National (
hydrosense/) — nationwide, research-grade forecasting across USGS/WQP stations in any US state: normalized database, pooled generalized models (no station memorization), leave-one-station-out / cross-state validation, conformal uncertainty, continuous accuracy tracking, paper-grade figures. Dashboard:results/national.html. Docs: docs/NATIONAL_ARCHITECTURE.md, docs/NATIONAL_METHODOLOGY.md, docs/NATIONAL_ROADMAP.md, docs/IMPROVEMENT_AUDIT.md, docs/AUDIT_IEEE_REVIEW.md (independent IEEE-style review with trustworthiness scorecard). -
Alameda Creek regional demo — the original physics-informed nitrate-risk system for one watershed, with a PINN solving the 1-D advection-diffusion PDE, LiDAR channel geometry, and live operator views. Demo site:
results/pinn-demo.html(15 sections, all interactive).
Repo: https://github.com/a6nkay-source/Hydrosense
# national pipeline (any states/parameters)
/usr/bin/python3 scripts/national_pipeline.py \
--states CA TX FL NY WA \
--parameters temperature conductivity turbidity nitrateOne-command reproduction via the Makefile — see REPRODUCIBILITY_GUIDE.md:
make test # 58 offline tests (no network)
make national # full ingest + train + evaluate
make hardening # adaptive calibration + validation + leave-one-state-out
make registry # regenerate EXPERIMENT_REGISTRY.md from the DB
make all # everythingPublication infrastructure: EXPERIMENT_REGISTRY.md (auto-generated, provenance-complete) · PAPER_RESULTS.md (every claim → experiment + figure + statistical evidence, unsupported claims flagged) · PRODUCTION_HARDENING_REPORT.md.
Manual equivalents (or docker build -t hydrosense . && docker run --rm hydrosense for the test suite):
pip install -r requirements.txt # or use the Dockerfile
python -m pytest tests/ -q # 58 offline tests, no network
# 1. ingest + train + evaluate (writes experiments/, leaderboard, dashboards)
python scripts/national_pipeline.py --states CA TX FL NY WA --max-stations 6
# 2. improvement benchmark (delta-mode, tuned PINN, ensembles, storm workflow)
python scripts/improve_and_benchmark.py
# 3. physics evidence (with/without-physics ablation, residuals, gradients)
python scripts/physics_diagnostics.py
# 4. statistics + calibration fixes (BH, bootstrap CIs, adaptive conformal)
python scripts/rigor_fixes.py
# 5. audits + reports + papers, then publish (tests gate the push)
python scripts/audit_report.py && python scripts/hros_cycle.py
python scripts/post_experiment.py --message "reproduction run" --no-pushSeeds are fixed (42); every run records its git commit and full config in
the experiments table and results/experiments/. Key artifacts:
results/leaderboard.csv ·
docs/PHYSICS.md ·
docs/AUDIT_IEEE_REVIEW.md ·
docs/IJCNN_REVIEW_2026-07.md ·
FINAL_IJCNN_READINESS_REPORT.md ·
results/physics_diagnostics/ ·
docs/paper/.
Hydrosense evaluates every feature against the category it serves — see docs/FEATURE_EVIDENCE_TABLE.md:
- Skill features (models, engineered inputs) are judged on leakage-safe RMSE and significance vs persistence. Satellite, spatial, hydrologic, regime, and ENSO features were all proposed, tested, and rejected on evidence.
- Trustworthiness features (uncertainty, OOD detection, confidence, explainability, monitoring) are judged on reliability and transparency and are kept even when they do not lower RMSE. Adaptive conformal calibration, an out-of-distribution detector, analog-event retrieval, the model-health board, and TreeSHAP explanations are all kept on this basis.
Distribution-shift robustness (docs/DISTRIBUTION_SHIFT.md): an OOD detector recognizes unfamiliar conditions (novelty tracks turbidity error at r=0.27) and widens uncertainty during storms rather than being confidently wrong. The wet-season weakness itself is unfixed — the model now flags it instead of hiding it.
Formal IEEE-style paper with all current data: PDF
results/paper/hydrosense_ieee.pdf
(8-page two-column, figures + tables + references) and compilable IEEEtran
source results/paper/hydrosense_ieee.tex
(pdflatex hydrosense_ieee.tex for camera-ready). Regenerate with
python3 scripts/build_ieee_paper.py. Headline honest findings inside:
temperature skill is significant but does not survive a wet-season holdout
(persistence wins out-of-distribution); conductance/turbidity deploy
persistence; physics helps turbidity only.
A polished, figure-rich overview renders at
results/paper/index.html (open locally, or via the
server at /paper/index.html) — hero, geographic coverage, method with code,
results tables, physics validation, and honest limitations, all built from
verified figures.
Model & data pipeline: leakage-free feature encoder → pooled model zoo (no station IDs) → physics regularizer → delta-mode prediction → hierarchical conformal uncertainty → exact-TreeSHAP explanation + risk index → continuous verification. Governing equations and the physics-regularization derivation are in docs/PHYSICS.md.
┌─ 4 USGS gauges ──┐
├─ Open-Meteo rainfall ─┤
├─ NASA GIBS satellite ─┼─→ feature pipeline → 3 models → daily log → demo site
├─ USGS 3DEP LiDAR ─┤ ↑
└─ NLCD / PRISM (static) ─┘ launchd cron, no human in the loop
This section is the single most important page of the README. Every claim the demo makes should be traceable to one of these three buckets.
| Signal | Source | Cadence | Where in demo |
|---|---|---|---|
| Discharge (4 gauges) | USGS NWIS instantaneous | 15 min | Sections 0, 0.5 (live) |
| Rainfall (Sunol watershed) | Open-Meteo Archive | daily | Sections 5, 12 |
| Rainfall forecast (Sunol, 7 d) | Open-Meteo Forecast | hourly refresh | Section 5 |
| Turbidity (Coyote regional proxy) | USGS NWIS instantaneous | 15 min | Section 10 |
| Satellite RGB + NDVI | NASA GIBS MODIS Terra | daily / 8-day | Sections 0, 9 |
| Map base layer | ESRI World Imagery (Sentinel-2 / Maxar) | seasonal | Section 0 |
| Output | Inputs | Method |
|---|---|---|
| Peak nitrate prediction | rainfall + discharge | closed-form physics kernel peak = 2.0 + 0.45·rain + 0.02·max(0, Q-5) |
| Spatial concentration C(x,t) | velocity, diffusion | PINN solving ∂C/∂t + u·∂C/∂x - D·∂²C/∂x² = S (research artifact, not in live calendar) |
| Channel velocity | LiDAR bed slope | Manning equation u = R^(2/3)·√S/n (Section 7) |
| Confidence intervals | residuals stratified by rain band | split conformal, per-day (Section 5) |
| Validation regression | predicted peak vs measured turbidity | Pearson / Spearman r (Section 10) |
| Operator action ETAs | velocity × distance | LiDAR-derived 7.9 h headwater → intake (Section 8) |
| Output | Why it's not solid | Where labeled |
|---|---|---|
| Kernel coefficients (0.45 mg/L/mm, 0.02 mg/L/cfs, 1.6 d decay) | Literature estimates, not regressed against measured nitrate | Sections 5, 8 footnotes |
| Spatial PDE → station concentration | Currently exp(-x/80) placeholder, not the trained PINN |
Section 8 footnote |
| Operator action recommendations | Role-keyed templates, not learned policies | Section 8 footnote |
| NDWI proxy | Derived from rendered NDVI tile, not Sentinel-2 band math | Section 7 footnote |
| Cloud detection | Brightness threshold, not the official MODIS cloud mask | Section 7 footnote |
Watershed land cover (data/processed/watershed_features.json) |
Static summary, doesn't refresh | JSON honest_note |
| Operator role labels (release point / calibration gauge / TREATMENT INTAKE) | Descriptive only; no operational partnership exists | Section 8 |
- Satellite imagery does NOT directly measure nitrate. It's a proxy for turbidity / watershed wetness / cloud cover.
- The "98.2 % safety status correct" is over a 60-day dry-season window where the trivial baseline guess (always safe) wins by default.
- The PINN's R² ≈ 0 on the synthetic Gaussian benchmark — a known limitation of sparse-data PINN training, not a deception.
hydrosense/
├── README.md this file
├── PINN_ROADMAP.md historical: how the PINN was built (long)
├── docs/PRESS_RELEASE_ALPHA.md press release prose
│
├── alameda_creek_pinn/ PYTHON SIDE — training, ingestion, modeling
│ ├── src/
│ │ ├── data/satellite.py NASA GIBS daily-feature pipeline
│ │ ├── data/sentinel2.py Sentinel-2 spectral indices (NDWI, NDTI, etc.)
│ │ ├── data/lidar.py USGS 3DEP point-cloud → channel profile
│ │ ├── features.py 33-feature unified daily frame
│ │ ├── physics/advection_diffusion.py PDE residual
│ │ └── train/trainer.py PINN trainer
│ ├── scripts/
│ │ ├── prediction_tracker.py DAILY 06:00 cron — fetch + predict + log
│ │ ├── train_ml.py WEEKLY 03:00 — RF + GradientBoost + kernel
│ │ ├── conformal_intervals.py WEEKLY — per-rain-band ±half-widths
│ │ ├── turbidity_validation.py WEEKLY — kernel vs measured turbidity r
│ │ ├── evaluate_daily.py DAILY — rolling-window metrics + CSV
│ │ ├── run_research_pipeline.py research — full 9-step evaluation pipeline
│ │ ├── backfill_predictions.py once — seed 60 days of history
│ │ ├── backfill_accuracy_log.py once — seed the trend chart
│ │ ├── export_lidar_artifacts.py once — write profile + metrics for demo
│ │ └── train.py research — PINN training (LiDAR / no-LiDAR)
│ ├── experiments/
│ │ ├── base.yaml PINN config (lidar.enabled: true)
│ │ └── baseline_no_lidar.yaml ablation config
│ ├── data/
│ │ ├── raw/ USGS + Open-Meteo + LiDAR sample CSVs
│ │ ├── processed/ watershed_features.json
│ │ ├── satellite/ cached daily GIBS features
│ │ └── runs/<timestamp>/ versioned ML run dirs
│ └── models/
│ ├── pinn_lidar_v1/best.pt trained PINN (LiDAR velocity)
│ └── pinn_no_lidar_v1/best.pt trained PINN (default velocity)
│
└── results/ DEMO SITE
├── index.html cinematic homepage
├── pinn-demo.html 12-section live demo
├── accuracy.html legacy dashboard
├── press-release.html styled press release
├── data/ served as JSON / CSV
│ ├── predictions.json 399+ entries, rolling
│ ├── accuracy_log.{json,csv} 46+ daily metric snapshots
│ ├── lidar_profile.json channel bed + slope per site
│ ├── lidar_metrics.json LiDAR vs no-LiDAR PINN comparison
│ ├── ml_metrics.json latest train_ml.py run
│ ├── conformal_intervals.json per-rain-band ±half-widths
│ ├── turbidity_validation.json external sensor correlation
│ └── research/ research pipeline outputs
│ ├── dashboard_data.json combined dashboard feed
│ ├── regime_analysis.json regime + storm event metrics
│ ├── event_evaluation.json event-based scoring
│ ├── adaptive_conformal.json rolling conformal calibration
│ ├── changepoints.json detected regime shifts
│ ├── ablation.json feature group ablation
│ └── validation.json cross-validation results
└── scripts/
├── prediction_tracker.py (older copy, kept for the live demo)
├── backfill_predictions.py (older copy)
└── rivertree_proxy.py NVIDIA Integrate proxy for the AI agent
USGS + Open-Meteo → feature assembly (rain/lag/doy) ──┬──→ ONNX MLP → Section 5 (primary)
└──→ kernel fallback → Section 5 (if ONNX fails)
peak = 2.0 + 0.45·rain + 0.02·max(0, Q-5)
↳ ±half-width from conformal_intervals.json
The trained neural network — a small PyTorch MLP (7 features → 32 → 32 → 16 → 1, ~1.3 K params, 8.6 KB ONNX) — is the primary inference path. It loads via onnxruntime-web 1.20.1 in the browser and runs entirely in WASM. If the runtime is missing, the ONNX file 404s, or inference throws, Section 5 silently falls back to the closed-form kernel — visible via the badge above the forecast cards (mint = ONNX, amber = fallback).
The PyTorch ↔ ONNX parity is asserted on every export:
max |Δ| ≈ 1e-6 (float32 round-trip tolerance) — see
alameda_creek_pinn/scripts/test_onnx_parity.py.
Important distinction. The PINN (Section 7) is a separate spatial-PDE
artifact that takes (x, t) and outputs C(x, t) — it has no rainfall input
and cannot drive a rainfall→peak forecast. It exports to pinn.onnx (68 KB)
as a parity demo, not as live inference. The forecast MLP is what Section 5
actually calls.
06:00 launchd cron → prediction_tracker.py → predictions.json gains 7 entries
06:00 launchd cron → evaluate_daily.py → accuracy_log gains 1 row
03:00 Sunday cron → train_ml.py → data/runs/<ts>/ + ml_metrics.json
03:00 Sunday cron → conformal_intervals.py → refreshed ±half-widths
03:00 Sunday cron → turbidity_validation.py → Section 10 r refreshes
PINN training: python -m scripts.train experiments/base.yaml (LiDAR-derived u)
python -m scripts.train experiments/baseline_no_lidar.yaml
python -m scripts.export_lidar_artifacts (writes to results/data/)
Research pipeline: python alameda_creek_pinn/scripts/run_research_pipeline.py
→ regime classification + storm event detection
→ event-based evaluation (rainfall, discharge, nitrate events)
→ adaptive conformal prediction (rolling 60-day window)
→ change-point detection (CUSUM + sliding-window tests)
→ feature ablation (6 groups, permutation importance)
→ advanced validation (rolling-origin CV, LOSO, temporal holdout)
→ automated reports (results/reports/)
→ dashboard data (results/data/research/dashboard_data.json)
| # | Section | Anchor | Live signal |
|---|---|---|---|
| 0 | Watershed map + 4 gauges | #map |
discharge values update every 60 s |
| 0.5 | 5 live data streams | #data |
turbidity + 3 discharges + rainfall |
| 1 | Channel simulation | #sim |
client-side advection-diffusion |
| 2 | Model comparison (analytical vs PINN vs MLP) | #compare |
research output |
| 3 | Impact dashboard | #impact |
static numbers |
| 4 | Project explainer | #explainer |
static |
| 5 | 7-day forecast calendar | #forecast |
per-day rain × Q → peak ± conformal band |
| 6 | "What if?" scenario explorer | #scenario |
slider-driven storm hypothetical |
| 7 | LiDAR channel profile | #lidar |
bed elevation + slope + Manning u |
| 8 | Spread + operator actions | #spread |
4-station table with real ETAs |
| 9 | ML model comparison | #ml |
RF / GB / kernel test metrics |
| 10 | External turbidity validation | #validation |
Pearson r vs measured sensor |
| 11 | Accuracy trend over time | #trend |
14-day rolling MAE × 3 models × 46 days |
| 12 | Unified time-series | #timeseries |
rain + Q + peak on shared timeline |
| 13 | Model Lab | #modellab |
4-solver live compare + XAI via FastAPI |
| 14 | Research Dashboard | #research |
regime metrics, events, conformal, ablation, change-points |
- USGS NWIS REST API — discharge (
00060), turbidity (63680), temp (00010), specific conductance (00095) - USGS 3DEP — 10 m DEM, used for channel slope via LiDAR-Manning velocity
- USGS Alameda Creek near Niles —
11179000, the primary calibration gauge - USGS Alameda Creek at Sunol —
11173200, headwater + the only Alameda gauge that registers turbidity (currently offline) - USGS Coyote Creek at Hwy 237 —
11172175, regional turbidity fallback (different watershed) - USGS Water Quality Portal — discrete nitrate samples (sparse, ~monthly)
- Open-Meteo Forecast API — 7-day rainfall outlook, hourly refresh
- Open-Meteo Archive API — historical daily rainfall for verification
- NASA GIBS — daily MODIS Terra true-color + 8-day NDVI composite, no key
- ESRI World Imagery — Sentinel-2 / Maxar mosaic, no key
- Watershed climatology: PRISM 30-year normals (1991–2020), summarized once
- MRLC NLCD 2021 30 m land cover, summarized once into
data/processed/watershed_features.json
| Model | Type | Trainable params | Used for |
|---|---|---|---|
| Physics kernel | Closed form | 0 | Live calendar prediction (Sections 5, 8, 12) |
| Random Forest | sklearn 1.6 | 300 trees, depth 10 | Compared in Section 9, contributes feature importances |
| Gradient Boost | sklearn 1.6 (XGBoost stand-in) | 400 trees, depth 3 | Compared in Section 9 |
| Persistence | y_today = y_yesterday | 0 | Baseline floor |
| Rain-only | kernel minus runoff term | 0 | Ablation in Section 9 |
| PINN (LiDAR-u) | PyTorch, Fourier features | 16,701 | Research artifact (Sections 2, 7), NOT in live inference |
| PINN (no-LiDAR) | same | 16,701 | Ablation for Section 7 metrics |
Splits are chronological only (train_ml.py uses 70/15/15 by date). No
shuffling — that would leak future info into training.
Both launchd jobs are installed and active:
launchctl list | grep hydrosense
- 2 com.hydrosense.tracker # daily 06:00 PT — fetch + predict + log
- 0 com.hydrosense.retrain # Sundays 03:00 — retrain + recalibrate
Logs at results/data/tracker.log and results/data/retrain.log.
plist files at ~/Library/LaunchAgents/com.hydrosense.{tracker,retrain}.plist.
-
Self-consistent target. ML models in Section 9 score MAE 0 against the kernel by definition — the target column is the kernel's own output. The interesting comparison is against measured nitrate, which we don't have continuously.
-
No winter testing. Calibration data is April–June 2026 (dry season). Only 6 of 60 days had measurable rain (≥0.5 mm). The "heavy" conformal band has n=0 calibration days — we cannot bound uncertainty during atmospheric rivers yet.
-
PINN not in the live inference path (intentional). The calendar uses an ONNX-deployed trained MLP — that's the trained network running live. The PINN itself is a spatial-PDE artifact that takes (x, t) → C and has no rainfall input, so it can't drive a forecast. It is exported to ONNX (
pinn.onnx, 68 KB) for parity/demo, not for production inference. R² ≈ 0 on the synthetic Gaussian benchmark limits its standalone ceiling regardless. -
Turbidity validation Pearson r = +0.056, p = 0.67. Statistically insignificant. The reason is in #2 — there's nothing to correlate against in a dry-season window. Section 10 says this explicitly.
-
Niles turbidity sensor doesn't exist. Section 0.5 displays a fallback constant (4.2 FNU) when the real-time fetch fails — which it always does, because gauge 11179000 doesn't measure turbidity. Sunol (11173200) registers a turbidity sensor but it's currently offline.
-
Sample LiDAR data. The 2 LiDAR CSVs (~20 points/site) are sample-size for the format spec, not training data. The slope estimate is statistically meaningless; real Manning velocity needs the full USGS 3DEP tile.
-
Operator action recommendations are templates, not learned policies. The "+7.9 h lead time" and "18 antecedent-risk days" are real (computed), but "switch to reservoir intake" is descriptive.
-
Land cover percentages don't refresh. NLCD updates every 3-5 years; we summarized once and stored statically.
PINN → live inference✅ Done. The trained MLP runs in-browser via ONNX Runtime Web. PINN itself exported as parity demo.- Re-add
discharge_cfsas a model feature once 30+ days of in-distribution training data accumulate (it was dropped because backfilled training rows had it NaN-filled). - Sentinel-2 band math via Copernicus Data Space Ecosystem (key required, free). Real NDWI / NDTI for in-channel pixels at 10 m.
- Continuous nitrate sensor (~$5k hardware). Eliminates the turbidity-proxy detour.
- Wet-season testing (Nov–Mar). Calibrates the "heavy" conformal band against real first-flush events.
- GitHub Pages deploy so the demo is publicly viewable without local serve.
dataretrieval-pythonswap-in. Replaces hand-rolled USGS URL fetches with the maintained Python package.
# 1. Install deps
pip install -r requirements.txt # torch numpy pandas scikit-learn scipy pillow pyyaml
# 2. Bootstrap the prediction log (once)
cd alameda_creek_pinn
python -m scripts.backfill_predictions --days 60
# 3. Train ML models (chronological splits)
python -m scripts.train_ml
python -m scripts.conformal_intervals
python -m scripts.turbidity_validation
# 4. Seed the accuracy-trend chart (once)
python -m scripts.backfill_accuracy_log --window 14 --step 1
# 5. (optional) Install the daily/weekly launchd jobs — see ~/Library/LaunchAgents/
# 6. Serve the demo locally
cd results && python3 -m http.server 8000
# → http://localhost:8000/pinn-demo.htmlUSGS for the water data and 3DEP LiDAR. Open-Meteo for free weather forecasts. NASA for GIBS satellite imagery. ESRI for the World Imagery basemap. MRLC for NLCD land cover. PRISM Climate Group at Oregon State. Developed by: • Anshuman Kumar • Arjun Khurana Development assistance provided by Claude for model implementation, debugging, and training workflows.
