Skip to content

Commit 38a72eb

Browse files
authored
Merge pull request #2796 from fermga/copilot/expand-coupling-metrics
Expand coupling_metrics with canonical TNFR measurements
2 parents f0f84e6 + 5994ad9 commit 38a72eb

File tree

3 files changed

+433
-5
lines changed

3 files changed

+433
-5
lines changed

src/tnfr/operators/definitions.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1453,13 +1453,30 @@ def _validate_preconditions(self, G: TNFRGraph, node: Any) -> None:
14531453

14541454
validate_coupling(G, node)
14551455

1456+
def _capture_state(self, G: TNFRGraph, node: Any) -> dict[str, Any]:
1457+
"""Capture node state before operator application, including edge count."""
1458+
# Get base state (epi, vf, dnfr, theta)
1459+
state = super()._capture_state(G, node)
1460+
1461+
# Add edge count for coupling-specific metrics
1462+
state["edges"] = G.degree(node)
1463+
1464+
return state
1465+
14561466
def _collect_metrics(
14571467
self, G: TNFRGraph, node: Any, state_before: dict[str, Any]
14581468
) -> dict[str, Any]:
1459-
"""Collect UM-specific metrics."""
1469+
"""Collect UM-specific metrics with expanded canonical measurements."""
14601470
from .metrics import coupling_metrics
14611471

1462-
return coupling_metrics(G, node, state_before["theta"], state_before["dnfr"])
1472+
return coupling_metrics(
1473+
G,
1474+
node,
1475+
state_before["theta"],
1476+
dnfr_before=state_before["dnfr"],
1477+
vf_before=state_before["vf"],
1478+
edges_before=state_before.get("edges", None),
1479+
)
14631480

14641481

14651482
@register_operator

src/tnfr/operators/metrics.py

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -552,9 +552,19 @@ def dissonance_metrics(
552552
}
553553

554554

555-
def coupling_metrics(G: TNFRGraph, node: NodeId, theta_before: float, dnfr_before: float = None) -> dict[str, Any]:
555+
def coupling_metrics(
556+
G: TNFRGraph,
557+
node: NodeId,
558+
theta_before: float,
559+
dnfr_before: float = None,
560+
vf_before: float = None,
561+
edges_before: int = None,
562+
) -> dict[str, Any]:
556563
"""UM - Coupling metrics: phase alignment, link formation, synchrony, ΔNFR reduction.
557564
565+
Extended metrics for Coupling (UM) operator that track structural changes,
566+
network formation, and synchronization effectiveness.
567+
558568
Parameters
559569
----------
560570
G : TNFRGraph
@@ -565,16 +575,64 @@ def coupling_metrics(G: TNFRGraph, node: NodeId, theta_before: float, dnfr_befor
565575
Phase value before operator application
566576
dnfr_before : float, optional
567577
ΔNFR value before operator application (for reduction tracking)
578+
vf_before : float, optional
579+
Structural frequency (νf) before operator application
580+
edges_before : int, optional
581+
Number of edges before operator application
568582
569583
Returns
570584
-------
571585
dict
572-
Coupling-specific metrics including phase synchronization and ΔNFR reduction
586+
Coupling-specific metrics including:
587+
588+
**Phase metrics:**
589+
590+
- theta_shift: Absolute phase change
591+
- theta_final: Post-coupling phase
592+
- mean_neighbor_phase: Average phase of neighbors
593+
- phase_alignment: Alignment with neighbors [0,1]
594+
- phase_dispersion: Standard deviation of phases in local cluster
595+
- is_synchronized: Boolean indicating strong synchronization (alignment > 0.8)
596+
597+
**Frequency metrics:**
598+
599+
- delta_vf: Change in structural frequency (νf)
600+
- vf_final: Post-coupling structural frequency
601+
602+
**Reorganization metrics:**
603+
604+
- delta_dnfr: Change in ΔNFR
605+
- dnfr_stabilization: Reduction of reorganization pressure (positive if stabilized)
606+
- dnfr_final: Post-coupling ΔNFR
607+
- dnfr_reduction: Absolute reduction (before - after)
608+
- dnfr_reduction_pct: Percentage reduction
609+
610+
**Network metrics:**
611+
612+
- neighbor_count: Number of neighbors after coupling
613+
- new_edges_count: Number of edges added
614+
- total_edges: Total edges after coupling
615+
- coupling_strength_total: Sum of coupling weights on edges
616+
- local_coherence: Kuramoto order parameter of local subgraph
617+
618+
Notes
619+
-----
620+
The extended metrics align with TNFR canonical theory (§2.2.2) that UM creates
621+
structural links through phase synchronization (φᵢ(t) ≈ φⱼ(t)). The metrics
622+
capture both the synchronization quality and the network structural changes
623+
resulting from coupling.
624+
625+
See Also
626+
--------
627+
operators.definitions.Coupling : UM operator implementation
628+
metrics.phase_coherence.compute_phase_alignment : Phase alignment computation
573629
"""
574630
import math
631+
import statistics
575632

576633
theta_after = _get_node_attr(G, node, ALIAS_THETA)
577634
dnfr_after = _get_node_attr(G, node, ALIAS_DNFR)
635+
vf_after = _get_node_attr(G, node, ALIAS_VF)
578636
neighbors = list(G.neighbors(node))
579637
neighbor_count = len(neighbors)
580638

@@ -587,6 +645,7 @@ def coupling_metrics(G: TNFRGraph, node: NodeId, theta_before: float, dnfr_befor
587645
mean_neighbor_phase = theta_after
588646
phase_alignment = 0.0
589647

648+
# Base metrics (always present)
590649
metrics = {
591650
"operator": "Coupling",
592651
"glyph": "UM",
@@ -597,17 +656,68 @@ def coupling_metrics(G: TNFRGraph, node: NodeId, theta_before: float, dnfr_befor
597656
"phase_alignment": max(0.0, phase_alignment),
598657
}
599658

600-
# Add ΔNFR reduction metrics if dnfr_before is provided
659+
# Structural frequency metrics (if vf_before provided)
660+
if vf_before is not None:
661+
delta_vf = vf_after - vf_before
662+
metrics.update({
663+
"delta_vf": delta_vf,
664+
"vf_final": vf_after,
665+
})
666+
667+
# ΔNFR reduction metrics (if dnfr_before provided)
601668
if dnfr_before is not None:
602669
dnfr_reduction = dnfr_before - dnfr_after
603670
dnfr_reduction_pct = (dnfr_reduction / (abs(dnfr_before) + 1e-9)) * 100.0
671+
dnfr_stabilization = dnfr_before - dnfr_after # Positive if stabilized
604672
metrics.update({
605673
"dnfr_before": dnfr_before,
606674
"dnfr_after": dnfr_after,
675+
"delta_dnfr": dnfr_after - dnfr_before,
607676
"dnfr_reduction": dnfr_reduction,
608677
"dnfr_reduction_pct": dnfr_reduction_pct,
678+
"dnfr_stabilization": dnfr_stabilization,
679+
"dnfr_final": dnfr_after,
609680
})
610681

682+
# Edge/network formation metrics (if edges_before provided)
683+
edges_after = G.degree(node)
684+
if edges_before is not None:
685+
new_edges_count = edges_after - edges_before
686+
metrics.update({
687+
"new_edges_count": new_edges_count,
688+
"total_edges": edges_after,
689+
})
690+
else:
691+
# Still provide total_edges even without edges_before
692+
metrics["total_edges"] = edges_after
693+
694+
# Coupling strength (sum of edge weights)
695+
coupling_strength_total = 0.0
696+
for neighbor in neighbors:
697+
edge_data = G.get_edge_data(node, neighbor)
698+
if edge_data and isinstance(edge_data, dict):
699+
coupling_strength_total += edge_data.get('coupling', 0.0)
700+
metrics["coupling_strength_total"] = coupling_strength_total
701+
702+
# Phase dispersion (standard deviation of local phases)
703+
if neighbor_count > 1:
704+
phases = [theta_after] + [_get_node_attr(G, n, ALIAS_THETA) for n in neighbors]
705+
phase_std = statistics.stdev(phases)
706+
metrics["phase_dispersion"] = phase_std
707+
else:
708+
metrics["phase_dispersion"] = 0.0
709+
710+
# Local coherence (Kuramoto order parameter of subgraph)
711+
if neighbor_count > 0:
712+
from ..metrics.phase_coherence import compute_phase_alignment
713+
local_coherence = compute_phase_alignment(G, node, radius=1)
714+
metrics["local_coherence"] = local_coherence
715+
else:
716+
metrics["local_coherence"] = 0.0
717+
718+
# Synchronization indicator
719+
metrics["is_synchronized"] = phase_alignment > 0.8
720+
611721
return metrics
612722

613723

0 commit comments

Comments
 (0)