Source code for hiper.metrics.hyperedge_fragmentation

# -*- coding: utf-8 -*-
"""
hyperedge_fragmentation.py

This module implements the Hyperedge Fragmentation Index (HFI) metric for
measuring how much hyperedges have been fragmented by perturbations.
"""

from typing import TYPE_CHECKING

import numpy as np

if TYPE_CHECKING:
    from hiper.core import Hypernetwork


[docs] class HyperedgeFragmentationIndex: """ Computes the Hyperedge Fragmentation Index (HFI) metric. The Hyperedge Fragmentation Index measures how much the original hyperedges have been fragmented by a perturbation: :math:`\\text{HFI} = 1 - \\frac{1}{|E|} \\sum_{e \\in E} \\frac{|e \\cap V'|}{|e|}` where: - E is the original set of hyperedges - V' is the set of nodes after perturbation - :math:`e \\cap V'` is the intersection of original hyperedge e with remaining nodes - :math:`|e|` is the original size of hyperedge e A value of 0.0 indicates no fragmentation (all hyperedges intact), while 1.0 indicates complete fragmentation (all hyperedges destroyed). """
[docs] def __init__(self): """Initialize the Hyperedge Fragmentation Index metric.""" self.name = "Hyperedge Fragmentation Index" self.symbol = "HFI"
[docs] @staticmethod def compute(perturbed_hypergraph: 'Hypernetwork', original_hypergraph: 'Hypernetwork') -> float: """ Compute the Hyperedge Fragmentation Index metric. Args: perturbed_hypergraph: The hypergraph after perturbation. original_hypergraph: The original hypergraph before perturbation. Returns: The hyperedge fragmentation index value in [0, 1]. Raises: ValueError: If original_hypergraph has no hyperedges. """ if original_hypergraph.size() == 0: # No original hyperedges to fragment return 0.0 # Get the set of remaining nodes after perturbation remaining_nodes = set(perturbed_hypergraph.nodes.keys()) total_preservation_ratio = 0.0 hyperedge_count = 0 # For each original hyperedge, calculate preservation ratio for hyperedge_id in original_hypergraph.edges.keys(): original_nodes = set( original_hypergraph.get_nodes(hyperedge_id)) if len(original_nodes) == 0: # Skip empty hyperedges continue # Find intersection with remaining nodes preserved_nodes = original_nodes.intersection(remaining_nodes) # Calculate preservation ratio for this hyperedge preservation_ratio = len(preserved_nodes) / len(original_nodes) total_preservation_ratio += preservation_ratio hyperedge_count += 1 if hyperedge_count == 0: return 0.0 # Average preservation ratio avg_preservation = total_preservation_ratio / hyperedge_count # Fragmentation index is 1 minus preservation fragmentation_index = 1.0 - avg_preservation return max(0.0, min(1.0, fragmentation_index)) # Clamp to [0, 1]
[docs] @staticmethod def compute_detailed(perturbed_hypergraph: 'Hypernetwork', original_hypergraph: 'Hypernetwork') -> dict: """ Compute detailed fragmentation statistics. Args: perturbed_hypergraph: The hypergraph after perturbation. original_hypergraph: The original hypergraph before perturbation. Returns: Dictionary containing detailed fragmentation analysis. """ if original_hypergraph.size() == 0: return { 'fragmentation_index': 0.0, 'total_hyperedges': 0, 'intact_hyperedges': 0, 'partially_fragmented': 0, 'completely_destroyed': 0, 'preservation_ratios': [] } remaining_nodes = set(perturbed_hypergraph.nodes.keys()) preservation_ratios = [] intact_count = 0 partial_count = 0 destroyed_count = 0 for hyperedge_id in original_hypergraph.edges.keys(): original_nodes = set( original_hypergraph.get_nodes(hyperedge_id)) if len(original_nodes) == 0: continue preserved_nodes = original_nodes.intersection(remaining_nodes) preservation_ratio = len(preserved_nodes) / len(original_nodes) preservation_ratios.append(preservation_ratio) if preservation_ratio == 1.0: intact_count += 1 elif preservation_ratio == 0.0: destroyed_count += 1 else: partial_count += 1 total_hyperedges = len(preservation_ratios) avg_preservation = (np.mean(preservation_ratios) if preservation_ratios else 0.0) fragmentation_index = 1.0 - avg_preservation return { 'fragmentation_index': fragmentation_index, 'total_hyperedges': total_hyperedges, 'intact_hyperedges': intact_count, 'partially_fragmented': partial_count, 'completely_destroyed': destroyed_count, 'preservation_ratios': preservation_ratios, 'avg_preservation_ratio': avg_preservation }
[docs] @staticmethod def get_hyperedge_fragmentation_map(perturbed_hypergraph: 'Hypernetwork', original_hypergraph: 'Hypernetwork' ) -> dict: """ Get fragmentation status for each individual hyperedge. Args: perturbed_hypergraph: The hypergraph after perturbation. original_hypergraph: The original hypergraph before perturbation. Returns: Dictionary mapping hyperedge IDs to their fragmentation status. """ remaining_nodes = set(perturbed_hypergraph.nodes.keys()) fragmentation_map = {} for hyperedge_id in original_hypergraph.edges.keys(): original_nodes = set( original_hypergraph.get_nodes(hyperedge_id)) if len(original_nodes) == 0: fragmentation_map[hyperedge_id] = { 'status': 'empty', 'preservation_ratio': 0.0, 'original_size': 0, 'preserved_size': 0 } continue preserved_nodes = original_nodes.intersection(remaining_nodes) preservation_ratio = len(preserved_nodes) / len(original_nodes) if preservation_ratio == 1.0: status = 'intact' elif preservation_ratio == 0.0: status = 'destroyed' else: status = 'fragmented' fragmentation_map[hyperedge_id] = { 'status': status, 'preservation_ratio': preservation_ratio, 'original_size': len(original_nodes), 'preserved_size': len(preserved_nodes), 'original_nodes': original_nodes, 'preserved_nodes': preserved_nodes } return fragmentation_map
[docs] def __str__(self) -> str: """String representation of the metric.""" return f"{self.name} ({self.symbol})"
[docs] def __repr__(self) -> str: """Detailed string representation.""" return (f"HyperedgeFragmentationIndex(name='{self.name}', " f"symbol='{self.symbol}')")