Skip to content

Commit 4edc061

Browse files
Copilotfermga
andcommitted
Update plan - use existing TNFR cache system for cascade detection
Co-authored-by: fermga <203334638+fermga@users.noreply.github.com>
1 parent 81afed2 commit 4edc061

File tree

1 file changed

+315
-0
lines changed

1 file changed

+315
-0
lines changed
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
"""Performance benchmarks for cascade detection in large networks.
2+
3+
This module tests the scalability of detect_cascade() for networks
4+
with >1000 nodes, targeting <100ms detection time for 10k node networks.
5+
6+
References:
7+
- Issue: [THOL][Performance] Optimizar detect_cascade() para escalabilidad
8+
- Module: src/tnfr/operators/cascade.py
9+
"""
10+
11+
import time
12+
13+
import networkx as nx
14+
import pytest
15+
16+
from tnfr.operators.cascade import detect_cascade
17+
from tnfr.structural import create_nfr
18+
19+
20+
def create_test_network_with_cascade(n_nodes, cascade_length=None):
21+
"""Create test network with simulated THOL cascade propagations.
22+
23+
Parameters
24+
----------
25+
n_nodes : int
26+
Number of nodes in the network
27+
cascade_length : int, optional
28+
Number of propagation events to simulate.
29+
Defaults to n_nodes // 10 for realistic cascade density.
30+
31+
Returns
32+
-------
33+
TNFRGraph
34+
Network with thol_propagations populated
35+
"""
36+
if cascade_length is None:
37+
cascade_length = max(10, n_nodes // 10)
38+
39+
# Create base network
40+
G = nx.Graph()
41+
42+
# Add nodes with TNFR attributes
43+
for i in range(n_nodes):
44+
G.add_node(i, epi=0.50, vf=1.0, theta=0.1 + i * 0.001)
45+
46+
# Add edges for coupling
47+
# Create small-world topology for realistic network
48+
if n_nodes < 100:
49+
# Complete graph for small networks
50+
for i in range(n_nodes):
51+
for j in range(i + 1, n_nodes):
52+
G.add_edge(i, j)
53+
else:
54+
# Watts-Strogatz small-world for large networks
55+
# k=6 means each node connects to 6 neighbors
56+
k = min(6, n_nodes - 1)
57+
G = nx.watts_strogatz_graph(n=n_nodes, k=k, p=0.1)
58+
59+
# Add TNFR attributes to regenerated graph
60+
for i in range(n_nodes):
61+
G.nodes[i]["epi"] = 0.50
62+
G.nodes[i]["vf"] = 1.0
63+
G.nodes[i]["theta"] = 0.1 + i * 0.001
64+
65+
# Simulate cascade propagations
66+
# Each propagation event affects 2-5 neighbors
67+
propagations = []
68+
for event_idx in range(cascade_length):
69+
source_node = event_idx % n_nodes
70+
71+
# Get neighbors for this source
72+
neighbors = list(G.neighbors(source_node))
73+
if not neighbors:
74+
continue
75+
76+
# Propagate to 2-5 neighbors (or all if fewer exist)
77+
import random
78+
random.seed(42 + event_idx) # Deterministic for reproducibility
79+
n_targets = min(random.randint(2, 5), len(neighbors))
80+
targets = random.sample(neighbors, n_targets)
81+
82+
# Create propagation record
83+
propagations.append({
84+
"source_node": source_node,
85+
"propagations": [(t, 0.10) for t in targets],
86+
"timestamp": 10 + event_idx,
87+
})
88+
89+
G.graph["thol_propagations"] = propagations
90+
G.graph["THOL_CASCADE_MIN_NODES"] = 3
91+
92+
return G
93+
94+
95+
class TestCascadeDetectionScaling:
96+
"""Test cascade detection performance vs network size."""
97+
98+
@pytest.mark.parametrize("n_nodes", [100, 500, 1000, 5000])
99+
def test_cascade_detection_time(self, n_nodes, benchmark):
100+
"""Measure cascade detection time for various network sizes.
101+
102+
Target: <100ms for 10,000 nodes (tested separately due to time).
103+
Expected scaling: Should be sub-linear with incremental cache.
104+
"""
105+
G = create_test_network_with_cascade(n_nodes)
106+
107+
# Benchmark the detection
108+
result = benchmark(detect_cascade, G)
109+
110+
# Verify correctness
111+
assert "is_cascade" in result
112+
assert "affected_nodes" in result
113+
assert "cascade_depth" in result
114+
assert "total_propagations" in result
115+
116+
# Performance assertion (generous bounds for CI variability)
117+
# After optimization, these should be much faster
118+
if n_nodes <= 1000:
119+
# Small networks should be fast even without optimization
120+
assert benchmark.stats.median < 0.1, (
121+
f"Detection too slow for n={n_nodes}: {benchmark.stats.median:.3f}s"
122+
)
123+
elif n_nodes <= 5000:
124+
# Mid-size networks: current implementation may struggle
125+
# After optimization, should be <50ms
126+
assert benchmark.stats.median < 0.5, (
127+
f"Detection too slow for n={n_nodes}: {benchmark.stats.median:.3f}s"
128+
)
129+
130+
def test_cascade_detection_10k_nodes(self, benchmark):
131+
"""Test detection on large 10k node network.
132+
133+
This is the target scenario from the issue.
134+
Current implementation may be slow; after optimization should be <100ms.
135+
"""
136+
n_nodes = 10000
137+
G = create_test_network_with_cascade(n_nodes)
138+
139+
result = benchmark(detect_cascade, G)
140+
141+
# Verify correctness
142+
assert result["is_cascade"] is True
143+
assert len(result["affected_nodes"]) >= 3
144+
145+
# Performance target from issue: <100ms = 0.1s
146+
# Allow 1s for now (will improve with optimization)
147+
assert benchmark.stats.median < 1.0, (
148+
f"Detection too slow for 10k nodes: {benchmark.stats.median:.3f}s"
149+
)
150+
151+
def test_multiple_detections_same_network(self):
152+
"""Test repeated detections on same network (cache benefit).
153+
154+
With incremental cache, subsequent detections should be O(1).
155+
"""
156+
n_nodes = 5000
157+
G = create_test_network_with_cascade(n_nodes)
158+
159+
# First detection (may build cache)
160+
start = time.time()
161+
result1 = detect_cascade(G)
162+
first_time = time.time() - start
163+
164+
# Second detection (should use cache)
165+
start = time.time()
166+
result2 = detect_cascade(G)
167+
second_time = time.time() - start
168+
169+
# Results should be identical
170+
assert result1["is_cascade"] == result2["is_cascade"]
171+
assert len(result1["affected_nodes"]) == len(result2["affected_nodes"])
172+
173+
# Note: Without cache, times will be similar
174+
# With cache, second should be much faster
175+
print(f"First: {first_time:.3f}s, Second: {second_time:.3f}s")
176+
177+
178+
class TestCascadeDetectionCorrectness:
179+
"""Test that optimization preserves correctness."""
180+
181+
def test_no_cascade_empty_propagations(self):
182+
"""Empty propagations should report no cascade."""
183+
G = nx.Graph()
184+
G.add_node(0, epi=0.50, vf=1.0, theta=0.1)
185+
G.graph["thol_propagations"] = []
186+
187+
result = detect_cascade(G)
188+
189+
assert result["is_cascade"] is False
190+
assert len(result["affected_nodes"]) == 0
191+
assert result["cascade_depth"] == 0
192+
assert result["total_propagations"] == 0
193+
194+
def test_small_cascade_below_threshold(self):
195+
"""Cascade affecting <3 nodes should not be detected."""
196+
G = nx.Graph()
197+
G.add_node(0, epi=0.50, vf=1.0, theta=0.1)
198+
G.add_node(1, epi=0.50, vf=1.0, theta=0.1)
199+
G.add_edge(0, 1)
200+
201+
# Propagation affecting only 2 nodes
202+
G.graph["thol_propagations"] = [
203+
{
204+
"source_node": 0,
205+
"propagations": [(1, 0.10)],
206+
"timestamp": 10,
207+
}
208+
]
209+
G.graph["THOL_CASCADE_MIN_NODES"] = 3
210+
211+
result = detect_cascade(G)
212+
213+
assert result["is_cascade"] is False
214+
assert len(result["affected_nodes"]) == 2 # Source + target
215+
216+
def test_cascade_above_threshold(self):
217+
"""Cascade affecting ≥3 nodes should be detected."""
218+
G = create_test_network_with_cascade(n_nodes=10, cascade_length=5)
219+
220+
result = detect_cascade(G)
221+
222+
# With 5 propagation events in 10-node network, should reach threshold
223+
assert result["is_cascade"] is True
224+
assert len(result["affected_nodes"]) >= 3
225+
assert result["cascade_depth"] > 0
226+
assert result["total_propagations"] > 0
227+
228+
def test_affected_nodes_counted_once(self):
229+
"""Each node should be counted only once even if affected multiple times."""
230+
G = nx.Graph()
231+
for i in range(5):
232+
G.add_node(i, epi=0.50, vf=1.0, theta=0.1)
233+
234+
# Multiple propagations to same nodes
235+
G.graph["thol_propagations"] = [
236+
{
237+
"source_node": 0,
238+
"propagations": [(1, 0.10), (2, 0.09)],
239+
"timestamp": 10,
240+
},
241+
{
242+
"source_node": 1,
243+
"propagations": [(2, 0.08), (3, 0.07)], # Node 2 affected again
244+
"timestamp": 11,
245+
},
246+
]
247+
G.graph["THOL_CASCADE_MIN_NODES"] = 3
248+
249+
result = detect_cascade(G)
250+
251+
# Should count nodes 0, 1, 2, 3 = 4 unique nodes
252+
assert len(result["affected_nodes"]) == 4
253+
assert result["affected_nodes"] == {0, 1, 2, 3}
254+
255+
256+
class TestCascadeDetectionEdgeCases:
257+
"""Test edge cases and boundary conditions."""
258+
259+
def test_single_node_isolated(self):
260+
"""Isolated node should report no cascade."""
261+
G = nx.Graph()
262+
G.add_node(0, epi=0.50, vf=1.0, theta=0.1)
263+
G.graph["thol_propagations"] = []
264+
265+
result = detect_cascade(G)
266+
267+
assert result["is_cascade"] is False
268+
269+
def test_very_large_cascade(self):
270+
"""Very large cascade should be handled correctly."""
271+
n_nodes = 1000
272+
# Dense cascade: propagations = n_nodes (one per node)
273+
G = create_test_network_with_cascade(n_nodes, cascade_length=n_nodes)
274+
275+
result = detect_cascade(G)
276+
277+
assert result["is_cascade"] is True
278+
# Should affect significant portion of network
279+
assert len(result["affected_nodes"]) > n_nodes // 2
280+
281+
def test_custom_cascade_threshold(self):
282+
"""Custom cascade threshold should be respected."""
283+
G = create_test_network_with_cascade(n_nodes=20, cascade_length=3)
284+
285+
# Set high threshold
286+
G.graph["THOL_CASCADE_MIN_NODES"] = 100
287+
288+
result = detect_cascade(G)
289+
290+
# Should not detect cascade with high threshold
291+
assert result["is_cascade"] is False
292+
293+
# Lower threshold
294+
G.graph["THOL_CASCADE_MIN_NODES"] = 5
295+
result = detect_cascade(G)
296+
297+
# Should detect with lower threshold
298+
assert result["is_cascade"] is True
299+
300+
301+
if __name__ == "__main__":
302+
# Quick manual test
303+
print("Testing cascade detection performance...\n")
304+
305+
for n in [100, 500, 1000, 5000, 10000]:
306+
G = create_test_network_with_cascade(n)
307+
308+
start = time.time()
309+
result = detect_cascade(G)
310+
elapsed = time.time() - start
311+
312+
print(f"n={n:5d}: {elapsed:.3f}s - "
313+
f"cascade={result['is_cascade']}, "
314+
f"affected={len(result['affected_nodes']):4d}, "
315+
f"depth={result['cascade_depth']:3d}")

0 commit comments

Comments
 (0)