Skip to content

Commit 2d86579

Browse files
authored
Merge pull request #2814 from fermga/copilot/enrich-telemetry-metrics
Enhance THOL telemetry with cascade depth, propagation radius, and collective coherence metrics
2 parents 40ca0c4 + 9a2648b commit 2d86579

File tree

5 files changed

+987
-82
lines changed

5 files changed

+987
-82
lines changed

src/tnfr/operators/metabolism.py

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,17 @@
3939
from ..types import NodeId, TNFRGraph
4040

4141
from ..alias import get_attr
42-
from ..constants.aliases import ALIAS_DNFR, ALIAS_EPI, ALIAS_THETA, ALIAS_VF
42+
from ..constants.aliases import ALIAS_EPI, ALIAS_THETA
4343
from ..utils import get_numpy
4444

4545
__all__ = [
4646
"capture_network_signals",
4747
"metabolize_signals_into_subepi",
4848
"propagate_subepi_to_network",
49+
"compute_cascade_depth",
50+
"compute_propagation_radius",
51+
"compute_subepi_collective_coherence",
52+
"compute_metabolic_activity_index",
4953
]
5054

5155

@@ -329,3 +333,185 @@ def propagate_subepi_to_network(
329333
G.nodes[neighbor]["epi_history"] = history[-10:] # Keep last 10
330334

331335
return propagations
336+
337+
338+
def compute_cascade_depth(G: TNFRGraph, node: NodeId) -> int:
339+
"""Compute maximum hierarchical depth of bifurcation cascade.
340+
341+
Recursively measures how many levels of nested sub-EPIs exist,
342+
where each sub-EPI can itself bifurcate into deeper levels.
343+
344+
Parameters
345+
----------
346+
G : TNFRGraph
347+
Graph containing bifurcation history
348+
node : NodeId
349+
Root node of cascade analysis
350+
351+
Returns
352+
-------
353+
int
354+
Maximum cascade depth (0 if no bifurcation occurred)
355+
356+
Examples
357+
--------
358+
>>> # Single-level bifurcation
359+
>>> compute_cascade_depth(G, node)
360+
1
361+
362+
>>> # Multi-level cascade (sub-EPIs bifurcated further)
363+
>>> compute_cascade_depth(G_complex, node)
364+
3
365+
366+
Notes
367+
-----
368+
TNFR Principle: Cascade depth measures the hierarchical complexity
369+
of emergent self-organization. Depth = 1 indicates direct bifurcation;
370+
depth > 1 indicates recursive, multi-scale emergence.
371+
"""
372+
sub_epis = G.nodes[node].get("sub_epis", [])
373+
if not sub_epis:
374+
return 0
375+
376+
# For now: depth = 1 (direct children)
377+
# TODO: If sub-EPIs become independent nodes, recurse
378+
max_depth = 1
379+
380+
for sub in sub_epis:
381+
# If sub-EPI spawned its own children (future enhancement)
382+
nested_depth = sub.get("cascade_depth", 0)
383+
max_depth = max(max_depth, 1 + nested_depth)
384+
385+
return max_depth
386+
387+
388+
def compute_propagation_radius(G: TNFRGraph) -> int:
389+
"""Count total unique nodes affected by THOL cascades.
390+
391+
Parameters
392+
----------
393+
G : TNFRGraph
394+
Graph with THOL propagation history
395+
396+
Returns
397+
-------
398+
int
399+
Number of nodes reached by at least one propagation event
400+
401+
Notes
402+
-----
403+
TNFR Principle: Propagation radius measures the spatial extent
404+
of cascade effects across the network. High radius indicates
405+
network-wide self-organization.
406+
407+
Examples
408+
--------
409+
>>> # Local cascade (few nodes)
410+
>>> compute_propagation_radius(G_local)
411+
3
412+
413+
>>> # Network-wide cascade
414+
>>> compute_propagation_radius(G_wide)
415+
15
416+
"""
417+
propagations = G.graph.get("thol_propagations", [])
418+
affected_nodes = set()
419+
420+
for prop in propagations:
421+
affected_nodes.add(prop["source_node"])
422+
for target, _ in prop["propagations"]:
423+
affected_nodes.add(target)
424+
425+
return len(affected_nodes)
426+
427+
428+
def compute_subepi_collective_coherence(G: TNFRGraph, node: NodeId) -> float:
429+
"""Calculate coherence of sub-EPI ensemble.
430+
431+
Measures how structurally aligned the emergent sub-EPIs are.
432+
Low variance = high coherence = stable emergence.
433+
434+
Parameters
435+
----------
436+
G : TNFRGraph
437+
Graph containing the node
438+
node : NodeId
439+
Node with sub-EPIs to analyze
440+
441+
Returns
442+
-------
443+
float
444+
Coherence metric [0, 1] where 1 = perfect alignment
445+
446+
Notes
447+
-----
448+
Uses variance-based coherence:
449+
C_sub = 1 / (1 + var(sub_epi_magnitudes))
450+
451+
TNFR Principle: Coherent bifurcation produces sub-EPIs with similar
452+
structural magnitudes, indicating controlled emergence vs chaotic
453+
fragmentation.
454+
455+
Examples
456+
--------
457+
>>> # Coherent bifurcation (similar sub-EPIs)
458+
>>> compute_subepi_collective_coherence(G, node)
459+
0.85
460+
461+
>>> # Chaotic fragmentation (varied sub-EPIs)
462+
>>> compute_subepi_collective_coherence(G_chaotic, node)
463+
0.23
464+
"""
465+
np = get_numpy()
466+
467+
sub_epis = G.nodes[node].get("sub_epis", [])
468+
if len(sub_epis) < 2:
469+
return 0.0 # Need ≥2 sub-EPIs to measure coherence
470+
471+
epi_values = [sub["epi"] for sub in sub_epis]
472+
variance = float(np.var(epi_values))
473+
474+
# Coherence: inverse relationship with variance
475+
coherence = 1.0 / (1.0 + variance)
476+
return coherence
477+
478+
479+
def compute_metabolic_activity_index(G: TNFRGraph, node: NodeId) -> float:
480+
"""Measure proportion of sub-EPIs generated through network metabolism.
481+
482+
Parameters
483+
----------
484+
G : TNFRGraph
485+
Graph containing the node
486+
node : NodeId
487+
Node to analyze
488+
489+
Returns
490+
-------
491+
float
492+
Ratio [0, 1] of metabolized sub-EPIs to total sub-EPIs
493+
1.0 = all sub-EPIs included network context
494+
0.0 = all sub-EPIs were purely internal bifurcations
495+
496+
Notes
497+
-----
498+
TNFR Principle: Metabolic activity measures how much network context
499+
influenced bifurcation. High index indicates external pressure drove
500+
emergence; low index indicates internal acceleration dominated.
501+
502+
Examples
503+
--------
504+
>>> # Network-driven bifurcation
505+
>>> compute_metabolic_activity_index(G_coupled, node)
506+
0.90
507+
508+
>>> # Internal-only bifurcation
509+
>>> compute_metabolic_activity_index(G_isolated, node)
510+
0.0
511+
"""
512+
sub_epis = G.nodes[node].get("sub_epis", [])
513+
if not sub_epis:
514+
return 0.0
515+
516+
metabolized_count = sum(1 for sub in sub_epis if sub.get("metabolized", False))
517+
return metabolized_count / len(sub_epis)

0 commit comments

Comments
 (0)