Commit 34af6f0b authored by Turner, Sean's avatar Turner, Sean
Browse files

Add interactive diagnostic visualization module



Restructure to mixed Rust+Python maturin package (python-source = "python")
so Python modules can live alongside the Rust extension. Add diagnostics
module with three public functions:

- cascade_diagnostics(): Interactive Plotly figure with dropdown reservoir
  selector, linked flow/elevation/power panels, and infeasible power shading
- network_diagram(): Publication-quality static matplotlib network diagram
  with trapezoid reservoir nodes, Bezier edges, and lag labels
- save_diagnostics_html(): Standalone HTML export

Includes 28 tests and a Cumberland example notebook demonstrating both
the network diagram and interactive time series diagnostics.

Co-Authored-By: default avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 4e06d20f
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -6,7 +6,7 @@ license = "BSD-3-Clause"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "powersheds"
name = "_lib"
crate-type = ["cdylib"]

[dependencies]
+188 −0
Original line number Diff line number Diff line
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "# Powersheds Cumberland Cascade Diagnostics\n\nThis notebook loads the 8-reservoir Cumberland River Basin cascade,\nruns the Powersheds simulation, and produces:\n1. A **static network diagram** showing the cascade topology\n2. An **interactive time series diagnostic** (Plotly) with dropdown reservoir selection"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "from dataclasses import dataclass\nfrom pathlib import Path\n\nimport pandas as pd\nimport yaml\n\nimport powersheds\nfrom powersheds.diagnostics import cascade_diagnostics, network_diagram, save_diagnostics_html"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Load cascade configuration and time series"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@dataclass\n",
    "class ReservoirData:\n",
    "    object_type: str\n",
    "    capacity: float\n",
    "    initial_pool_elevation: float\n",
    "    min_power_pool: float\n",
    "    set_storage: list[float]\n",
    "    set_elevation: list[float]\n",
    "    hpf_h: list[float]\n",
    "    hpf_p: list[float]\n",
    "    hpf_q: list[float]\n",
    "    tailwater_elevation: float\n",
    "    max_release: float\n",
    "    min_release: float\n",
    "    catchment_inflow: list[float]\n",
    "    target_power: list[float]\n",
    "    simulation_order: int\n",
    "    downstream_object: str\n",
    "\n",
    "@dataclass\n",
    "class RiverData:\n",
    "    object_type: str\n",
    "    simulation_order: int\n",
    "    downstream_object: str\n",
    "    lag: int\n",
    "    legacy_flows: list[float]\n",
    "\n",
    "@dataclass\n",
    "class ConfluenceData:\n",
    "    object_type: str\n",
    "    simulation_order: int\n",
    "    downstream_object: str\n",
    "\n",
    "@dataclass\n",
    "class CascadeData:\n",
    "    reservoirs: dict[str, ReservoirData]\n",
    "    rivers: dict[str, RiverData]\n",
    "    confluences: dict[str, ConfluenceData]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "CUMBERLAND_DIR = Path(\".\").resolve()\n",
    "\n",
    "with open(CUMBERLAND_DIR / \"cascade_config.yaml\") as f:\n",
    "    config_dict = yaml.safe_load(f)\n",
    "\n",
    "reservoir_dict = {}\n",
    "river_dict = {}\n",
    "confluence_dict = {}\n",
    "\n",
    "for name, specs in config_dict.items():\n",
    "    if specs[\"object_type\"] == \"reservoir\":\n",
    "        ts = pd.read_csv(CUMBERLAND_DIR / \"time_series\" / f\"{name}.csv\")\n",
    "        se = pd.read_csv(CUMBERLAND_DIR / \"storage_HWelevation_tables\" / f\"{name}.csv\")\n",
    "        hpf = pd.read_parquet(CUMBERLAND_DIR / \"head_power_flow_tables\" / name)\n",
    "        reservoir_dict[name] = ReservoirData(\n",
    "            **specs,\n",
    "            catchment_inflow=ts[\"catchment_inflow\"].tolist(),\n",
    "            target_power=ts[\"target_power\"].tolist(),\n",
    "            set_storage=se[\"storage_Mm3\"].tolist(),\n",
    "            set_elevation=se[\"elevation_m\"].tolist(),\n",
    "            hpf_h=hpf[\"H_m\"].astype(float).tolist(),\n",
    "            hpf_p=hpf[\"P_MW\"].astype(float).tolist(),\n",
    "            hpf_q=hpf[\"Q_cumecs\"].astype(float).tolist(),\n",
    "        )\n",
    "    elif specs[\"object_type\"] == \"river\":\n",
    "        legacy = pd.read_csv(CUMBERLAND_DIR / \"time_series\" / f\"{name}.csv\")\n",
    "        river_dict[name] = RiverData(\n",
    "            **specs,\n",
    "            legacy_flows=legacy[\"legacy_flow\"].tolist(),\n",
    "        )\n",
    "    elif specs[\"object_type\"] == \"confluence\":\n",
    "        confluence_dict[name] = ConfluenceData(**specs)\n",
    "\n",
    "cascade_data = CascadeData(\n",
    "    reservoirs=reservoir_dict,\n",
    "    rivers=river_dict,\n",
    "    confluences=confluence_dict,\n",
    ")\n",
    "\n",
    "print(f\"Loaded {len(reservoir_dict)} reservoirs, {len(river_dict)} rivers, {len(confluence_dict)} confluences\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Run the simulation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "results = powersheds.simulate_cascade(cascade_data)\n",
    "\n",
    "print(f\"Simulation complete: {len(results)} objects\")\n",
    "for name, data in results.items():\n",
    "    if isinstance(data, dict) and \"actual_power\" in data:\n",
    "        n = len(data[\"actual_power\"])\n",
    "        print(f\"  {name}: {n} hours\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 3. Network diagram\n\nStatic visualization of the cascade topology. Trapezoid nodes represent\nreservoirs (amber bar = hydropower), circles represent confluences, and\nedges show river connections with lag times in hours."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "network_diagram(cascade_data, title=\"Cumberland River Basin Cascade\");"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 4. Interactive time series diagnostics\n\nThree linked panels (Flows, Elevation, Power) with shared x-axis zoom/pan.\nUse the **dropdown** to switch between reservoirs.\n\nRed shading in the Power panel highlights **infeasible generation** where\nactual power falls below the target."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "fig = cascade_diagnostics(\n    results,\n    title=\"Cumberland River Basin \\u2014 Cascade Diagnostics\",\n    height=900,\n    width=1100,\n)\nfig.show()"
  },
  {
   "cell_type": "markdown",
   "source": "## 5. Export to standalone HTML\n\nSave the interactive time series as a self-contained HTML file.",
   "metadata": {}
  },
  {
   "cell_type": "code",
   "source": "output_path = save_diagnostics_html(\n    results,\n    output_path=\"cumberland_diagnostics.html\",\n    title=\"Cumberland River Basin \\u2014 Cascade Diagnostics\",\n)\nprint(f\"Saved: {output_path.resolve()}\")",
   "metadata": {},
   "execution_count": null,
   "outputs": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
 No newline at end of file
+5 −0
Original line number Diff line number Diff line
@@ -26,6 +26,11 @@ dependencies = [

[tool.maturin]
features = ["pyo3/extension-module"]
module-name = "powersheds._lib"
python-source = "python"

[project.optional-dependencies]
viz = ["plotly>=5.18"]

[dependency-groups]
dev = [
+1 −0
Original line number Diff line number Diff line
from powersheds._lib import *
+805 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading