Skip to content

Commit fc61b33

Browse files
authored
Merge pull request #2866 from fermga/copilot/add-density-metrics-to-contraction
Add structural density metrics to NUL (Contraction) operator
2 parents db6fa49 + 1bf320c commit fc61b33

File tree

3 files changed

+584
-2
lines changed

3 files changed

+584
-2
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""Demonstration of structural density metrics for NUL (Contraction) operator.
2+
3+
This example shows how the new density metrics enable:
4+
1. Validation of canonical NUL behavior
5+
2. Early warning for over-compression
6+
3. Analysis of density evolution
7+
4. Research workflow support
8+
"""
9+
10+
from tnfr.structural import create_nfr
11+
from tnfr.operators import apply_glyph
12+
from tnfr.types import Glyph
13+
from tnfr.constants import DNFR_PRIMARY, VF_PRIMARY, EPI_PRIMARY
14+
from tnfr.operators.metrics import contraction_metrics
15+
16+
17+
def demonstrate_density_metrics():
18+
"""Demonstrate the new structural density metrics."""
19+
print("=" * 70)
20+
print("NUL (Contraction) Operator - Structural Density Metrics")
21+
print("=" * 70)
22+
print()
23+
24+
# Example 1: Normal contraction with moderate density
25+
print("Example 1: Normal Contraction (Moderate Density)")
26+
print("-" * 70)
27+
28+
G, node = create_nfr('test_node', epi=0.5, vf=1.0)
29+
G.nodes[node][DNFR_PRIMARY] = 0.2
30+
31+
epi_before = G.nodes[node][EPI_PRIMARY]
32+
vf_before = G.nodes[node][VF_PRIMARY]
33+
dnfr_before = G.nodes[node][DNFR_PRIMARY]
34+
35+
print(f"Before contraction:")
36+
print(f" EPI: {epi_before:.4f}")
37+
print(f" νf: {vf_before:.4f}")
38+
print(f" ΔNFR: {dnfr_before:.4f}")
39+
print(f" Density (|ΔNFR|/EPI): {abs(dnfr_before) / epi_before:.4f}")
40+
print()
41+
42+
# Apply contraction
43+
apply_glyph(G, node, Glyph.NUL)
44+
45+
# Collect metrics
46+
metrics = contraction_metrics(G, node, vf_before, epi_before)
47+
48+
print(f"After contraction:")
49+
print(f" EPI: {metrics['epi_final']:.4f}")
50+
print(f" νf: {metrics['vf_final']:.4f}")
51+
print(f" ΔNFR: {metrics['dnfr_final']:.4f}")
52+
print()
53+
54+
print("New Density Metrics:")
55+
print(f" density_before: {metrics['density_before']:.4f}")
56+
print(f" density_after: {metrics['density_after']:.4f}")
57+
print(f" densification_ratio: {metrics['densification_ratio']:.4f}")
58+
print(f" is_critical_density: {metrics['is_critical_density']}")
59+
print()
60+
61+
print("✓ Canonical behavior validated:")
62+
print(f" - Density increased: {metrics['density_after'] > metrics['density_before']}")
63+
print(f" - ΔNFR densified: {metrics.get('dnfr_densified', False)}")
64+
print(f" - Safe compression: {not metrics['is_critical_density']}")
65+
print()
66+
print()
67+
68+
# Example 2: High density scenario (approaching critical threshold)
69+
print("Example 2: High Density Contraction (Warning Case)")
70+
print("-" * 70)
71+
72+
G2, node2 = create_nfr('high_density_node', epi=0.3, vf=1.0)
73+
G2.nodes[node2][DNFR_PRIMARY] = 1.5 # High ΔNFR
74+
75+
epi_before2 = G2.nodes[node2][EPI_PRIMARY]
76+
vf_before2 = G2.nodes[node2][VF_PRIMARY]
77+
dnfr_before2 = G2.nodes[node2][DNFR_PRIMARY]
78+
79+
print(f"Before contraction:")
80+
print(f" EPI: {epi_before2:.4f}")
81+
print(f" νf: {vf_before2:.4f}")
82+
print(f" ΔNFR: {dnfr_before2:.4f}")
83+
print(f" Density (|ΔNFR|/EPI): {abs(dnfr_before2) / epi_before2:.4f}")
84+
print()
85+
86+
# Apply contraction
87+
apply_glyph(G2, node2, Glyph.NUL)
88+
89+
# Collect metrics
90+
metrics2 = contraction_metrics(G2, node2, vf_before2, epi_before2)
91+
92+
print(f"After contraction:")
93+
print(f" EPI: {metrics2['epi_final']:.4f}")
94+
print(f" νf: {metrics2['vf_final']:.4f}")
95+
print(f" ΔNFR: {metrics2['dnfr_final']:.4f}")
96+
print()
97+
98+
print("New Density Metrics:")
99+
print(f" density_before: {metrics2['density_before']:.4f}")
100+
print(f" density_after: {metrics2['density_after']:.4f}")
101+
print(f" densification_ratio: {metrics2['densification_ratio']:.4f}")
102+
print(f" is_critical_density: {metrics2['is_critical_density']}")
103+
print()
104+
105+
if metrics2['is_critical_density']:
106+
print("⚠️ WARNING: Critical density exceeded!")
107+
print(f" Density {metrics2['density_after']:.2f} > threshold 5.0")
108+
print(" Risk of over-compression. Consider:")
109+
print(" - Apply IL (Coherence) to stabilize")
110+
print(" - Avoid further NUL operations")
111+
print(" - Monitor for node collapse")
112+
print()
113+
print()
114+
115+
# Example 3: Density evolution analysis
116+
print("Example 3: Density Evolution Analysis")
117+
print("-" * 70)
118+
119+
print("Tracking density across varying initial conditions:")
120+
print()
121+
print(f"{'Initial ΔNFR':<15} {'Density Before':<15} {'Density After':<15} {'Ratio':<10} {'Critical':<10}")
122+
print("-" * 70)
123+
124+
for initial_dnfr in [0.1, 0.2, 0.3, 0.4, 0.5]:
125+
G3, node3 = create_nfr('evolution_node', epi=0.5, vf=1.0)
126+
G3.nodes[node3][DNFR_PRIMARY] = initial_dnfr
127+
128+
epi_before3 = G3.nodes[node3][EPI_PRIMARY]
129+
vf_before3 = G3.nodes[node3][VF_PRIMARY]
130+
131+
apply_glyph(G3, node3, Glyph.NUL)
132+
133+
metrics3 = contraction_metrics(G3, node3, vf_before3, epi_before3)
134+
135+
critical_mark = "⚠️ YES" if metrics3['is_critical_density'] else "✓ NO"
136+
137+
print(f"{initial_dnfr:<15.2f} "
138+
f"{metrics3['density_before']:<15.4f} "
139+
f"{metrics3['density_after']:<15.4f} "
140+
f"{metrics3['densification_ratio']:<10.4f} "
141+
f"{critical_mark:<10}")
142+
143+
print()
144+
print("Observations:")
145+
print(" - Densification ratio remains relatively constant (~1.59)")
146+
print(" - This validates canonical NUL behavior")
147+
print(" - Higher initial ΔNFR leads to higher final density")
148+
print(" - Critical density warning activates appropriately")
149+
print()
150+
151+
152+
if __name__ == "__main__":
153+
demonstrate_density_metrics()

src/tnfr/operators/metrics.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,9 @@ def contraction_metrics(
12941294
) -> dict[str, Any]:
12951295
"""NUL - Contraction metrics: νf decrease, core concentration, ΔNFR densification.
12961296
1297+
Collects comprehensive contraction metrics including structural density dynamics
1298+
that validate canonical NUL behavior and enable early warning for over-compression.
1299+
12971300
Parameters
12981301
----------
12991302
G : TNFRGraph
@@ -1308,9 +1311,52 @@ def contraction_metrics(
13081311
Returns
13091312
-------
13101313
dict
1311-
Contraction-specific metrics including structural compression and
1312-
canonical ΔNFR densification tracking.
1314+
Contraction-specific metrics including:
1315+
1316+
**Basic metrics:**
1317+
1318+
- operator: "Contraction"
1319+
- glyph: "NUL"
1320+
- vf_decrease: Absolute reduction in νf
1321+
- vf_final: Post-contraction νf
1322+
- delta_epi: EPI change
1323+
- epi_final: Post-contraction EPI
1324+
- dnfr_final: Post-contraction ΔNFR
1325+
- contraction_factor: Ratio of vf_after / vf_before
1326+
1327+
**Densification metrics (if available):**
1328+
1329+
- densification_factor: ΔNFR amplification factor (typically 1.35)
1330+
- dnfr_densified: Boolean indicating densification occurred
1331+
- dnfr_before: ΔNFR value before contraction
1332+
- dnfr_increase: Absolute ΔNFR change (dnfr_after - dnfr_before)
1333+
1334+
**Structural density metrics (NEW):**
1335+
1336+
- density_before: |ΔNFR| / max(EPI, ε) before contraction
1337+
- density_after: |ΔNFR| / max(EPI, ε) after contraction
1338+
- densification_ratio: density_after / density_before
1339+
- is_critical_density: Warning flag (density > threshold)
1340+
1341+
Notes
1342+
-----
1343+
**Structural Density**: Defined as ρ = |ΔNFR| / max(EPI, ε) where ε = 1e-9.
1344+
This captures the concentration of reorganization pressure per unit structure.
1345+
1346+
**Critical Density**: When density exceeds CRITICAL_DENSITY_THRESHOLD (default: 5.0),
1347+
it indicates over-compression risk where the node may become unstable.
1348+
1349+
**Densification Ratio**: Quantifies how much density increased during contraction.
1350+
Canonical NUL should produce densification_ratio ≈ densification_factor / contraction_factor.
1351+
1352+
See Also
1353+
--------
1354+
Contraction : NUL operator implementation
1355+
validate_contraction : Preconditions for safe contraction
13131356
"""
1357+
# Small epsilon for numerical stability
1358+
EPSILON = 1e-9
1359+
13141360
vf_after = _get_node_attr(G, node, ALIAS_VF)
13151361
epi_after = _get_node_attr(G, node, ALIAS_EPI)
13161362
dnfr_after = _get_node_attr(G, node, ALIAS_DNFR)
@@ -1325,6 +1371,18 @@ def contraction_metrics(
13251371
densification_factor = last_entry.get("densification_factor")
13261372
dnfr_before = last_entry.get("dnfr_before")
13271373

1374+
# Calculate structural density before and after
1375+
# Density = |ΔNFR| / max(EPI, ε)
1376+
density_before = abs(dnfr_before) / max(abs(epi_before), EPSILON) if dnfr_before is not None else 0.0
1377+
density_after = abs(dnfr_after) / max(abs(epi_after), EPSILON)
1378+
1379+
# Calculate densification ratio (how much density increased)
1380+
densification_ratio = density_after / density_before if density_before > EPSILON else float('inf')
1381+
1382+
# Get critical density threshold from graph config or use default
1383+
critical_density_threshold = float(G.graph.get("CRITICAL_DENSITY_THRESHOLD", 5.0))
1384+
is_critical_density = density_after > critical_density_threshold
1385+
13281386
metrics = {
13291387
"operator": "Contraction",
13301388
"glyph": "NUL",
@@ -1344,6 +1402,12 @@ def contraction_metrics(
13441402
metrics["dnfr_before"] = dnfr_before
13451403
metrics["dnfr_increase"] = dnfr_after - dnfr_before if dnfr_before else 0.0
13461404

1405+
# Add NEW structural density metrics
1406+
metrics["density_before"] = density_before
1407+
metrics["density_after"] = density_after
1408+
metrics["densification_ratio"] = densification_ratio
1409+
metrics["is_critical_density"] = is_critical_density
1410+
13471411
return metrics
13481412

13491413

0 commit comments

Comments
 (0)