Skip to content

Commit 4a57cda

Browse files
authored
Merge pull request #2889 from fermga/copilot/add-transition-metrics
[MEDIUM] NAV: Implement transition metrics - regime origin, phase shift, frequency change
2 parents 194a9e3 + e6799bf commit 4a57cda

File tree

4 files changed

+936
-18
lines changed

4 files changed

+936
-18
lines changed
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
"""Demo script showing enhanced NAV transition metrics.
2+
3+
This script demonstrates the comprehensive metrics now collected by the NAV
4+
(Transition) operator, including regime classification, scaling factors, and
5+
latency tracking.
6+
"""
7+
8+
from tnfr.structural import create_nfr, run_sequence
9+
from tnfr.operators.definitions import Silence, Transition, Coherence
10+
from tnfr.alias import get_attr
11+
from tnfr.constants.aliases import ALIAS_VF, ALIAS_THETA, ALIAS_DNFR, ALIAS_EPI
12+
import json
13+
14+
15+
def print_metrics(metrics: dict, title: str):
16+
"""Pretty print metrics."""
17+
print(f"\n{'='*60}")
18+
print(f"{title}")
19+
print('='*60)
20+
21+
# Core identification
22+
print(f"Operator: {metrics['operator']} ({metrics['glyph']})")
23+
24+
# Regime classification
25+
print(f"\nRegime Classification:")
26+
print(f" Origin: {metrics['regime_origin']}")
27+
print(f" Destination: {metrics['regime_destination']}")
28+
print(f" Type: {metrics['transition_type']}")
29+
30+
# Phase metrics
31+
print(f"\nPhase Metrics:")
32+
print(f" Magnitude: {metrics['phase_shift_magnitude']:.4f} rad")
33+
print(f" Signed: {metrics['phase_shift_signed']:.4f} rad")
34+
print(f" Final: {metrics['theta_final']:.4f} rad")
35+
36+
# Structural scaling
37+
print(f"\nStructural Scaling:")
38+
print(f" νf scaling: {metrics['vf_scaling_factor']:.4f}x")
39+
print(f" ΔNFR damping: {metrics['dnfr_damping_ratio']:.4f}x")
40+
if metrics['epi_preservation'] is not None:
41+
print(f" EPI preservation: {metrics['epi_preservation']:.4f}")
42+
43+
# Deltas
44+
print(f"\nChanges:")
45+
print(f" Δνf: {metrics['delta_vf']:+.4f}")
46+
print(f" ΔΔNFR: {metrics['delta_dnfr']:+.4f}")
47+
48+
# Latency
49+
if metrics['latency_duration'] is not None:
50+
print(f"\nLatency:")
51+
print(f" Duration: {metrics['latency_duration']:.6f} seconds")
52+
53+
# Status
54+
print(f"\nStatus:")
55+
print(f" Transition complete: {metrics['transition_complete']}")
56+
57+
58+
def demo_latent_to_active():
59+
"""Demonstrate latent → active reactivation."""
60+
print("\n" + "="*60)
61+
print("DEMO 1: Latent → Active Reactivation")
62+
print("="*60)
63+
64+
# Create node and apply silence
65+
G, node = create_nfr("demo1", epi=0.5, vf=0.8)
66+
G.graph["COLLECT_OPERATOR_METRICS"] = True
67+
68+
print("\n1. Initial state:")
69+
print(f" EPI={get_attr(G.nodes[node], ALIAS_EPI, 0.0):.4f}, "
70+
f"νf={get_attr(G.nodes[node], ALIAS_VF, 0.0):.4f}")
71+
72+
# Apply silence to enter latency
73+
run_sequence(G, node, [Silence()])
74+
print("\n2. After Silence (SHA):")
75+
print(f" EPI={get_attr(G.nodes[node], ALIAS_EPI, 0.0):.4f}, "
76+
f"νf={get_attr(G.nodes[node], ALIAS_VF, 0.0):.4f}, "
77+
f"latent={G.nodes[node].get('latent', False)}")
78+
79+
# Apply transition
80+
Transition()(G, node)
81+
print("\n3. After Transition (NAV):")
82+
print(f" EPI={get_attr(G.nodes[node], ALIAS_EPI, 0.0):.4f}, "
83+
f"νf={get_attr(G.nodes[node], ALIAS_VF, 0.0):.4f}, "
84+
f"latent={G.nodes[node].get('latent', False)}")
85+
86+
# Show metrics
87+
metrics = G.graph["operator_metrics"][-1]
88+
print_metrics(metrics, "Reactivation Metrics")
89+
90+
# Verify expectations
91+
assert metrics["transition_type"] == "reactivation"
92+
assert metrics["regime_origin"] == "latent"
93+
assert metrics["vf_scaling_factor"] > 1.0 # νf increased
94+
assert metrics["latency_duration"] is not None
95+
96+
print("\n✓ Latent → Active reactivation successful!")
97+
98+
99+
def demo_active_to_active():
100+
"""Demonstrate active → active standard transition."""
101+
print("\n" + "="*60)
102+
print("DEMO 2: Active → Active Standard Transition")
103+
print("="*60)
104+
105+
# Create node in active regime
106+
G, node = create_nfr("demo2", epi=0.4, vf=0.6)
107+
G.graph["COLLECT_OPERATOR_METRICS"] = True
108+
109+
print("\n1. Initial state (active regime):")
110+
print(f" EPI={get_attr(G.nodes[node], ALIAS_EPI, 0.0):.4f}, "
111+
f"νf={get_attr(G.nodes[node], ALIAS_VF, 0.0):.4f}")
112+
113+
# Apply transition
114+
Transition()(G, node)
115+
116+
print("\n2. After Transition (NAV):")
117+
print(f" EPI={get_attr(G.nodes[node], ALIAS_EPI, 0.0):.4f}, "
118+
f"νf={get_attr(G.nodes[node], ALIAS_VF, 0.0):.4f}")
119+
120+
# Show metrics
121+
metrics = G.graph["operator_metrics"][-1]
122+
print_metrics(metrics, "Standard Transition Metrics")
123+
124+
# Verify expectations
125+
assert metrics["transition_type"] == "regime_change"
126+
assert metrics["regime_origin"] == "active"
127+
assert metrics["regime_destination"] == "active"
128+
# Active transition preserves νf by default
129+
assert 0.9 <= metrics["vf_scaling_factor"] <= 1.1
130+
assert metrics["latency_duration"] is None # No prior silence
131+
132+
print("\n✓ Active → Active transition successful!")
133+
134+
135+
def demo_resonant_to_active():
136+
"""Demonstrate resonant → active stabilization."""
137+
print("\n" + "="*60)
138+
print("DEMO 3: Resonant → Active Stabilization")
139+
print("="*60)
140+
141+
# Create node in resonant regime (high EPI and νf)
142+
G, node = create_nfr("demo3", epi=0.7, vf=0.9)
143+
G.graph["COLLECT_OPERATOR_METRICS"] = True
144+
145+
print("\n1. Initial state (resonant regime):")
146+
print(f" EPI={get_attr(G.nodes[node], ALIAS_EPI, 0.0):.4f}, "
147+
f"νf={get_attr(G.nodes[node], ALIAS_VF, 0.0):.4f}")
148+
149+
# Apply transition
150+
Transition()(G, node)
151+
152+
print("\n2. After Transition (NAV):")
153+
print(f" EPI={get_attr(G.nodes[node], ALIAS_EPI, 0.0):.4f}, "
154+
f"νf={get_attr(G.nodes[node], ALIAS_VF, 0.0):.4f}")
155+
156+
# Show metrics
157+
metrics = G.graph["operator_metrics"][-1]
158+
print_metrics(metrics, "Stabilization Metrics")
159+
160+
# Verify expectations
161+
assert metrics["regime_origin"] == "resonant"
162+
# Resonant transition reduces νf slightly for stability
163+
assert metrics["vf_scaling_factor"] < 1.0
164+
assert metrics["vf_scaling_factor"] >= 0.9
165+
166+
print("\n✓ Resonant → Active stabilization successful!")
167+
168+
169+
def demo_phase_wrapping():
170+
"""Demonstrate phase wrapping at 2π boundary."""
171+
print("\n" + "="*60)
172+
print("DEMO 4: Phase Wrapping at 2π Boundary")
173+
print("="*60)
174+
175+
# Create node with phase near 2π
176+
G, node = create_nfr("demo4", epi=0.5, vf=0.6, theta=6.0)
177+
G.graph["COLLECT_OPERATOR_METRICS"] = True
178+
179+
theta_before = get_attr(G.nodes[node], ALIAS_THETA, 0.0)
180+
print(f"\n1. Initial phase: {theta_before:.4f} rad (near 2π = {2*3.14159:.4f})")
181+
182+
# Apply transition
183+
Transition()(G, node)
184+
185+
theta_after = get_attr(G.nodes[node], ALIAS_THETA, 0.0)
186+
print(f"2. Final phase: {theta_after:.4f} rad")
187+
188+
# Show metrics
189+
metrics = G.graph["operator_metrics"][-1]
190+
191+
print(f"\nPhase Change:")
192+
print(f" Raw: {theta_after - theta_before:+.4f} rad")
193+
print(f" Wrapped: {metrics['phase_shift_signed']:+.4f} rad")
194+
print(f" Magnitude: {metrics['phase_shift_magnitude']:.4f} rad")
195+
196+
# Verify wrapping
197+
import math
198+
assert abs(metrics['phase_shift_signed']) <= math.pi
199+
print(f"\n✓ Phase correctly wrapped to [-π, π]!")
200+
201+
202+
def demo_epi_preservation():
203+
"""Demonstrate EPI preservation tracking."""
204+
print("\n" + "="*60)
205+
print("DEMO 5: EPI Preservation Tracking")
206+
print("="*60)
207+
208+
# Create node and stabilize with coherence
209+
G, node = create_nfr("demo5", epi=0.6, vf=0.7)
210+
G.graph["COLLECT_OPERATOR_METRICS"] = True
211+
212+
# Apply coherence then transition
213+
run_sequence(G, node, [Coherence()])
214+
epi_before_nav = get_attr(G.nodes[node], ALIAS_EPI, 0.0)
215+
216+
Transition()(G, node)
217+
epi_after_nav = get_attr(G.nodes[node], ALIAS_EPI, 0.0)
218+
219+
# Show metrics
220+
metrics = G.graph["operator_metrics"][-1]
221+
222+
print(f"\nEPI Tracking:")
223+
print(f" Before NAV: {epi_before_nav:.6f}")
224+
print(f" After NAV: {epi_after_nav:.6f}")
225+
print(f" Preservation ratio: {metrics['epi_preservation']:.6f}")
226+
print(f" Drift: {abs(epi_after_nav - epi_before_nav):.6f}")
227+
228+
# Verify preservation
229+
assert metrics['epi_preservation'] is not None
230+
assert 0.95 <= metrics['epi_preservation'] <= 1.05
231+
print(f"\n✓ EPI identity preserved (< 5% drift)!")
232+
233+
234+
if __name__ == "__main__":
235+
print("\n" + "="*60)
236+
print("NAV TRANSITION METRICS DEMONSTRATION")
237+
print("Enhanced metrics for regime classification and scaling")
238+
print("="*60)
239+
240+
demo_latent_to_active()
241+
demo_active_to_active()
242+
demo_resonant_to_active()
243+
demo_phase_wrapping()
244+
demo_epi_preservation()
245+
246+
print("\n" + "="*60)
247+
print("ALL DEMOS COMPLETED SUCCESSFULLY!")
248+
print("="*60)
249+
print("\nKey Enhancements:")
250+
print(" ✓ Regime origin/destination classification")
251+
print(" ✓ Transition type (reactivation/phase_shift/regime_change)")
252+
print(" ✓ Phase shift magnitude with proper 2π wrapping")
253+
print(" ✓ Frequency scaling factor (νf_after / νf_before)")
254+
print(" ✓ ΔNFR damping ratio")
255+
print(" ✓ EPI preservation tracking")
256+
print(" ✓ Latency duration from SHA → NAV")
257+
print(" ✓ Full backward compatibility")
258+
print()

