Spaces:
Running on Zero
Running on Zero
Delete examples
Browse files- examples/example_pipeline.py +0 -214
- examples/explore_datasets.py +0 -196
- examples/test_fusion.py +0 -50
- examples/test_graph.py +0 -148
- examples/test_loaders.py +0 -81
- examples/visualize_full_graph.py +0 -194
- examples/visualize_graph.py +0 -210
examples/example_pipeline.py
DELETED
|
@@ -1,214 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Example pipeline demonstrating Phase 1: Data Fusion and Graph Construction.
|
| 3 |
-
|
| 4 |
-
This script shows how to use the implemented modules to:
|
| 5 |
-
1. Load hydrometric and SAFRAN data
|
| 6 |
-
2. Clean and validate data
|
| 7 |
-
3. Fuse data sources
|
| 8 |
-
4. Extract basin features
|
| 9 |
-
5. Build physics-informed graph
|
| 10 |
-
6. Prepare tensors for GNN training
|
| 11 |
-
"""
|
| 12 |
-
from pathlib import Path
|
| 13 |
-
from src.config.settings import (
|
| 14 |
-
WATERSHED_RISLE, WATERSHED_EURE, IDPR_FILE,
|
| 15 |
-
BASIN_IDS, RANDOM_SEED
|
| 16 |
-
)
|
| 17 |
-
from src.data.loaders.hydrometric import HydrometricLoader
|
| 18 |
-
from src.data.loaders.safran import SAFRANLoader
|
| 19 |
-
from src.data.loaders.shapefile import ShapefileLoader
|
| 20 |
-
from src.data.loaders.idpr import IDPRLoader
|
| 21 |
-
from src.data.extractors.topological import TopologicalExtractor
|
| 22 |
-
from src.data.extractors.karst import KarstExtractor
|
| 23 |
-
from src.fusion.fusion_engine import DataFusionEngine
|
| 24 |
-
from src.graph.builder import HydrologicalGraphBuilder
|
| 25 |
-
from src.preprocessing.tensor_builder import SpatioTemporalTensorBuilder
|
| 26 |
-
from src.utils.helpers import set_seed, get_device, ensure_dir, print_data_summary
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def main():
|
| 30 |
-
"""Run the complete Phase 1 pipeline."""
|
| 31 |
-
|
| 32 |
-
print("="*80)
|
| 33 |
-
print("PHASE 1: Data Fusion and Graph Construction for Hydrological GNN")
|
| 34 |
-
print("="*80)
|
| 35 |
-
|
| 36 |
-
# Set random seed for reproducibility
|
| 37 |
-
set_seed(RANDOM_SEED)
|
| 38 |
-
|
| 39 |
-
# Get computation device
|
| 40 |
-
device = get_device(prefer_gpu=True)
|
| 41 |
-
|
| 42 |
-
# Create output directories
|
| 43 |
-
output_dir = ensure_dir(Path("outputs"))
|
| 44 |
-
data_dir = ensure_dir(output_dir / "processed_data")
|
| 45 |
-
graph_dir = ensure_dir(output_dir / "graphs")
|
| 46 |
-
tensor_dir = ensure_dir(output_dir / "tensors")
|
| 47 |
-
|
| 48 |
-
# ========================================================================
|
| 49 |
-
# STEP 1: Load Data
|
| 50 |
-
# ========================================================================
|
| 51 |
-
print("\n[STEP 1] Loading data sources...")
|
| 52 |
-
|
| 53 |
-
# Note: Update these paths to your actual data locations
|
| 54 |
-
# This is a template - you'll need to adjust based on your data structure
|
| 55 |
-
|
| 56 |
-
# Example: Load hydrometric data
|
| 57 |
-
# hydro_loader = HydrometricLoader(
|
| 58 |
-
# data_path=Path("datasets/hydrometric_data"),
|
| 59 |
-
# file_format="csv"
|
| 60 |
-
# )
|
| 61 |
-
# hydro_df = hydro_loader.load()
|
| 62 |
-
# print_data_summary(hydro_df, "Hydrometric Data")
|
| 63 |
-
|
| 64 |
-
# Example: Load SAFRAN meteorological data
|
| 65 |
-
# safran_loader = SAFRANLoader(
|
| 66 |
-
# data_path=Path("datasets/safran_data"),
|
| 67 |
-
# file_format="csv"
|
| 68 |
-
# )
|
| 69 |
-
# safran_df = safran_loader.load()
|
| 70 |
-
# print_data_summary(safran_df, "SAFRAN Data")
|
| 71 |
-
|
| 72 |
-
print("Data loading configured. Update paths to your actual data.")
|
| 73 |
-
|
| 74 |
-
# ========================================================================
|
| 75 |
-
# STEP 2: Extract Basin Features
|
| 76 |
-
# ========================================================================
|
| 77 |
-
print("\n[STEP 2] Extracting basin features from shapefiles...")
|
| 78 |
-
|
| 79 |
-
# Load watershed shapefiles
|
| 80 |
-
# risle_loader = ShapefileLoader(WATERSHED_RISLE)
|
| 81 |
-
# eure_loader = ShapefileLoader(WATERSHED_EURE)
|
| 82 |
-
# risle_gdf = risle_loader.load()
|
| 83 |
-
# eure_gdf = eure_loader.load()
|
| 84 |
-
|
| 85 |
-
# Extract topological features (if DEM available)
|
| 86 |
-
# topo_extractor = TopologicalExtractor(dem_path=Path("path/to/dem.tif"))
|
| 87 |
-
# risle_features = topo_extractor.extract(risle_gdf)
|
| 88 |
-
# eure_features = topo_extractor.extract(eure_gdf)
|
| 89 |
-
|
| 90 |
-
# Extract karst features
|
| 91 |
-
# idpr_loader = IDPRLoader(IDPR_FILE)
|
| 92 |
-
# idpr_df = idpr_loader.load()
|
| 93 |
-
# karst_extractor = KarstExtractor(idpr_data=idpr_df)
|
| 94 |
-
# karst_features = karst_extractor.extract(risle_gdf)
|
| 95 |
-
|
| 96 |
-
print("Basin feature extraction configured.")
|
| 97 |
-
|
| 98 |
-
# ========================================================================
|
| 99 |
-
# STEP 3: Data Fusion
|
| 100 |
-
# ========================================================================
|
| 101 |
-
print("\n[STEP 3] Fusing data sources...")
|
| 102 |
-
|
| 103 |
-
# Initialize fusion engine
|
| 104 |
-
fusion_engine = DataFusionEngine(
|
| 105 |
-
normalize=True,
|
| 106 |
-
normalization_method="robust", # Recommended for hydrological data
|
| 107 |
-
clean_data=True
|
| 108 |
-
)
|
| 109 |
-
|
| 110 |
-
# Fuse datasets
|
| 111 |
-
# fused_df = fusion_engine.fuse(
|
| 112 |
-
# hydrometric_df=hydro_df,
|
| 113 |
-
# safran_df=safran_df,
|
| 114 |
-
# basin_features=None # Add extracted basin features here
|
| 115 |
-
# )
|
| 116 |
-
|
| 117 |
-
# Save fused data
|
| 118 |
-
# fusion_engine.save_fused_data(data_dir / "fused_data.csv")
|
| 119 |
-
# print(fusion_engine.get_summary())
|
| 120 |
-
|
| 121 |
-
print("Data fusion configured.")
|
| 122 |
-
|
| 123 |
-
# ========================================================================
|
| 124 |
-
# STEP 4: Build Physics-Informed Graph
|
| 125 |
-
# ========================================================================
|
| 126 |
-
print("\n[STEP 4] Building physics-informed graph...")
|
| 127 |
-
|
| 128 |
-
# Initialize graph builder
|
| 129 |
-
graph_builder = HydrologicalGraphBuilder(
|
| 130 |
-
allow_disconnected=True, # Phase 1: Allow disconnected components
|
| 131 |
-
allow_cross_basin=False # Phase 1: No cross-basin edges
|
| 132 |
-
)
|
| 133 |
-
|
| 134 |
-
# Prepare station and edge data
|
| 135 |
-
# You'll need to create these from your data:
|
| 136 |
-
# - station_df: [station_id, basin_id, latitude, longitude, elevation, ...]
|
| 137 |
-
# - edge_df: [source, target, distance, elevation_gradient, ...]
|
| 138 |
-
|
| 139 |
-
# Example structure:
|
| 140 |
-
# import pandas as pd
|
| 141 |
-
# station_df = pd.DataFrame({
|
| 142 |
-
# 'station_id': ['S1', 'S2', 'S3'],
|
| 143 |
-
# 'basin_id': [0, 0, 1], # 0=La Risle, 1=La Eure
|
| 144 |
-
# 'latitude': [48.5, 48.6, 49.0],
|
| 145 |
-
# 'longitude': [0.5, 0.6, 0.7],
|
| 146 |
-
# 'elevation': [100, 90, 120]
|
| 147 |
-
# })
|
| 148 |
-
#
|
| 149 |
-
# edge_df = pd.DataFrame({
|
| 150 |
-
# 'source': ['S1', 'S2'],
|
| 151 |
-
# 'target': ['S2', 'S3'],
|
| 152 |
-
# 'distance': [10.5, 15.2], # km
|
| 153 |
-
# 'elevation_gradient': [10, -30], # m
|
| 154 |
-
# 'basin_id': [0, 1]
|
| 155 |
-
# })
|
| 156 |
-
|
| 157 |
-
# Build graph
|
| 158 |
-
# graph_data = graph_builder.build(
|
| 159 |
-
# station_df=station_df,
|
| 160 |
-
# edge_df=edge_df,
|
| 161 |
-
# basin_features=None
|
| 162 |
-
# )
|
| 163 |
-
|
| 164 |
-
# Save graph
|
| 165 |
-
# graph_builder.save_graph(graph_data, graph_dir / "hydrological_graph.pt")
|
| 166 |
-
# print(graph_builder.get_graph_statistics())
|
| 167 |
-
|
| 168 |
-
print("Graph construction configured.")
|
| 169 |
-
print("Note: Use QGIS DEM hydrology tools to extract station_edges.csv")
|
| 170 |
-
|
| 171 |
-
# ========================================================================
|
| 172 |
-
# STEP 5: Prepare Tensors for GNN Training
|
| 173 |
-
# ========================================================================
|
| 174 |
-
print("\n[STEP 5] Preparing spatio-temporal tensors...")
|
| 175 |
-
|
| 176 |
-
# Initialize tensor builder
|
| 177 |
-
tensor_builder = SpatioTemporalTensorBuilder(
|
| 178 |
-
input_window=30, # 30 days of history
|
| 179 |
-
forecast_horizons=[1, 3, 7, 14], # Forecast 1, 3, 7, 14 days ahead
|
| 180 |
-
)
|
| 181 |
-
|
| 182 |
-
# Build tensors
|
| 183 |
-
# X, Y = tensor_builder.build_tensors(
|
| 184 |
-
# fused_df=fused_df,
|
| 185 |
-
# station_ids=None # Use all stations
|
| 186 |
-
# )
|
| 187 |
-
|
| 188 |
-
# Print tensor shapes
|
| 189 |
-
# print(tensor_builder.get_tensor_shapes(X, Y))
|
| 190 |
-
|
| 191 |
-
# Save tensors
|
| 192 |
-
# tensor_builder.save_tensors(X, Y, tensor_dir / "training_tensors.pt")
|
| 193 |
-
|
| 194 |
-
print("Tensor preparation configured.")
|
| 195 |
-
|
| 196 |
-
# ========================================================================
|
| 197 |
-
# Summary
|
| 198 |
-
# ========================================================================
|
| 199 |
-
print("\n" + "="*80)
|
| 200 |
-
print("Pipeline template complete!")
|
| 201 |
-
print("="*80)
|
| 202 |
-
print("\nNext steps:")
|
| 203 |
-
print("1. Update data paths to your actual hydrometric/SAFRAN data")
|
| 204 |
-
print("2. Use QGIS to extract river network topology (station_edges.csv)")
|
| 205 |
-
print("3. Run this pipeline to generate fused data, graphs, and tensors")
|
| 206 |
-
print("4. Proceed to GNN model development (Phase 1 continuation)")
|
| 207 |
-
print("\nOutputs will be saved to:")
|
| 208 |
-
print(f" - Fused data: {data_dir}")
|
| 209 |
-
print(f" - Graphs: {graph_dir}")
|
| 210 |
-
print(f" - Tensors: {tensor_dir}")
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
if __name__ == "__main__":
|
| 214 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/explore_datasets.py
DELETED
|
@@ -1,196 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Dataset exploration script.
|
| 3 |
-
Efficiently explores available datasets and generates summary report.
|
| 4 |
-
"""
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
import pandas as pd
|
| 7 |
-
from src.config.settings import (
|
| 8 |
-
STATION_LIST, IDPR_FILE, ADES_STATIONS, ADES_LEVELS,
|
| 9 |
-
WATERSHED_RISLE, WATERSHED_EURE, WATERSHED_MERGED
|
| 10 |
-
)
|
| 11 |
-
from src.utils.station_utils import load_station_metadata, get_station_summary
|
| 12 |
-
from src.data.loaders.shapefile import ShapefileLoader
|
| 13 |
-
from src.data.loaders.idpr import IDPRLoader
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
def main():
|
| 17 |
-
"""Explore all available datasets."""
|
| 18 |
-
|
| 19 |
-
print("="*80)
|
| 20 |
-
print("DATASET EXPLORATION - Hydrological GNN Project")
|
| 21 |
-
print("="*80)
|
| 22 |
-
|
| 23 |
-
# ========================================================================
|
| 24 |
-
# 1. Station Metadata
|
| 25 |
-
# ========================================================================
|
| 26 |
-
print("\n[1] STATION METADATA")
|
| 27 |
-
print("-" * 80)
|
| 28 |
-
|
| 29 |
-
try:
|
| 30 |
-
station_df = load_station_metadata(add_basin_id=True)
|
| 31 |
-
print(f"✓ Loaded {len(station_df)} stations from station_list.csv")
|
| 32 |
-
print(f"\nColumns: {list(station_df.columns)}")
|
| 33 |
-
|
| 34 |
-
# Basin summary
|
| 35 |
-
summary = get_station_summary()
|
| 36 |
-
print(f"\nBasin Distribution:")
|
| 37 |
-
print(f" - La Risle: {summary['risle_stations']} stations")
|
| 38 |
-
print(f" - La Eure: {summary['eure_stations']} stations")
|
| 39 |
-
|
| 40 |
-
# Coordinate bounds
|
| 41 |
-
print(f"\nSpatial Extent:")
|
| 42 |
-
print(f" - Longitude: {summary['bbox']['lon_min']:.3f} to {summary['bbox']['lon_max']:.3f}")
|
| 43 |
-
print(f" - Latitude: {summary['bbox']['lat_min']:.3f} to {summary['bbox']['lat_max']:.3f}")
|
| 44 |
-
|
| 45 |
-
# Sample stations
|
| 46 |
-
print(f"\nSample Stations (La Risle):")
|
| 47 |
-
risle_sample = station_df[station_df['basin_id'] == 0].head(3)
|
| 48 |
-
for _, row in risle_sample.iterrows():
|
| 49 |
-
print(f" - {row['station_code']}: {row['station_name']}")
|
| 50 |
-
|
| 51 |
-
print(f"\nSample Stations (La Eure):")
|
| 52 |
-
eure_sample = station_df[station_df['basin_id'] == 1].head(3)
|
| 53 |
-
for _, row in eure_sample.iterrows():
|
| 54 |
-
print(f" - {row['station_code']}: {row['station_name']}")
|
| 55 |
-
|
| 56 |
-
except Exception as e:
|
| 57 |
-
print(f"✗ Error loading station metadata: {e}")
|
| 58 |
-
|
| 59 |
-
# ========================================================================
|
| 60 |
-
# 2. IDPR Data
|
| 61 |
-
# ========================================================================
|
| 62 |
-
print("\n[2] IDPR DATA (Infiltration vs Runoff)")
|
| 63 |
-
print("-" * 80)
|
| 64 |
-
|
| 65 |
-
try:
|
| 66 |
-
idpr_loader = IDPRLoader(IDPR_FILE)
|
| 67 |
-
idpr_df = idpr_loader.load()
|
| 68 |
-
print(f"✓ Loaded {len(idpr_df)} IDPR values")
|
| 69 |
-
print(f"\nColumns: {list(idpr_df.columns)}")
|
| 70 |
-
|
| 71 |
-
print(f"\nIDPR Statistics:")
|
| 72 |
-
print(f" - Mean IDPR: {idpr_df['IDPR'].mean():.1f}")
|
| 73 |
-
print(f" - Min IDPR: {idpr_df['IDPR'].min():.1f} (more runoff)")
|
| 74 |
-
print(f" - Max IDPR: {idpr_df['IDPR'].max():.1f} (more infiltration)")
|
| 75 |
-
|
| 76 |
-
# IDPR by basin
|
| 77 |
-
if 'basin_name' in idpr_df.columns:
|
| 78 |
-
print(f"\nIDPR by Basin:")
|
| 79 |
-
for basin in idpr_df['basin_name'].unique():
|
| 80 |
-
basin_data = idpr_df[idpr_df['basin_name'] == basin]
|
| 81 |
-
print(f" - {basin}: mean={basin_data['IDPR'].mean():.1f}, "
|
| 82 |
-
f"min={basin_data['IDPR'].min():.1f}, "
|
| 83 |
-
f"max={basin_data['IDPR'].max():.1f}")
|
| 84 |
-
|
| 85 |
-
except Exception as e:
|
| 86 |
-
print(f"✗ Error loading IDPR data: {e}")
|
| 87 |
-
|
| 88 |
-
# ========================================================================
|
| 89 |
-
# 3. ADES Groundwater Data
|
| 90 |
-
# ========================================================================
|
| 91 |
-
print("\n[3] ADES GROUNDWATER DATA")
|
| 92 |
-
print("-" * 80)
|
| 93 |
-
|
| 94 |
-
try:
|
| 95 |
-
# Groundwater stations
|
| 96 |
-
gw_stations = pd.read_csv(ADES_STATIONS)
|
| 97 |
-
print(f"✓ Loaded {len(gw_stations)} groundwater monitoring wells")
|
| 98 |
-
print(f"\nColumns: {list(gw_stations.columns)[:10]}... ({len(gw_stations.columns)} total)")
|
| 99 |
-
|
| 100 |
-
print(f"\nGroundwater Wells Statistics:")
|
| 101 |
-
print(f" - Mean altitude: {gw_stations['altitude_station'].mean():.1f} m")
|
| 102 |
-
print(f" - Mean depth: {gw_stations['profondeur_investigation'].mean():.1f} m")
|
| 103 |
-
print(f" - Total measurements: {gw_stations['nb_mesures_piezo'].sum():.0f}")
|
| 104 |
-
|
| 105 |
-
# Groundwater levels (large file - just peek)
|
| 106 |
-
print(f"\n✓ Groundwater levels file exists: {ADES_LEVELS.exists()}")
|
| 107 |
-
if ADES_LEVELS.exists():
|
| 108 |
-
# Just read first few rows
|
| 109 |
-
gw_levels_sample = pd.read_csv(ADES_LEVELS, nrows=5)
|
| 110 |
-
print(f" - Estimated total rows: ~272,379 (large file!)")
|
| 111 |
-
print(f" - Columns: {list(gw_levels_sample.columns)[:8]}...")
|
| 112 |
-
|
| 113 |
-
except Exception as e:
|
| 114 |
-
print(f"✗ Error loading ADES data: {e}")
|
| 115 |
-
|
| 116 |
-
# ========================================================================
|
| 117 |
-
# 4. Watershed Shapefiles
|
| 118 |
-
# ========================================================================
|
| 119 |
-
print("\n[4] WATERSHED SHAPEFILES")
|
| 120 |
-
print("-" * 80)
|
| 121 |
-
|
| 122 |
-
try:
|
| 123 |
-
# La Risle
|
| 124 |
-
risle_loader = ShapefileLoader(WATERSHED_RISLE)
|
| 125 |
-
risle_gdf = risle_loader.load()
|
| 126 |
-
print(f"✓ La Risle watershed loaded")
|
| 127 |
-
print(f" - CRS: {risle_gdf.crs}")
|
| 128 |
-
print(f" - Area: {risle_gdf.geometry.area.sum() / 1e6:.2f} km²")
|
| 129 |
-
print(f" - Attributes: {list(risle_gdf.columns)}")
|
| 130 |
-
|
| 131 |
-
# La Eure
|
| 132 |
-
eure_loader = ShapefileLoader(WATERSHED_EURE)
|
| 133 |
-
eure_gdf = eure_loader.load()
|
| 134 |
-
print(f"\n✓ La Eure watershed loaded")
|
| 135 |
-
print(f" - CRS: {eure_gdf.crs}")
|
| 136 |
-
print(f" - Area: {eure_gdf.geometry.area.sum() / 1e6:.2f} km²")
|
| 137 |
-
|
| 138 |
-
# Merged
|
| 139 |
-
merged_loader = ShapefileLoader(WATERSHED_MERGED)
|
| 140 |
-
merged_gdf = merged_loader.load()
|
| 141 |
-
print(f"\n✓ Merged watershed loaded")
|
| 142 |
-
print(f" - Total area: {merged_gdf.geometry.area.sum() / 1e6:.2f} km²")
|
| 143 |
-
|
| 144 |
-
except Exception as e:
|
| 145 |
-
print(f"✗ Error loading shapefiles: {e}")
|
| 146 |
-
|
| 147 |
-
# ========================================================================
|
| 148 |
-
# 5. Missing Data Summary
|
| 149 |
-
# ========================================================================
|
| 150 |
-
print("\n[5] MISSING DATA")
|
| 151 |
-
print("-" * 80)
|
| 152 |
-
|
| 153 |
-
missing_data = [
|
| 154 |
-
("Hydrometric data (Hub'Eau)", "datasets/hydrometric/", "*.csv or *.json"),
|
| 155 |
-
("SAFRAN meteorological data", "datasets/safran/", "*.csv or *.json"),
|
| 156 |
-
("DEM elevation data", "datasets/DEM/", "*.tif"),
|
| 157 |
-
("River network topology", "datasets/station_edges.csv", "CSV file"),
|
| 158 |
-
("Basin attributes (extracted)", "datasets/basin_features/", "*.csv")
|
| 159 |
-
]
|
| 160 |
-
|
| 161 |
-
print("The following datasets are needed to run the full pipeline:\n")
|
| 162 |
-
for name, path, format_type in missing_data:
|
| 163 |
-
print(f" ✗ {name}")
|
| 164 |
-
print(f" Expected: {path} ({format_type})")
|
| 165 |
-
|
| 166 |
-
# ========================================================================
|
| 167 |
-
# Summary
|
| 168 |
-
# ========================================================================
|
| 169 |
-
print("\n" + "="*80)
|
| 170 |
-
print("SUMMARY")
|
| 171 |
-
print("="*80)
|
| 172 |
-
print("\n✓ Available Data:")
|
| 173 |
-
print(" - 27 hydrometric station locations (La Risle: 12, La Eure: 13)")
|
| 174 |
-
print(" - 27 IDPR values (karst infiltration/runoff index)")
|
| 175 |
-
print(" - 114 groundwater monitoring wells (ADES)")
|
| 176 |
-
print(" - 272,379 groundwater level measurements")
|
| 177 |
-
print(" - 3 watershed boundary shapefiles (Risle, Eure, Merged)")
|
| 178 |
-
|
| 179 |
-
print("\n✗ Missing Data:")
|
| 180 |
-
print(" - Hydrometric time series (QmnJ, QIXnJ, HIXnJ) - Hub'Eau API")
|
| 181 |
-
print(" - SAFRAN meteorological time series (12 variables) - Météo-France")
|
| 182 |
-
print(" - DEM for topological feature extraction")
|
| 183 |
-
print(" - River network topology (station_edges.csv)")
|
| 184 |
-
|
| 185 |
-
print("\n📋 Next Steps:")
|
| 186 |
-
print(" 1. Download hydrometric data from Hub'Eau API")
|
| 187 |
-
print(" 2. Download SAFRAN meteorological data for station coordinates")
|
| 188 |
-
print(" 3. Acquire DEM (IGN RGE ALTI or EU-DEM)")
|
| 189 |
-
print(" 4. Use QGIS to extract river network topology")
|
| 190 |
-
print(" 5. Run example_pipeline.py to process all data")
|
| 191 |
-
|
| 192 |
-
print("\n" + "="*80)
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
if __name__ == "__main__":
|
| 196 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/test_fusion.py
DELETED
|
@@ -1,50 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Test updated DataFusionEngine with actual downloaded data.
|
| 3 |
-
"""
|
| 4 |
-
import sys
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
|
| 7 |
-
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 8 |
-
|
| 9 |
-
from src.fusion.fusion_engine import DataFusionEngine
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
def main():
|
| 13 |
-
# Initialize fusion engine
|
| 14 |
-
fusion = DataFusionEngine(
|
| 15 |
-
hydrometric_dir="datasets/hydrometric",
|
| 16 |
-
era5_dir="datasets/safran",
|
| 17 |
-
station_list_path="datasets/station_list.csv",
|
| 18 |
-
normalize=False, # Keep raw values for inspection
|
| 19 |
-
normalization_method="robust"
|
| 20 |
-
)
|
| 21 |
-
|
| 22 |
-
# Run fusion for January 2020 (our ERA5 sample data)
|
| 23 |
-
fused_df = fusion.fuse(
|
| 24 |
-
start_date="2000-01-01",
|
| 25 |
-
end_date="2026-01-31"
|
| 26 |
-
)
|
| 27 |
-
|
| 28 |
-
# Display results
|
| 29 |
-
print("\n=== FUSED DATA SAMPLE ===")
|
| 30 |
-
print(fused_df.head(10))
|
| 31 |
-
|
| 32 |
-
print("\n=== COLUMNS ===")
|
| 33 |
-
print(fused_df.columns.tolist())
|
| 34 |
-
|
| 35 |
-
print("\n=== DATA TYPES ===")
|
| 36 |
-
print(fused_df.dtypes)
|
| 37 |
-
|
| 38 |
-
print("\n=== SUMMARY STATISTICS BY STATION ===")
|
| 39 |
-
summary = fusion.get_summary_stats()
|
| 40 |
-
print(summary)
|
| 41 |
-
|
| 42 |
-
# Save output
|
| 43 |
-
output_path = "datasets/fused/fused.csv"
|
| 44 |
-
fusion.save(output_path)
|
| 45 |
-
|
| 46 |
-
print(f"\n✓ Test complete!")
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
if __name__ == "__main__":
|
| 50 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/test_graph.py
DELETED
|
@@ -1,148 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Comprehensive test script for physics-informed hydrological graph construction and spatiotemporal dataset creation.
|
| 3 |
-
"""
|
| 4 |
-
import sys
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
import pandas as pd
|
| 7 |
-
import numpy as np
|
| 8 |
-
import torch
|
| 9 |
-
|
| 10 |
-
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 11 |
-
|
| 12 |
-
from src.fusion.fusion_engine import DataFusionEngine
|
| 13 |
-
from src.graph.builder import HydrologicalGraphBuilder
|
| 14 |
-
from src.graph.graph_from_fused import FusedDataGraphBuilder
|
| 15 |
-
from src.graph.visualize import GraphVisualizer
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def create_synthetic_fused_data():
|
| 19 |
-
"""Create synthetic fused data if datasets/fused/fused.csv does not exist."""
|
| 20 |
-
print(" Creating synthetic fused data for testing...")
|
| 21 |
-
dates = pd.date_range("2020-01-01", "2020-01-31", freq="D")
|
| 22 |
-
stations = [
|
| 23 |
-
{"station_code": "H4012010", "basin_id": 0, "lat": 49.123, "lon": 0.543},
|
| 24 |
-
{"station_code": "H4023020", "basin_id": 0, "lat": 49.200, "lon": 0.600},
|
| 25 |
-
{"station_code": "H4050001", "basin_id": 0, "lat": 49.300, "lon": 0.700},
|
| 26 |
-
{"station_code": "H9011010", "basin_id": 1, "lat": 48.800, "lon": 1.200},
|
| 27 |
-
{"station_code": "H9022020", "basin_id": 1, "lat": 48.900, "lon": 1.300},
|
| 28 |
-
]
|
| 29 |
-
|
| 30 |
-
records = []
|
| 31 |
-
for d in dates:
|
| 32 |
-
for st in stations:
|
| 33 |
-
records.append({
|
| 34 |
-
"date": d,
|
| 35 |
-
"station_code": st["station_code"],
|
| 36 |
-
"basin_id": st["basin_id"],
|
| 37 |
-
"discharge_m3s": np.random.uniform(2.0, 50.0),
|
| 38 |
-
"waterlevel_mm": np.random.uniform(300.0, 1500.0),
|
| 39 |
-
"temp_C": np.random.uniform(5.0, 18.0),
|
| 40 |
-
"wind_speed_ms": np.random.uniform(1.0, 8.0),
|
| 41 |
-
"IDPR": 120.0 if st["basin_id"] == 0 else 150.0
|
| 42 |
-
})
|
| 43 |
-
|
| 44 |
-
df = pd.DataFrame(records)
|
| 45 |
-
station_df = pd.DataFrame(stations)
|
| 46 |
-
return df, station_df
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def main():
|
| 50 |
-
print("=" * 80)
|
| 51 |
-
print("SPATIOTEMPORAL HYDROLOGICAL GRAPH CONSTRUCTION TEST")
|
| 52 |
-
print("=" * 80)
|
| 53 |
-
|
| 54 |
-
# Step 1: Load or create fused data & station metadata
|
| 55 |
-
print("\n1. Loading fused data & station list...")
|
| 56 |
-
fused_path = Path("datasets/fused/fused.csv")
|
| 57 |
-
station_path = Path("datasets/station_list.csv")
|
| 58 |
-
|
| 59 |
-
if fused_path.exists() and station_path.exists():
|
| 60 |
-
fused_df = pd.read_csv(fused_path)
|
| 61 |
-
fused_df['date'] = pd.to_datetime(fused_df['date'])
|
| 62 |
-
stations = pd.read_csv(station_path)
|
| 63 |
-
print(f" ✓ Loaded fused data from {fused_path}")
|
| 64 |
-
else:
|
| 65 |
-
fused_df, stations = create_synthetic_fused_data()
|
| 66 |
-
print(" ✓ Synthetic test data generated.")
|
| 67 |
-
|
| 68 |
-
fused_stations = fused_df['station_code'].unique()
|
| 69 |
-
station_coords = stations[stations['station_code'].isin(fused_stations)].copy()
|
| 70 |
-
if 'station_id' not in station_coords.columns:
|
| 71 |
-
station_coords['station_id'] = station_coords['station_code']
|
| 72 |
-
|
| 73 |
-
if 'basin_id' not in station_coords.columns:
|
| 74 |
-
basin_map = fused_df[['station_code', 'basin_id']].drop_duplicates().set_index('station_code')['basin_id']
|
| 75 |
-
station_coords['basin_id'] = station_coords['station_code'].map(basin_map)
|
| 76 |
-
|
| 77 |
-
print(f" ✓ Total observations: {len(fused_df)}")
|
| 78 |
-
print(f" ✓ Active stations: {len(station_coords)}")
|
| 79 |
-
|
| 80 |
-
# Step 2: Test HydrologicalGraphBuilder (Physics-informed static topology)
|
| 81 |
-
print("\n2. Testing HydrologicalGraphBuilder (Static River Graph)...")
|
| 82 |
-
phy_builder = HydrologicalGraphBuilder(allow_disconnected=True, allow_cross_basin=False)
|
| 83 |
-
|
| 84 |
-
sample_edges = pd.DataFrame([
|
| 85 |
-
{"source": station_coords['station_id'].iloc[0], "target": station_coords['station_id'].iloc[1], "distance": 12.5, "elevation_gradient": 15.0, "basin_id": 0}
|
| 86 |
-
]) if len(station_coords) >= 2 else pd.DataFrame()
|
| 87 |
-
|
| 88 |
-
static_graph = phy_builder.build(station_df=station_coords, edge_df=sample_edges)
|
| 89 |
-
stats = phy_builder.get_graph_statistics()
|
| 90 |
-
|
| 91 |
-
print(f" ✓ Static nodes tensor shape: {static_graph.x.shape}")
|
| 92 |
-
print(f" ✓ Static edge index shape: {static_graph.edge_index.shape}")
|
| 93 |
-
print(f" ✓ Basin IDs tensor: {static_graph.basin_id}")
|
| 94 |
-
|
| 95 |
-
# Step 3: Test FusedDataGraphBuilder (Spatial-Temporal Snapshots & Sequences)
|
| 96 |
-
print("\n3. Testing FusedDataGraphBuilder...")
|
| 97 |
-
graph_builder = FusedDataGraphBuilder(
|
| 98 |
-
station_coords=station_coords,
|
| 99 |
-
max_distance_km=100.0,
|
| 100 |
-
allow_cross_basin=False,
|
| 101 |
-
custom_edge_index=static_graph.edge_index,
|
| 102 |
-
custom_edge_attr=static_graph.edge_attr
|
| 103 |
-
)
|
| 104 |
-
|
| 105 |
-
feature_cols = [c for c in ['discharge_m3s', 'waterlevel_mm', 'temp_C', 'wind_speed_ms', 'IDPR'] if c in fused_df.columns]
|
| 106 |
-
sample_date = fused_df['date'].iloc[0].strftime('%Y-%m-%d')
|
| 107 |
-
|
| 108 |
-
snapshot = graph_builder.build_snapshot_graph(
|
| 109 |
-
fused_df=fused_df,
|
| 110 |
-
date=sample_date,
|
| 111 |
-
feature_cols=feature_cols
|
| 112 |
-
)
|
| 113 |
-
|
| 114 |
-
print(f" ✓ Snapshot for {sample_date}:")
|
| 115 |
-
print(f" Node features shape: {snapshot.x.shape}")
|
| 116 |
-
print(f" Edge index shape: {snapshot.edge_index.shape}")
|
| 117 |
-
|
| 118 |
-
# Step 4: Test Spatiotemporal Tensor Generation X in R^(N x T x S x F)
|
| 119 |
-
print("\n4. Generating 4D Spatiotemporal Tensors for GNN Training...")
|
| 120 |
-
window_size = 5
|
| 121 |
-
X_tensor, Y_tensor = graph_builder.build_spatiotemporal_tensors(
|
| 122 |
-
fused_df=fused_df,
|
| 123 |
-
window_size=window_size,
|
| 124 |
-
stride=1,
|
| 125 |
-
feature_cols=feature_cols,
|
| 126 |
-
target_cols=['discharge_m3s'] if 'discharge_m3s' in feature_cols else None
|
| 127 |
-
)
|
| 128 |
-
|
| 129 |
-
print(f" ✓ Input Tensor X shape: {X_tensor.shape} [N_samples, T_window, S_nodes, F_features]")
|
| 130 |
-
if Y_tensor is not None:
|
| 131 |
-
print(f" ✓ Target Tensor Y shape: {Y_tensor.shape} [N_samples, S_nodes, Target_features]")
|
| 132 |
-
|
| 133 |
-
# Step 5: Test Visualization
|
| 134 |
-
print("\n5. Testing Graph Visualizer...")
|
| 135 |
-
try:
|
| 136 |
-
vis = GraphVisualizer(station_coords=station_coords, edge_index=snapshot.edge_index.numpy())
|
| 137 |
-
fig = vis.plot_spatial_graph(title="Test Spatial Hydrological Network")
|
| 138 |
-
print(" ✓ Spatial graph plot generated successfully.")
|
| 139 |
-
except Exception as e:
|
| 140 |
-
print(f" ! Visualization notice: {e}")
|
| 141 |
-
|
| 142 |
-
print("\n" + "=" * 80)
|
| 143 |
-
print("ALL GRAPH PIPELINE TESTS PASSED")
|
| 144 |
-
print("=" * 80)
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
if __name__ == "__main__":
|
| 148 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/test_loaders.py
DELETED
|
@@ -1,81 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Test updated loaders with actual downloaded data.
|
| 3 |
-
"""
|
| 4 |
-
import sys
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
import pandas as pd
|
| 7 |
-
|
| 8 |
-
# Add src to path
|
| 9 |
-
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 10 |
-
|
| 11 |
-
from src.data.loaders.hydrometric import HydrometricLoader
|
| 12 |
-
from src.data.loaders.safran import SAFRANLoader
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
def main():
|
| 16 |
-
print("=" * 80)
|
| 17 |
-
print("TESTING DATA LOADERS WITH ACTUAL DATA")
|
| 18 |
-
print("=" * 80)
|
| 19 |
-
|
| 20 |
-
# Test 1: Hub'Eau Hydrometric Data
|
| 21 |
-
print("\n1. Loading Hub'Eau hydrometric data...")
|
| 22 |
-
hydro_loader = HydrometricLoader(
|
| 23 |
-
data_path=Path("datasets/hydrometric"),
|
| 24 |
-
min_quality=True
|
| 25 |
-
)
|
| 26 |
-
|
| 27 |
-
hydro_df = hydro_loader.load()
|
| 28 |
-
print(f" ✓ Loaded {len(hydro_df):,} observations")
|
| 29 |
-
print(f" Stations: {hydro_df['station_code'].nunique()}")
|
| 30 |
-
print(f" Date range: {hydro_df['date'].min()} to {hydro_df['date'].max()}")
|
| 31 |
-
print(f" Columns: {list(hydro_df.columns)}")
|
| 32 |
-
print(f"\n Sample:")
|
| 33 |
-
print(hydro_df.head(3))
|
| 34 |
-
|
| 35 |
-
# Test 2: ERA5 Data
|
| 36 |
-
print("\n2. Loading ERA5 meteorological data...")
|
| 37 |
-
|
| 38 |
-
# Load station coordinates
|
| 39 |
-
stations = pd.read_csv("datasets/station_list.csv")
|
| 40 |
-
station_coords = stations[['station_code', 'lat', 'lon']].copy()
|
| 41 |
-
|
| 42 |
-
era5_loader = SAFRANLoader(
|
| 43 |
-
data_path=Path("datasets/safran"),
|
| 44 |
-
station_coords=station_coords,
|
| 45 |
-
interp_method="nearest"
|
| 46 |
-
)
|
| 47 |
-
|
| 48 |
-
era5_df = era5_loader.load()
|
| 49 |
-
print(f" ✓ Loaded {len(era5_df):,} observations")
|
| 50 |
-
print(f" Stations: {era5_df['station_code'].nunique()}")
|
| 51 |
-
print(f" Date range: {era5_df['date'].min()} to {era5_df['date'].max()}")
|
| 52 |
-
print(f" Columns: {list(era5_df.columns)}")
|
| 53 |
-
|
| 54 |
-
# Convert units
|
| 55 |
-
print("\n3. Converting ERA5 units (K→°C, m→mm)...")
|
| 56 |
-
era5_df = SAFRANLoader.convert_units(era5_df)
|
| 57 |
-
print(f" ✓ Converted columns: {[c for c in era5_df.columns if c not in ['date', 'station_code']]}")
|
| 58 |
-
print(f"\n Sample:")
|
| 59 |
-
print(era5_df.head(3))
|
| 60 |
-
|
| 61 |
-
# Test 3: Merge data
|
| 62 |
-
print("\n4. Merging hydrometric + ERA5...")
|
| 63 |
-
merged = pd.merge(
|
| 64 |
-
hydro_df,
|
| 65 |
-
era5_df,
|
| 66 |
-
on=['date', 'station_code'],
|
| 67 |
-
how='inner'
|
| 68 |
-
)
|
| 69 |
-
print(f" ✓ Merged {len(merged):,} rows")
|
| 70 |
-
print(f" Stations: {merged['station_code'].nunique()}")
|
| 71 |
-
print(f" Total columns: {len(merged.columns)}")
|
| 72 |
-
|
| 73 |
-
print("\n" + "=" * 80)
|
| 74 |
-
print("✓ ALL LOADERS WORKING!")
|
| 75 |
-
print("=" * 80)
|
| 76 |
-
|
| 77 |
-
return hydro_df, era5_df, merged
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
if __name__ == "__main__":
|
| 81 |
-
hydro_df, era5_df, merged_df = main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/visualize_full_graph.py
DELETED
|
@@ -1,194 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Generate and save complete visualization suite for the hydrological graph dataset.
|
| 3 |
-
"""
|
| 4 |
-
from pathlib import Path
|
| 5 |
-
import pandas as pd
|
| 6 |
-
import numpy as np
|
| 7 |
-
import torch
|
| 8 |
-
import networkx as nx
|
| 9 |
-
import matplotlib.pyplot as plt
|
| 10 |
-
|
| 11 |
-
import sys
|
| 12 |
-
from pathlib import Path
|
| 13 |
-
|
| 14 |
-
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 15 |
-
|
| 16 |
-
from src.graph.builder import HydrologicalGraphBuilder
|
| 17 |
-
from src.graph.visualize import GraphVisualizer
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def extract_station_coords(builder, fused_df: pd.DataFrame) -> pd.DataFrame:
|
| 21 |
-
"""Dynamically extract station metadata from builder attributes, graph, or fused data."""
|
| 22 |
-
# 1. Search builder instance attributes for a DataFrame
|
| 23 |
-
for attr, val in builder.__dict__.items():
|
| 24 |
-
if isinstance(val, pd.DataFrame) and len(val) > 0:
|
| 25 |
-
df = val.copy()
|
| 26 |
-
if 'station_id' in df.columns and 'station_code' not in df.columns:
|
| 27 |
-
df['station_code'] = df['station_id']
|
| 28 |
-
if 'latitude' in df.columns and 'lat' not in df.columns:
|
| 29 |
-
df['lat'] = df['latitude']
|
| 30 |
-
if 'longitude' in df.columns and 'lon' not in df.columns:
|
| 31 |
-
df['lon'] = df['longitude']
|
| 32 |
-
|
| 33 |
-
if {'station_code', 'lat', 'lon'}.issubset(df.columns) or 'basin_id' in df.columns:
|
| 34 |
-
print(f" ✓ Extracted station metadata from builder.{attr}")
|
| 35 |
-
return df
|
| 36 |
-
|
| 37 |
-
# 2. Extract from NetworkX graph if stored in builder
|
| 38 |
-
if hasattr(builder, 'G') and isinstance(builder.G, (nx.Graph, nx.DiGraph)):
|
| 39 |
-
rows = []
|
| 40 |
-
for n, data in builder.G.nodes(data=True):
|
| 41 |
-
pos = data.get('pos', (0.0, 0.0))
|
| 42 |
-
rows.append({
|
| 43 |
-
'station_code': data.get('station_code', f"node_{n}"),
|
| 44 |
-
'lon': pos[0],
|
| 45 |
-
'lat': pos[1],
|
| 46 |
-
'basin_id': data.get('basin_id', 0)
|
| 47 |
-
})
|
| 48 |
-
if rows:
|
| 49 |
-
print(" ✓ Extracted station metadata from builder network graph (builder.G)")
|
| 50 |
-
return pd.DataFrame(rows)
|
| 51 |
-
|
| 52 |
-
# 3. Fallback: Build station table from fused dataset unique stations
|
| 53 |
-
unique_stations = (
|
| 54 |
-
fused_df['station_code'].unique()
|
| 55 |
-
if 'station_code' in fused_df.columns
|
| 56 |
-
else fused_df['station_id'].unique()
|
| 57 |
-
)
|
| 58 |
-
print(" ! Generating station metadata table from fused observation stations...")
|
| 59 |
-
|
| 60 |
-
half = len(unique_stations) // 2 + 1
|
| 61 |
-
return pd.DataFrame({
|
| 62 |
-
'station_code': unique_stations,
|
| 63 |
-
'lat': [0.0 + i * 0.05 for i in range(len(unique_stations))],
|
| 64 |
-
'lon': [36.0 + i * 0.05 for i in range(len(unique_stations))],
|
| 65 |
-
'basin_id': [0 if i < half else 1 for i in range(len(unique_stations))]
|
| 66 |
-
})
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
def main():
|
| 70 |
-
print("=" * 80)
|
| 71 |
-
print("HYDROLOGICAL GRAPH FULL VISUALIZATION SUITE")
|
| 72 |
-
print("=" * 80)
|
| 73 |
-
|
| 74 |
-
# 1. Setup output directory
|
| 75 |
-
output_dir = Path("outputs/plots")
|
| 76 |
-
output_dir.mkdir(parents=True, exist_ok=True)
|
| 77 |
-
print(f"\n1. Output directory ready at: {output_dir.resolve()}")
|
| 78 |
-
|
| 79 |
-
# 2. Load fused observations data
|
| 80 |
-
print("\n2. Loading fused observations dataset...")
|
| 81 |
-
fused_csv_path = Path("datasets/fused/fused.csv")
|
| 82 |
-
fused_df = pd.read_csv(fused_csv_path)
|
| 83 |
-
|
| 84 |
-
if 'station_id' in fused_df.columns and 'station_code' not in fused_df.columns:
|
| 85 |
-
fused_df['station_code'] = fused_df['station_id']
|
| 86 |
-
|
| 87 |
-
# 3. Instantiate builder & extract station metadata + edge index
|
| 88 |
-
print("\n3. Initializing HydrologicalGraphBuilder...")
|
| 89 |
-
try:
|
| 90 |
-
builder = HydrologicalGraphBuilder()
|
| 91 |
-
except Exception:
|
| 92 |
-
builder = HydrologicalGraphBuilder(fused_df)
|
| 93 |
-
|
| 94 |
-
station_coords = extract_station_coords(builder, fused_df)
|
| 95 |
-
|
| 96 |
-
if 'basin_id' not in station_coords.columns:
|
| 97 |
-
station_coords['basin_id'] = 0
|
| 98 |
-
|
| 99 |
-
edge_index = getattr(builder, 'edge_index', None)
|
| 100 |
-
if edge_index is None and hasattr(builder, 'get_edge_index'):
|
| 101 |
-
edge_index = builder.get_edge_index()
|
| 102 |
-
|
| 103 |
-
if isinstance(edge_index, torch.Tensor):
|
| 104 |
-
edge_index = edge_index.cpu().numpy()
|
| 105 |
-
|
| 106 |
-
# 4. Initialize Visualizer
|
| 107 |
-
print("\n4. Initializing GraphVisualizer...")
|
| 108 |
-
visualizer = GraphVisualizer(
|
| 109 |
-
station_coords=station_coords,
|
| 110 |
-
edge_index=edge_index,
|
| 111 |
-
figsize=(12, 8),
|
| 112 |
-
directed=True
|
| 113 |
-
)
|
| 114 |
-
|
| 115 |
-
# -------------------------------------------------------------------------
|
| 116 |
-
# Plot 1: Spatial Graph Layout
|
| 117 |
-
# -------------------------------------------------------------------------
|
| 118 |
-
print(" [1/4] Plotting Spatial Graph Network...")
|
| 119 |
-
fig1 = visualizer.plot_spatial_graph(
|
| 120 |
-
title="Hydrological Station Graph & Basin Networks",
|
| 121 |
-
show_labels=True,
|
| 122 |
-
save_path=output_dir / "01_spatial_network.png"
|
| 123 |
-
)
|
| 124 |
-
plt.close(fig1)
|
| 125 |
-
|
| 126 |
-
# -------------------------------------------------------------------------
|
| 127 |
-
# Plot 2: Node Feature Heatmap
|
| 128 |
-
# -------------------------------------------------------------------------
|
| 129 |
-
print(" [2/4] Plotting Feature Heatmap...")
|
| 130 |
-
exclude_cols = {'basin_id', 'lat', 'lon', 'latitude', 'longitude', 'station_code', 'station_id', 'date'}
|
| 131 |
-
numeric_cols = [c for c in fused_df.select_dtypes(include=[np.number]).columns if c not in exclude_cols]
|
| 132 |
-
target_feature = numeric_cols[0] if numeric_cols else fused_df.columns[2]
|
| 133 |
-
|
| 134 |
-
# Use pivot_table with aggfunc='mean' to safely aggregate duplicate timestamps per station
|
| 135 |
-
pivoted = fused_df.pivot_table(
|
| 136 |
-
index='date',
|
| 137 |
-
columns='station_code',
|
| 138 |
-
values=target_feature,
|
| 139 |
-
aggfunc='mean'
|
| 140 |
-
).ffill().bfill().fillna(0)
|
| 141 |
-
|
| 142 |
-
feature_matrix_2d = pivoted.values.T # (num_nodes, num_timesteps)
|
| 143 |
-
|
| 144 |
-
fig2 = visualizer.plot_feature_heatmap(
|
| 145 |
-
feature_matrix=feature_matrix_2d,
|
| 146 |
-
feature_names=[target_feature],
|
| 147 |
-
title=f"Station Feature Heatmap ({target_feature})",
|
| 148 |
-
save_path=output_dir / "02_feature_heatmap.png"
|
| 149 |
-
)
|
| 150 |
-
plt.close(fig2)
|
| 151 |
-
|
| 152 |
-
# -------------------------------------------------------------------------
|
| 153 |
-
# Plot 3: Temporal Evolution Across Stations
|
| 154 |
-
# -------------------------------------------------------------------------
|
| 155 |
-
print(" [3/4] Plotting Temporal Feature Evolution...")
|
| 156 |
-
recent_df = fused_df.tail(100 * len(station_coords))
|
| 157 |
-
|
| 158 |
-
time_series = recent_df.pivot_table(
|
| 159 |
-
index='date',
|
| 160 |
-
columns='station_code',
|
| 161 |
-
values=target_feature,
|
| 162 |
-
aggfunc='mean'
|
| 163 |
-
).ffill().bfill().fillna(0)
|
| 164 |
-
|
| 165 |
-
temporal_data = time_series.values[:, :, np.newaxis]
|
| 166 |
-
dates = [str(d)[:10] for d in time_series.index]
|
| 167 |
-
|
| 168 |
-
fig3 = visualizer.plot_temporal_evolution(
|
| 169 |
-
temporal_data=temporal_data,
|
| 170 |
-
feature_idx=0,
|
| 171 |
-
feature_name=target_feature,
|
| 172 |
-
dates=dates,
|
| 173 |
-
title=f"Temporal Dynamics of {target_feature} (Recent Windows)",
|
| 174 |
-
save_path=output_dir / "03_temporal_evolution.png"
|
| 175 |
-
)
|
| 176 |
-
plt.close(fig3)
|
| 177 |
-
|
| 178 |
-
# -------------------------------------------------------------------------
|
| 179 |
-
# Plot 4: Dataset & Graph Statistics Summary
|
| 180 |
-
# -------------------------------------------------------------------------
|
| 181 |
-
print(" [4/4] Plotting Graph Statistics Overview...")
|
| 182 |
-
fig4 = visualizer.plot_graph_statistics(
|
| 183 |
-
fused_df=fused_df,
|
| 184 |
-
save_path=output_dir / "04_graph_statistics.png"
|
| 185 |
-
)
|
| 186 |
-
plt.close(fig4)
|
| 187 |
-
|
| 188 |
-
print("\n" + "=" * 80)
|
| 189 |
-
print(f"SUCCESS: Saved all 4 plots to {output_dir.resolve()}/")
|
| 190 |
-
print("=" * 80)
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
if __name__ == "__main__":
|
| 194 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/visualize_graph.py
DELETED
|
@@ -1,210 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Visualize hydrological graphs and data.
|
| 3 |
-
"""
|
| 4 |
-
import sys
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
import pandas as pd
|
| 7 |
-
import numpy as np
|
| 8 |
-
import matplotlib.pyplot as plt
|
| 9 |
-
|
| 10 |
-
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 11 |
-
|
| 12 |
-
from src.graph.visualize import GraphVisualizer
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
def main():
|
| 16 |
-
print("=" * 80)
|
| 17 |
-
print("GRAPH VISUALIZATION")
|
| 18 |
-
print("=" * 80)
|
| 19 |
-
|
| 20 |
-
# Create output directory
|
| 21 |
-
output_dir = Path("outputs/visualizations")
|
| 22 |
-
output_dir.mkdir(parents=True, exist_ok=True)
|
| 23 |
-
|
| 24 |
-
# Load fused data
|
| 25 |
-
print("\n1. Loading data...")
|
| 26 |
-
fused_path = Path("datasets/fused/jan2020_fused.csv")
|
| 27 |
-
fused_df = pd.read_csv(fused_path)
|
| 28 |
-
fused_df['date'] = pd.to_datetime(fused_df['date'])
|
| 29 |
-
print(f" ✓ Loaded {len(fused_df)} observations")
|
| 30 |
-
|
| 31 |
-
# Prepare station coordinates
|
| 32 |
-
stations = pd.read_csv("datasets/station_list.csv")
|
| 33 |
-
fused_stations = fused_df['station_code'].unique()
|
| 34 |
-
station_coords = stations[stations['station_code'].isin(fused_stations)].copy()
|
| 35 |
-
station_coords = station_coords[['station_code', 'lat', 'lon']].copy()
|
| 36 |
-
|
| 37 |
-
# Add basin_id
|
| 38 |
-
basin_map = fused_df[['station_code', 'basin_id']].drop_duplicates().set_index('station_code')['basin_id']
|
| 39 |
-
station_coords['basin_id'] = station_coords['station_code'].map(basin_map)
|
| 40 |
-
station_coords = station_coords.reset_index(drop=True)
|
| 41 |
-
|
| 42 |
-
# Build edge index (simple proximity) - compute directly to avoid torch/numpy issues
|
| 43 |
-
print("\n2. Building graph structure...")
|
| 44 |
-
|
| 45 |
-
def haversine_distance(lat1, lon1, lat2, lon2):
|
| 46 |
-
"""Calculate distance between two points (km)."""
|
| 47 |
-
R = 6371 # Earth radius in km
|
| 48 |
-
lat1, lon1, lat2, lon2 = map(np.radians, [lat1, lon1, lat2, lon2])
|
| 49 |
-
dlat = lat2 - lat1
|
| 50 |
-
dlon = lon2 - lon1
|
| 51 |
-
a = np.sin(dlat/2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2
|
| 52 |
-
c = 2 * np.arcsin(np.sqrt(a))
|
| 53 |
-
return R * c
|
| 54 |
-
|
| 55 |
-
# Compute proximity edges
|
| 56 |
-
edges = []
|
| 57 |
-
max_distance_km = 100.0
|
| 58 |
-
|
| 59 |
-
for i, station_i in station_coords.iterrows():
|
| 60 |
-
for j, station_j in station_coords.iterrows():
|
| 61 |
-
if i >= j:
|
| 62 |
-
continue
|
| 63 |
-
|
| 64 |
-
# Check basin constraint
|
| 65 |
-
if station_i['basin_id'] != station_j['basin_id']:
|
| 66 |
-
continue
|
| 67 |
-
|
| 68 |
-
# Calculate distance
|
| 69 |
-
dist = haversine_distance(
|
| 70 |
-
station_i['lat'], station_i['lon'],
|
| 71 |
-
station_j['lat'], station_j['lon']
|
| 72 |
-
)
|
| 73 |
-
|
| 74 |
-
if dist <= max_distance_km:
|
| 75 |
-
edges.append([i, j])
|
| 76 |
-
edges.append([j, i]) # Undirected
|
| 77 |
-
|
| 78 |
-
if not edges:
|
| 79 |
-
edges = [[i, i] for i in range(len(station_coords))]
|
| 80 |
-
|
| 81 |
-
edge_index = np.array(edges).T
|
| 82 |
-
print(f" ✓ {len(station_coords)} nodes, {edge_index.shape[1]} edges")
|
| 83 |
-
|
| 84 |
-
# Initialize visualizer
|
| 85 |
-
print("\n3. Initializing visualizer...")
|
| 86 |
-
viz = GraphVisualizer(
|
| 87 |
-
station_coords=station_coords,
|
| 88 |
-
edge_index=edge_index,
|
| 89 |
-
figsize=(12, 8)
|
| 90 |
-
)
|
| 91 |
-
|
| 92 |
-
# Plot 1: Spatial graph structure
|
| 93 |
-
print("\n4. Plotting spatial graph...")
|
| 94 |
-
fig1 = viz.plot_spatial_graph(
|
| 95 |
-
title="Hydrological Network - Geographic Layout",
|
| 96 |
-
show_labels=True,
|
| 97 |
-
save_path=output_dir / "01_spatial_graph.png"
|
| 98 |
-
)
|
| 99 |
-
plt.close(fig1)
|
| 100 |
-
|
| 101 |
-
# Plot 2: Spatial graph colored by discharge
|
| 102 |
-
print("\n5. Plotting graph with discharge values...")
|
| 103 |
-
# Get average discharge per station
|
| 104 |
-
avg_discharge = fused_df.groupby('station_code')['discharge_m3s'].mean()
|
| 105 |
-
discharge_values = station_coords['station_code'].map(avg_discharge).values
|
| 106 |
-
|
| 107 |
-
fig2 = viz.plot_spatial_graph(
|
| 108 |
-
node_values=discharge_values,
|
| 109 |
-
value_label="Avg Discharge (m³/s)",
|
| 110 |
-
title="Network Colored by Average Discharge",
|
| 111 |
-
show_labels=True,
|
| 112 |
-
save_path=output_dir / "02_discharge_spatial.png"
|
| 113 |
-
)
|
| 114 |
-
plt.close(fig2)
|
| 115 |
-
|
| 116 |
-
# Plot 3: Feature snapshot for a specific date
|
| 117 |
-
print("\n6. Plotting feature heatmap...")
|
| 118 |
-
date = "2020-01-15"
|
| 119 |
-
snapshot = fused_df[fused_df['date'] == date].copy()
|
| 120 |
-
snapshot = snapshot.set_index('station_code').reindex(station_coords['station_code'])
|
| 121 |
-
|
| 122 |
-
feature_cols = ['discharge_m3s', 'waterlevel_mm', 'temp_C', 'wind_speed_ms',
|
| 123 |
-
'avg_groundwater_level_m', 'avg_groundwater_depth_m', 'IDPR']
|
| 124 |
-
available = [c for c in feature_cols if c in snapshot.columns]
|
| 125 |
-
feature_matrix = snapshot[available].fillna(0).values
|
| 126 |
-
|
| 127 |
-
fig3 = viz.plot_feature_heatmap(
|
| 128 |
-
feature_matrix=feature_matrix,
|
| 129 |
-
feature_names=available,
|
| 130 |
-
title=f"Node Features - {date}",
|
| 131 |
-
save_path=output_dir / "03_feature_snapshot.png"
|
| 132 |
-
)
|
| 133 |
-
plt.close(fig3)
|
| 134 |
-
|
| 135 |
-
# Plot 4: Temporal feature evolution
|
| 136 |
-
print("\n7. Plotting temporal evolution...")
|
| 137 |
-
# Prepare temporal data
|
| 138 |
-
dates = sorted(fused_df['date'].unique())[:14] # First 2 weeks
|
| 139 |
-
temporal_data = []
|
| 140 |
-
|
| 141 |
-
for date in dates:
|
| 142 |
-
snapshot = fused_df[fused_df['date'] == date].copy()
|
| 143 |
-
snapshot = snapshot.set_index('station_code').reindex(station_coords['station_code'])
|
| 144 |
-
features = snapshot[available].fillna(0).values
|
| 145 |
-
temporal_data.append(features)
|
| 146 |
-
|
| 147 |
-
temporal_data = np.array(temporal_data) # (time, nodes, features)
|
| 148 |
-
|
| 149 |
-
# Plot discharge evolution
|
| 150 |
-
if 'discharge_m3s' in available:
|
| 151 |
-
feat_idx = available.index('discharge_m3s')
|
| 152 |
-
fig4 = viz.plot_temporal_evolution(
|
| 153 |
-
temporal_data=temporal_data,
|
| 154 |
-
feature_idx=feat_idx,
|
| 155 |
-
feature_name='Discharge (m³/s)',
|
| 156 |
-
dates=[d.strftime('%m-%d') for d in dates],
|
| 157 |
-
title='Discharge Evolution - First 2 Weeks',
|
| 158 |
-
save_path=output_dir / "04_discharge_evolution.png"
|
| 159 |
-
)
|
| 160 |
-
plt.close(fig4)
|
| 161 |
-
|
| 162 |
-
# Plot temperature evolution
|
| 163 |
-
if 'temp_C' in available:
|
| 164 |
-
feat_idx = available.index('temp_C')
|
| 165 |
-
fig5 = viz.plot_temporal_evolution(
|
| 166 |
-
temporal_data=temporal_data,
|
| 167 |
-
feature_idx=feat_idx,
|
| 168 |
-
feature_name='Temperature (°C)',
|
| 169 |
-
dates=[d.strftime('%m-%d') for d in dates],
|
| 170 |
-
title='Temperature Evolution - First 2 Weeks',
|
| 171 |
-
save_path=output_dir / "05_temperature_evolution.png"
|
| 172 |
-
)
|
| 173 |
-
plt.close(fig5)
|
| 174 |
-
|
| 175 |
-
# Plot 5: Complete heatmap over time
|
| 176 |
-
print("\n8. Plotting temporal heatmap...")
|
| 177 |
-
fig6 = viz.plot_feature_heatmap(
|
| 178 |
-
feature_matrix=temporal_data,
|
| 179 |
-
feature_names=available,
|
| 180 |
-
dates=[d.strftime('%m-%d') for d in dates],
|
| 181 |
-
title='Features Over Time (First 2 Weeks)',
|
| 182 |
-
save_path=output_dir / "06_temporal_heatmap.png"
|
| 183 |
-
)
|
| 184 |
-
plt.close(fig6)
|
| 185 |
-
|
| 186 |
-
# Plot 6: Graph statistics overview
|
| 187 |
-
print("\n9. Plotting graph statistics...")
|
| 188 |
-
fig7 = viz.plot_graph_statistics(
|
| 189 |
-
fused_df=fused_df,
|
| 190 |
-
save_path=output_dir / "07_graph_statistics.png"
|
| 191 |
-
)
|
| 192 |
-
plt.close(fig7)
|
| 193 |
-
|
| 194 |
-
# Summary
|
| 195 |
-
print("\n" + "=" * 80)
|
| 196 |
-
print("VISUALIZATION COMPLETE")
|
| 197 |
-
print("=" * 80)
|
| 198 |
-
print(f"\n✓ Generated 7 visualizations in {output_dir}/")
|
| 199 |
-
print("\nGenerated plots:")
|
| 200 |
-
print(" 01_spatial_graph.png - Geographic network layout")
|
| 201 |
-
print(" 02_discharge_spatial.png - Network colored by discharge")
|
| 202 |
-
print(" 03_feature_snapshot.png - Feature values on 2020-01-15")
|
| 203 |
-
print(" 04_discharge_evolution.png - Discharge time series")
|
| 204 |
-
print(" 05_temperature_evolution.png - Temperature time series")
|
| 205 |
-
print(" 06_temporal_heatmap.png - All features over time")
|
| 206 |
-
print(" 07_graph_statistics.png - Data overview statistics")
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
if __name__ == "__main__":
|
| 210 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|