Skip to content

Commit 8081ca1

Browse files
Copilotfermga
andcommitted
Add cascade visualization module and fix backward compatibility
Co-authored-by: fermga <203334638+fermga@users.noreply.github.com>
1 parent dff9c38 commit 8081ca1

File tree

3 files changed

+282
-1
lines changed

3 files changed

+282
-1
lines changed

src/tnfr/operators/metrics.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1054,8 +1054,11 @@ def self_organization_metrics(
10541054
d2epi = _get_node_attr(G, node, ALIAS_D2EPI)
10551055
dnfr = _get_node_attr(G, node, ALIAS_DNFR)
10561056

1057-
# Track nested EPI count from node attribute
1057+
# Track nested EPI count from node attribute or graph (backward compatibility)
10581058
nested_epi_count = len(G.nodes[node].get("sub_epis", []))
1059+
if nested_epi_count == 0:
1060+
# Fallback to old location for backward compatibility
1061+
nested_epi_count = len(G.graph.get("sub_epi", []))
10591062

10601063
# Cascade and propagation analysis
10611064
cascade_analysis = detect_cascade(G)

src/tnfr/visualization/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Health metrics dashboards with radar charts and gauges
66
- Pattern analysis with component highlighting
77
- Frequency timelines showing structural evolution
8+
- Cascade propagation and temporal dynamics (NEW)
89
910
Requires matplotlib for plotting. Install with::
1011
@@ -21,15 +22,28 @@
2122
>>> visualizer = SequenceVisualizer()
2223
>>> fig, ax = visualizer.plot_sequence_flow(sequence, result.health_metrics)
2324
>>> fig.savefig("sequence_flow.png")
25+
26+
>>> # Cascade visualization (NEW)
27+
>>> from tnfr.visualization import plot_cascade_propagation, plot_cascade_timeline
28+
>>> fig = plot_cascade_propagation(G)
29+
>>> fig.savefig("cascade_propagation.png")
2430
"""
2531

2632
_import_error: ImportError | None = None
2733

2834
try:
2935
from .sequence_plotter import SequenceVisualizer
36+
from .cascade_viz import (
37+
plot_cascade_propagation,
38+
plot_cascade_timeline,
39+
plot_cascade_metrics_summary,
40+
)
3041

3142
__all__ = [
3243
"SequenceVisualizer",
44+
"plot_cascade_propagation",
45+
"plot_cascade_timeline",
46+
"plot_cascade_metrics_summary",
3347
]
3448
except ImportError as _import_err:
3549
_import_error = _import_err
@@ -55,7 +69,13 @@ def _missing_viz_dependency(*args: _Any, **kwargs: _Any) -> None:
5569
) from _import_error
5670

5771
SequenceVisualizer = _missing_viz_dependency # type: ignore[assignment]
72+
plot_cascade_propagation = _missing_viz_dependency # type: ignore[assignment]
73+
plot_cascade_timeline = _missing_viz_dependency # type: ignore[assignment]
74+
plot_cascade_metrics_summary = _missing_viz_dependency # type: ignore[assignment]
5875

5976
__all__ = [
6077
"SequenceVisualizer",
78+
"plot_cascade_propagation",
79+
"plot_cascade_timeline",
80+
"plot_cascade_metrics_summary",
6181
]
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
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

Comments
 (0)