src/tnfr/operators/definitions.py

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3741,8 +3741,8 @@ def __call__(self, G: TNFRGraph, node: Any, **kw: Any) -> None:
37413741
Implements TNFR.pdf §2.3.11 canonical transition logic:
37423742
1. Detect current structural regime (latent/active/resonant)
37433743
2. Handle latency reactivation if node was in silence (SHA → NAV)
3744-
3. Apply grammar via parent __call__
3745-
4. Execute regime-specific structural transformation (θ, νf, ΔNFR)
3744+
3. Apply grammar and structural transformation
3745+
4. Collect metrics (if enabled)
37463746
37473747
Parameters
37483748
----------
@@ -3754,7 +3754,7 @@ def __call__(self, G: TNFRGraph, node: Any, **kw: Any) -> None:
37543754
Additional keyword arguments:
37553755
- phase_shift (float): Override default phase shift per regime
37563756
- vf_factor (float): Override νf scaling for active regime (default: 1.0)
3757-
- Other args forwarded to grammar layer via parent __call__
3757+
- Other args forwarded to grammar layer
37583758
37593759
Notes
37603760
-----
@@ -3781,19 +3781,67 @@ def __call__(self, G: TNFRGraph, node: Any, **kw: Any) -> None:
37813781
from ..alias import get_attr, set_attr
37823782
from ..constants.aliases import ALIAS_DNFR, ALIAS_EPI, ALIAS_THETA, ALIAS_VF
37833783

