|
| 1 | +"""Visualization tools for THOL cascade dynamics. |
| 2 | +
|
| 3 | +Provides plotting functions to visualize cascade propagation across networks, |
| 4 | +temporal evolution of cascades, and collective emergence patterns. |
| 5 | +
|
| 6 | +TNFR Canonical Principle |
| 7 | +------------------------- |
| 8 | +From "El pulso que nos atraviesa" (TNFR Manual, §2.2.10): |
| 9 | +
|
| 10 | + "THOL actúa como modulador central de plasticidad. Es el glifo que |
| 11 | + permite a la red reorganizar su topología sin intervención externa." |
| 12 | +
|
| 13 | +These visualizations make cascade dynamics observable and traceable, |
| 14 | +enabling scientific validation and debugging of self-organization. |
| 15 | +""" |
| 16 | + |
| 17 | +from __future__ import annotations |
| 18 | + |
| 19 | +from typing import TYPE_CHECKING, Any |
| 20 | + |
| 21 | +if TYPE_CHECKING: |
| 22 | + from ..types import TNFRGraph |
| 23 | + |
| 24 | +import matplotlib.pyplot as plt |
| 25 | + |
| 26 | +from ..alias import get_attr |
| 27 | +from ..constants.aliases import ALIAS_EPI |
| 28 | + |
| 29 | +try: |
| 30 | + import networkx as nx |
| 31 | + HAS_NETWORKX = True |
| 32 | +except ImportError: |
| 33 | + HAS_NETWORKX = False |
| 34 | + |
| 35 | +__all__ = [ |
| 36 | + "plot_cascade_propagation", |
| 37 | + "plot_cascade_timeline", |
| 38 | +] |
| 39 | + |
| 40 | + |
| 41 | +def plot_cascade_propagation(G: TNFRGraph, figsize: tuple[int, int] = (12, 8)): |
| 42 | + """Visualize THOL cascade propagation across network. |
| 43 | +
|
| 44 | + Creates network diagram with: |
| 45 | + - Node size = EPI magnitude |
| 46 | + - Node color = bifurcation occurred (red) or not (blue) |
| 47 | + - Edge thickness = coupling strength |
| 48 | + - Arrows = propagation direction |
| 49 | +
|
| 50 | + Parameters |
| 51 | + ---------- |
| 52 | + G : TNFRGraph |
| 53 | + Graph with THOL propagation history |
| 54 | + figsize : tuple[int, int], default (12, 8) |
| 55 | + Figure size in inches (width, height) |
| 56 | +
|
| 57 | + Returns |
| 58 | + ------- |
| 59 | + matplotlib.figure.Figure |
| 60 | + Figure object containing the cascade visualization |
| 61 | +
|
| 62 | + Notes |
| 63 | + ----- |
| 64 | + TNFR Principle: Cascade propagation shows how self-organization |
| 65 | + spreads through phase-aligned neighbors. Red nodes = bifurcation source, |
| 66 | + blue nodes = unaffected. Arrow thickness = propagation strength. |
| 67 | +
|
| 68 | + Examples |
| 69 | + -------- |
| 70 | + >>> # After running THOL sequence with cascades |
| 71 | + >>> fig = plot_cascade_propagation(G) |
| 72 | + >>> fig.savefig("cascade_propagation.png") |
| 73 | + >>> plt.show() |
| 74 | + """ |
| 75 | + if not HAS_NETWORKX: |
| 76 | + raise ImportError("NetworkX required for cascade visualization") |
| 77 | + |
| 78 | + propagations = G.graph.get("thol_propagations", []) |
| 79 | + |
| 80 | + fig, ax = plt.subplots(figsize=figsize) |
| 81 | + |
| 82 | + # Identify nodes that bifurcated (source nodes in propagations) |
| 83 | + bifurcated_nodes = set() |
| 84 | + for prop in propagations: |
| 85 | + bifurcated_nodes.add(prop["source_node"]) |
| 86 | + |
| 87 | + # Node colors: red = bifurcated, lightblue = normal |
| 88 | + node_colors = [ |
| 89 | + "red" if n in bifurcated_nodes else "lightblue" |
| 90 | + for n in G.nodes |
| 91 | + ] |
| 92 | + |
| 93 | + # Node sizes based on EPI magnitude |
| 94 | + node_sizes = [] |
| 95 | + for n in G.nodes: |
| 96 | + epi = float(get_attr(G.nodes[n], ALIAS_EPI, 0.5)) |
| 97 | + node_sizes.append(1000 * epi) |
| 98 | + |
| 99 | + # Compute layout |
| 100 | + pos = nx.spring_layout(G, seed=42) |
| 101 | + |
| 102 | + # Draw network structure |
| 103 | + nx.draw_networkx_nodes( |
| 104 | + G, pos, node_color=node_colors, node_size=node_sizes, ax=ax, alpha=0.8 |
| 105 | + ) |
| 106 | + nx.draw_networkx_edges(G, pos, alpha=0.3, ax=ax) |
| 107 | + nx.draw_networkx_labels(G, pos, ax=ax, font_size=10) |
| 108 | + |
| 109 | + # Draw propagation arrows |
| 110 | + for prop in propagations: |
| 111 | + source = prop["source_node"] |
| 112 | + for target, strength in prop["propagations"]: |
| 113 | + if source in pos and target in pos: |
| 114 | + ax.annotate( |
| 115 | + "", |
| 116 | + xy=pos[target], |
| 117 | + xytext=pos[source], |
| 118 | + arrowprops=dict( |
| 119 | + arrowstyle="->", |
| 120 | + color="red", |
| 121 | + lw=2 * strength, |
| 122 | + alpha=0.7, |
| 123 | + ), |
| 124 | + ) |
| 125 | + |
| 126 | + ax.set_title("THOL Cascade Propagation", fontsize=14, fontweight="bold") |
| 127 | + ax.axis("off") |
| 128 | + plt.tight_layout() |
| 129 | + return fig |
| 130 | + |
| 131 | + |
| 132 | +def plot_cascade_timeline(G: TNFRGraph, figsize: tuple[int, int] = (10, 5)): |
| 133 | + """Plot temporal evolution of cascade events. |
| 134 | +
|
| 135 | + Creates scatter plot showing: |
| 136 | + - X-axis: Timestamp (operator sequence step) |
| 137 | + - Y-axis: Number of propagation targets |
| 138 | + - Size: Indicates cascade magnitude |
| 139 | +
|
| 140 | + Parameters |
| 141 | + ---------- |
| 142 | + G : TNFRGraph |
| 143 | + Graph with THOL propagation history |
| 144 | + figsize : tuple[int, int], default (10, 5) |
| 145 | + Figure size in inches (width, height) |
| 146 | +
|
| 147 | + Returns |
| 148 | + ------- |
| 149 | + matplotlib.figure.Figure or None |
| 150 | + Figure object containing the timeline, or None if no cascades |
| 151 | +
|
| 152 | + Notes |
| 153 | + ----- |
| 154 | + TNFR Principle: Temporal evolution reveals cascade patterns. |
| 155 | + Spikes indicate strong propagation events; clusters indicate |
| 156 | + sustained collective reorganization. |
| 157 | +
|
| 158 | + Examples |
| 159 | + -------- |
| 160 | + >>> # After running THOL sequence with cascades |
| 161 | + >>> fig = plot_cascade_timeline(G) |
| 162 | + >>> if fig: |
| 163 | + ... fig.savefig("cascade_timeline.png") |
| 164 | + ... plt.show() |
| 165 | + """ |
| 166 | + propagations = G.graph.get("thol_propagations", []) |
| 167 | + |
| 168 | + if not propagations: |
| 169 | + print("No cascade events to plot") |
| 170 | + return None |
| 171 | + |
| 172 | + timestamps = [p["timestamp"] for p in propagations] |
| 173 | + cascade_sizes = [len(p["propagations"]) for p in propagations] |
| 174 | + |
| 175 | + fig, ax = plt.subplots(figsize=figsize) |
| 176 | + ax.scatter(timestamps, cascade_sizes, s=100, alpha=0.7, color="darkred") |
| 177 | + ax.plot(timestamps, cascade_sizes, linestyle="--", alpha=0.5, color="gray") |
| 178 | + |
| 179 | + ax.set_xlabel("Timestamp (operator sequence step)", fontsize=12) |
| 180 | + ax.set_ylabel("Propagation Targets", fontsize=12) |
| 181 | + ax.set_title("THOL Cascade Evolution", fontsize=14, fontweight="bold") |
| 182 | + ax.grid(alpha=0.3) |
| 183 | + |
| 184 | + plt.tight_layout() |
| 185 | + return fig |
| 186 | + |
| 187 | + |
| 188 | +def plot_cascade_metrics_summary( |
| 189 | + G: TNFRGraph, |
| 190 | + node_metrics: dict[Any, dict[str, Any]], |
| 191 | + figsize: tuple[int, int] = (14, 6), |
| 192 | +): |
| 193 | + """Create comprehensive cascade metrics dashboard. |
| 194 | +
|
| 195 | + Creates multi-panel visualization showing: |
| 196 | + - Panel 1: Cascade depth distribution |
| 197 | + - Panel 2: Sub-EPI coherence over time |
| 198 | + - Panel 3: Metabolic activity index |
| 199 | +
|
| 200 | + Parameters |
| 201 | + ---------- |
| 202 | + G : TNFRGraph |
| 203 | + Graph with THOL history |
| 204 | + node_metrics : dict |
| 205 | + Dictionary mapping node IDs to their THOL metrics |
| 206 | + figsize : tuple[int, int], default (14, 6) |
| 207 | + Figure size in inches (width, height) |
| 208 | +
|
| 209 | + Returns |
| 210 | + ------- |
| 211 | + matplotlib.figure.Figure |
| 212 | + Figure object containing the dashboard |
| 213 | +
|
| 214 | + Notes |
| 215 | + ----- |
| 216 | + TNFR Principle: Complete observability requires multiple metrics. |
| 217 | + This dashboard provides holistic view of self-organization dynamics. |
| 218 | +
|
| 219 | + Examples |
| 220 | + -------- |
| 221 | + >>> # Collect metrics during sequence |
| 222 | + >>> metrics_by_node = {} |
| 223 | + >>> for node in G.nodes: |
| 224 | + ... metrics_by_node[node] = self_organization_metrics(G, node, ...) |
| 225 | + >>> fig = plot_cascade_metrics_summary(G, metrics_by_node) |
| 226 | + >>> fig.savefig("cascade_metrics_dashboard.png") |
| 227 | + """ |
| 228 | + fig, axes = plt.subplots(1, 3, figsize=figsize) |
| 229 | + |
| 230 | + # Panel 1: Cascade depth distribution |
| 231 | + depths = [m.get("cascade_depth", 0) for m in node_metrics.values()] |
| 232 | + axes[0].hist(depths, bins=range(max(depths) + 2), alpha=0.7, color="steelblue") |
| 233 | + axes[0].set_xlabel("Cascade Depth", fontsize=11) |
| 234 | + axes[0].set_ylabel("Count", fontsize=11) |
| 235 | + axes[0].set_title("Cascade Depth Distribution", fontsize=12, fontweight="bold") |
| 236 | + axes[0].grid(alpha=0.3) |
| 237 | + |
| 238 | + # Panel 2: Sub-EPI coherence |
| 239 | + coherences = [m.get("subepi_coherence", 0) for m in node_metrics.values()] |
| 240 | + node_ids = list(node_metrics.keys()) |
| 241 | + axes[1].bar(range(len(node_ids)), coherences, alpha=0.7, color="forestgreen") |
| 242 | + axes[1].set_xlabel("Node Index", fontsize=11) |
| 243 | + axes[1].set_ylabel("Coherence [0,1]", fontsize=11) |
| 244 | + axes[1].set_title("Sub-EPI Collective Coherence", fontsize=12, fontweight="bold") |
| 245 | + axes[1].axhline(0.5, color="red", linestyle="--", alpha=0.5, label="Threshold") |
| 246 | + axes[1].legend() |
| 247 | + axes[1].grid(alpha=0.3) |
| 248 | + |
| 249 | + # Panel 3: Metabolic activity index |
| 250 | + activities = [m.get("metabolic_activity_index", 0) for m in node_metrics.values()] |
| 251 | + axes[2].bar(range(len(node_ids)), activities, alpha=0.7, color="darkorange") |
| 252 | + axes[2].set_xlabel("Node Index", fontsize=11) |
| 253 | + axes[2].set_ylabel("Activity [0,1]", fontsize=11) |
| 254 | + axes[2].set_title("Metabolic Activity Index", fontsize=12, fontweight="bold") |
| 255 | + axes[2].grid(alpha=0.3) |
| 256 | + |
| 257 | + plt.tight_layout() |
| 258 | + return fig |
0 commit comments