KateShkuryna
feat: NetworkX graph engine, shock propagation, GDELT ingestion
9580dda
Raw History Blame Contribute Delete
2.4 kB
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Any
import networkx as nx
import yaml
if TYPE_CHECKING:
Graph = nx.DiGraph[str, dict[str, Any], dict[str, Any]]
class GraphValidationError(ValueError):
"""Raised when the seed graph fails structural validation."""
def load_seed_graph(seed_dir: Path) -> Graph:
"""Load nodes.yaml and edges.yaml into a validated directed graph."""
nodes = yaml.safe_load((seed_dir / "nodes.yaml").read_text()) or []
edges = yaml.safe_load((seed_dir / "edges.yaml").read_text()) or []
graph: Graph = nx.DiGraph()
for node in nodes:
node_data = dict(node)
node_id = node_data.pop("id")
graph.add_node(node_id, **node_data)
dangling_errors: list[str] = []
for edge in edges:
edge_data = dict(edge)
source = edge_data.pop("source")
target = edge_data.pop("target")
if source not in graph or target not in graph:
dangling_errors.append(f"dangling edge reference: {source} -> {target}")
continue
graph.add_edge(source, target, **edge_data)
try:
_validate(graph)
except GraphValidationError as error:
dangling_errors.append(str(error))
if dangling_errors:
raise GraphValidationError("; ".join(dangling_errors))
return graph
def _validate(graph: Graph) -> None:
"""Raise one error containing every structural violation in the graph."""
errors: list[str] = []
for source, target, attributes in graph.edges(data=True):
if source not in graph.nodes or target not in graph.nodes:
errors.append(f"dangling edge reference: {source} -> {target}")
if source == target:
errors.append(f"self-loop not allowed: {source} -> {target}")
weight = attributes.get("weight")
if not isinstance(weight, int | float) or not 0.0 <= weight <= 1.0:
errors.append(f"edge weight out of [0,1]: {source} -> {target} = {weight}")
for node_id, attributes in graph.nodes(data=True):
for field in ("criticality", "resilience"):
value: Any = attributes.get(field)
if not isinstance(value, int | float) or not 0.0 <= value <= 1.0:
errors.append(f"node {node_id} missing/invalid {field}")
if errors:
raise GraphValidationError("; ".join(errors))