3784-
# 1. Detect current regime
3784+
# 1. Detect current regime and store for metrics collection
37853785
current_regime = self._detect_regime(G, node)
3786+
G.nodes[node]["_regime_before"] = current_regime
37863787

37873788
# 2. Handle latency reactivation if applicable
37883789
if G.nodes[node].get("latent", False):
37893790
self._handle_latency_transition(G, node)
37903791

3791-
# 3. Apply grammar base (delegates to parent which calls apply_glyph_with_grammar)
3792-
super().__call__(G, node, **kw)
3792+
# 3. Validate preconditions (if enabled)
3793+
validate_preconditions = kw.get("validate_preconditions", True) or G.graph.get(
3794+
"VALIDATE_PRECONDITIONS", False
3795+
)
3796+
if validate_preconditions:
3797+
self._validate_preconditions(G, node)
3798+
3799+
# 4. Capture state before for metrics/validation
3800+
collect_metrics = kw.get("collect_metrics", False) or G.graph.get(
3801+
"COLLECT_OPERATOR_METRICS", False
3802+
)
3803+
validate_equation = kw.get("validate_nodal_equation", False) or G.graph.get(
3804+
"VALIDATE_NODAL_EQUATION", False
3805+
)
37933806

3794-
# 4. Execute structural transition
3807+
state_before = None
3808+
if collect_metrics or validate_equation:
3809+
state_before = self._capture_state(G, node)
3810+
3811+
# 5. Apply grammar
3812+
from . import apply_glyph_with_grammar
3813+
apply_glyph_with_grammar(G, [node], self.glyph, kw.get("window"))
3814+
3815+
# 6. Execute structural transition (BEFORE metrics collection)
37953816
self._apply_structural_transition(G, node, current_regime, **kw)
37963817

