"""
evaluate.py — Comprehensive Evaluation for M3 Graph GNN
========================================================
Produces:
1. R² comparison table (GraphSAGE vs GAT vs Classical Baseline)
2. Attribution accuracy vs ground truth source emissions
3. Interactive graph visualization → assets/m3_graph.html
4. Summary results plot → assets/m3_results.png
5. Sample attribution example (printed + saved)
Usage:
python evaluate.py
"""
import json
import sys
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import torch
import networkx as nx
sys.path.insert(0, str(Path(__file__).parent))
from model import GATRegressor, GraphSAGERegressor, build_node_regression_targets
from attribution import SourceAttributor
DATA_DIR = Path("/home/user/workspace/MicroPlastiNet/data/processed/m3")
CKPT_DIR = Path("/home/user/workspace/MicroPlastiNet/src/m3_graph_gnn/checkpoints")
ASSETS_DIR = Path("/home/user/workspace/MicroPlastiNet/assets")
ASSETS_DIR.mkdir(parents=True, exist_ok=True)
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# ──────────────────────────────────────────────────────────────────────────────
# 1. Load everything
# ──────────────────────────────────────────────────────────────────────────────
def load_all():
import pandas as pd
data = torch.load(DATA_DIR / "flow_graph.pt", weights_only=False)
data = data.to(DEVICE)
test_df = pd.read_csv(DATA_DIR / "test.csv")
train_df = pd.read_csv(DATA_DIR / "train.csv")
val_df = pd.read_csv(DATA_DIR / "val.csv")
with open(DATA_DIR / "source_emissions_ground_truth.json") as f:
ground_truth = {int(k): float(v) for k, v in json.load(f).items()}
with open(CKPT_DIR / "training_results.json") as f:
train_results = json.load(f)
return data, train_df, val_df, test_df, ground_truth, train_results
# ──────────────────────────────────────────────────────────────────────────────
# 2. Load trained models
# ──────────────────────────────────────────────────────────────────────────────
def load_models():
sage = GraphSAGERegressor(in_channels=9, hidden_channels=128, num_layers=3)
sage.load_state_dict(torch.load(CKPT_DIR / "graphsage_best.pt", map_location=DEVICE, weights_only=True))
sage = sage.to(DEVICE).eval()
gat = GATRegressor(in_channels=9, hidden_channels=64, heads=8)
gat.load_state_dict(torch.load(CKPT_DIR / "gat_best.pt", map_location=DEVICE, weights_only=True))
gat = gat.to(DEVICE).eval()
return sage, gat
# ──────────────────────────────────────────────────────────────────────────────
# 3. Interactive graph visualization (PyVis)
# ──────────────────────────────────────────────────────────────────────────────
def build_graph_visualization(data, node_meta_list, G_nx=None):
"""Build an interactive HTML graph using PyVis."""
try:
from pyvis.network import Network
except ImportError:
print("PyVis not available — skipping HTML visualization")
return None
net = Network(
height="800px",
width="100%",
bgcolor="#ffffff",
font_color="#0f172a",
directed=True,
notebook=False,
)
net.set_options("""
var options = {
"nodes": {
"borderWidth": 2,
"shadow": {"enabled": true, "color": "rgba(0,0,0,0.5)"}
},
"edges": {
"arrows": {"to": {"enabled": true, "scaleFactor": 0.6}},
"smooth": {"type": "dynamic"},
"color": {"inherit": false, "opacity": 0.5}
},
"physics": {
"enabled": true,
"barnesHut": {
"gravitationalConstant": -4000,
"centralGravity": 0.3,
"springLength": 80,
"damping": 0.5
}
}
}
""")
# Color scheme
type_colors = {
"station": "#0284c7", # sky blue
"factory": "#dc2626", # red
"urban_runoff": "#ea580c", # orange
"agricultural_runoff": "#16a34a", # green
"junction": "#94a3b8", # slate
}
type_sizes = {
"station": 18,
"factory": 16,
"urban_runoff": 14,
"agricultural_runoff": 12,
"junction": 8,
}
# Add nodes
for nid in range(data.num_nodes):
ntype = data.node_types[nid]
label = data.node_labels[nid]
lat = data.node_lats[nid]
lon = data.node_lons[nid]
color = type_colors.get(ntype, "#ffffff")
size = type_sizes.get(ntype, 10)
title = (
f"{label}
"
f"Type: {ntype}
"
f"Lat: {lat:.4f}, Lon: {lon:.4f}
"
f"Node ID: {nid}"
)
net.add_node(
int(nid),
label=label if ntype != "junction" else "",
title=title,
color=color,
size=float(size),
)
# Add edges (subsample for readability — show all edges ≤500)
ei = data.edge_index.cpu().numpy()
ea = data.edge_attr.cpu().numpy()
n_edges = ei.shape[1]
for i in range(n_edges):
u, v = int(ei[0, i]), int(ei[1, i])
flow_rate = ea[i, 0]
dist = ea[i, 1]
# Color edges by flow rate
r = int(255 * (1 - flow_rate))
b = int(255 * flow_rate)
color_hex = f"#{r:02x}00{b:02x}"
title = f"Flow: {flow_rate:.2f} | Dist: {dist:.2f}"
net.add_edge(u, v, color=color_hex, title=title, width=float(max(0.5, flow_rate * 2)))
# Add legend using a separate approach
legend_html = """