Source code for powergrid_synth.core.analysis

r"""
Grid Analysis Module
====================

This module provides tools for analyzing the topological properties of power grid graphs.
It includes the :class:`GridAnalyzer` for calculating metrics like degree distribution,
path lengths, and clustering coefficients.
"""


import networkx as nx
import numpy as np
import matplotlib.pyplot as plt
from typing import Dict, Any, Optional

[docs] class GridAnalyzer: """ A class to perform topological analysis on power grid graphs. Can be used for both synthetic and real-world grids. Args: graph (nx.Graph): The networkx graph object representing the power grid. """ def __init__(self, graph: nx.Graph): self.graph = graph
[docs] def get_basic_stats(self) -> Dict[str, Any]: """ Returns fundamental counts (nodes, edges, density). Returns: Dict[str, Any]: A dictionary containing 'num_nodes', 'num_edges', and 'density'. """ return { "num_nodes": self.graph.number_of_nodes(), "num_edges": self.graph.number_of_edges(), "density": nx.density(self.graph) }
[docs] def get_path_metrics(self) -> Dict[str, Any]: """ Calculates path-based metrics: Diameter and Average Shortest Path Length. """ if nx.is_connected(self.graph): G_sub = self.graph is_connected = True else: # Get largest connected component largest_cc = max(nx.connected_components(self.graph), key=len) G_sub = self.graph.subgraph(largest_cc) is_connected = False try: diameter = nx.diameter(G_sub) avg_path_len = nx.average_shortest_path_length(G_sub) except Exception as e: # Fallback for empty graphs or other edge cases diameter = float('nan') avg_path_len = float('nan') return { "is_connected": is_connected, "lcc_size": len(G_sub), "diameter": diameter, "avg_path_length": avg_path_len }
[docs] def get_clustering_metrics(self) -> Dict[str, float]: """Calculates global and average local clustering coefficients.""" return { "avg_clustering_coef": nx.average_clustering(self.graph), "transitivity": nx.transitivity(self.graph) # Global clustering }
[docs] def analyze(self): """Prints a comprehensive text summary of the grid.""" stats = self.get_basic_stats() path_stats = self.get_path_metrics() clust_stats = self.get_clustering_metrics() print("\n=== Power Grid Topological Analysis ===") print(f"Nodes: {stats['num_nodes']}") print(f"Edges: {stats['num_edges']}") print(f"Density: {stats['density']:.6f}") conn_str = "Yes" if path_stats['is_connected'] else f"No (Metrics based on LCC of size {path_stats['lcc_size']})" print(f"Connected: {conn_str}") print(f"Diameter: {path_stats['diameter']}") print(f"Avg Shortest Path Length: {path_stats['avg_path_length']:.4f}") print(f"Avg Local Clustering Coeff: {clust_stats['avg_clustering_coef']:.4f}") print("=======================================\n")
[docs] def plot_degree_distribution(self, ax: Optional[plt.Axes] = None, log_scale: bool = True, figsize: tuple = (6, 4), title: str = "Degree Distribution"): """ Plots the degree distribution. Args: ax: Matplotlib axes to plot on. If None, creates a new figure. log_scale: If True, plots log-log. If False, plots linear histogram. figsize: Size of figure if creating a new one. title: Title of the plot. """ degrees = [d for n, d in self.graph.degree()] if not degrees: print("Graph has no nodes/degrees to plot.") return created_figure = False if ax is None: fig, ax = plt.subplots(figsize=figsize) created_figure = True if log_scale: # Log-Log Plot degree_counts = np.bincount(degrees) # Filter out zero counts for log plot vals = np.nonzero(degree_counts)[0] counts = degree_counts[vals] if len(vals) > 0: ax.loglog(vals, counts, 'bo', markersize=5) ax.set_xlabel("Degree (log)") ax.set_ylabel("Count (log)") else: # Linear Histogram ax.hist(degrees, bins=range(min(degrees), max(degrees) + 2), color='skyblue', edgecolor='black', alpha=0.7) ax.set_xlabel("Degree") ax.set_ylabel("Count") ax.set_title(title) ax.grid(True, which="both", ls="--", alpha=0.5) if created_figure: plt.tight_layout() plt.show()