3818+
# 7. Optional nodal equation validation
3819+
if validate_equation and state_before is not None:
3820+
from ..alias import get_attr
3821+
from ..constants.aliases import ALIAS_EPI
3822+
from .nodal_equation import validate_nodal_equation
3823+
3824+
dt = float(kw.get("dt", 1.0))
3825+
strict = G.graph.get("NODAL_EQUATION_STRICT", False)
3826+
epi_after = float(get_attr(G.nodes[node], ALIAS_EPI, 0.0))
3827+
3828+
validate_nodal_equation(
3829+
G,
3830+
node,
3831+
epi_before=state_before["epi"],
3832+
epi_after=epi_after,
3833+
dt=dt,
3834+
operator_name=self.name,
3835+
strict=strict,
3836+
)
3837+
3838+
# 8. Optional metrics collection (AFTER structural transformation)
3839+
if collect_metrics and state_before is not None:
3840+
metrics = self._collect_metrics(G, node, state_before)
3841+
if "operator_metrics" not in G.graph:
3842+
G.graph["operator_metrics"] = []
3843+
G.graph["operator_metrics"].append(metrics)
3844+
37973845
def _detect_regime(self, G: TNFRGraph, node: Any) -> str:
37983846
"""Detect current structural regime: latent/active/resonant.
37993847
@@ -3896,9 +3944,7 @@ def _handle_latency_transition(self, G: TNFRGraph, node: Any) -> None:
38963944
del G.nodes[node]["latency_start_time"]
38973945
if "preserved_epi" in G.nodes[node]:
38983946
del G.nodes[node]["preserved_epi"]
3899-
if "silence_duration" in G.nodes[node]:
3900-
# Keep silence_duration for telemetry
3901-
pass
3947+
# Keep silence_duration for telemetry/metrics - don't delete it
39023948

39033949
def _apply_structural_transition(
39043950
self, G: TNFRGraph, node: Any, regime: str, **kw: Any
@@ -3989,7 +4035,12 @@ def _collect_metrics(
39894035
from .metrics import transition_metrics
39904036

39914037
return transition_metrics(
3992-
G, node, state_before["dnfr"], state_before["vf"], state_before["theta"]
4038+
G,
4039+
node,
4040+
state_before["dnfr"],
4041+
state_before["vf"],
4042+
state_before["theta"],
4043+
epi_before=state_before.get("epi"),
39934044
)
39944045

39954046

0 commit comments

Comments
 (0)