Skip to content

Commit 7140a47

Browse files
Copilotfermga
andcommitted
Implement enhanced NAV transition metrics with regime classification and scaling factors
Co-authored-by: fermga <203334638+fermga@users.noreply.github.com>
1 parent 406d1b4 commit 7140a47

File tree

3 files changed

+678
-18
lines changed

3 files changed

+678
-18
lines changed

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

src/tnfr/operators/metrics.py

Lines changed: 182 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1882,8 +1882,13 @@ def transition_metrics(
18821882
dnfr_before: float,
18831883
vf_before: float,
18841884
theta_before: float,
1885+
epi_before: float | None = None,
18851886
) -> dict[str, Any]:
1886-
"""NAV - Transition metrics: regime handoff, ΔNFR rebalancing.
1887+
"""NAV - Transition metrics: regime classification, phase shift, frequency scaling.
1888+
1889+
Collects comprehensive transition metrics including regime origin/destination,
1890+
phase shift magnitude (properly wrapped), transition type classification, and
1891+
structural preservation ratios as specified in TNFR.pdf Table 2.3.
18871892
18881893
Parameters
18891894
----------
@@ -1897,31 +1902,201 @@ def transition_metrics(
18971902
νf value before operator application
18981903
theta_before : float
18991904
Phase value before operator application
1905+
epi_before : float, optional
1906+
EPI value before operator application (for preservation tracking)
19001907
19011908
Returns
19021909
-------
19031910
dict
1904-
Transition-specific metrics including handoff success
1911+
Transition-specific metrics including:
1912+
1913+
**Core metrics (existing)**:
1914+
1915+
- operator: "Transition"
1916+
- glyph: "NAV"
1917+
- delta_theta: Signed phase change
1918+
- delta_vf: Change in νf
1919+
- delta_dnfr: Change in ΔNFR
1920+
- dnfr_final: Final ΔNFR value
1921+
- vf_final: Final νf value
1922+
- theta_final: Final phase value
1923+
- transition_complete: Boolean (|ΔNFR| < |νf|)
1924+
1925+
**Regime classification (NEW)**:
1926+
1927+
- regime_origin: "latent" | "active" | "resonant"
1928+
- regime_destination: "latent" | "active" | "resonant"
1929+
- transition_type: "reactivation" | "phase_shift" | "regime_change"
1930+
1931+
**Phase metrics (NEW)**:
1932+
1933+
- phase_shift_magnitude: Absolute phase change (radians, 0-π)
1934+
- phase_shift_signed: Signed phase change (radians, wrapped to [-π, π])
1935+
1936+
**Structural scaling (NEW)**:
1937+
1938+
- vf_scaling_factor: vf_after / vf_before
1939+
- dnfr_damping_ratio: dnfr_after / dnfr_before
1940+
- epi_preservation: epi_after / epi_before (if epi_before provided)
1941+
1942+
**Latency tracking (NEW)**:
1943+
1944+
- latency_duration: Time in silence (seconds) if transitioning from SHA
1945+
1946+
Notes
1947+
-----
1948+
**Regime Classification**:
1949+
1950+
- **Latent**: latent flag set OR νf < 0.05
1951+
- **Active**: Default operational state
1952+
- **Resonant**: EPI > 0.5 AND νf > 0.8
1953+
1954+
**Transition Type**:
1955+
1956+
- **reactivation**: From latent state (SHA → NAV flow)
1957+
- **phase_shift**: Significant phase change (|Δθ| > 0.3 rad)
1958+
- **regime_change**: Regime switch without significant phase shift
1959+
1960+
**Phase Shift Wrapping**:
1961+
1962+
Phase shifts are properly wrapped to [-π, π] range to handle 0-2π boundary
1963+
crossings correctly, ensuring accurate phase change measurement.
1964+
1965+
Examples
1966+
--------
1967+
>>> from tnfr.structural import create_nfr, run_sequence
1968+
>>> from tnfr.operators.definitions import Silence, Transition
1969+
>>>
1970+
>>> # Example: SHA → NAV reactivation
1971+
>>> G, node = create_nfr("test", epi=0.5, vf=0.8)
1972+
>>> G.graph["COLLECT_OPERATOR_METRICS"] = True
1973+
>>> run_sequence(G, node, [Silence(), Transition()])
1974+
>>>
1975+
>>> metrics = G.graph["operator_metrics"][-1]
1976+
>>> assert metrics["operator"] == "Transition"
1977+
>>> assert metrics["transition_type"] == "reactivation"
1978+
>>> assert metrics["regime_origin"] == "latent"
1979+
>>> assert metrics["latency_duration"] is not None
1980+
1981+
See Also
1982+
--------
1983+
operators.definitions.Transition : NAV operator implementation
1984+
operators.definitions.Transition._detect_regime : Regime detection logic
19051985
"""
1986+
import math
1987+
1988+
# Get current state (after transformation)
1989+
epi_after = _get_node_attr(G, node, ALIAS_EPI)
19061990
dnfr_after = _get_node_attr(G, node, ALIAS_DNFR)
19071991
vf_after = _get_node_attr(G, node, ALIAS_VF)
19081992
theta_after = _get_node_attr(G, node, ALIAS_THETA)
19091993

1994+
# === REGIME CLASSIFICATION ===
1995+
# Get regime origin from node attribute (stored by Transition operator before super().__call__)
1996+
regime_origin = G.nodes[node].get("_regime_before", None)
1997+
if regime_origin is None:
1998+
# Fallback: detect regime from before state
1999+
regime_origin = _detect_regime_from_state(
2000+
epi_before or epi_after, vf_before, False # Cannot access latent flag from before
2001+
)
2002+
2003+
# Detect destination regime
2004+
regime_destination = _detect_regime_from_state(
2005+
epi_after, vf_after, G.nodes[node].get("latent", False)
2006+
)
2007+
2008+
# === TRANSITION TYPE CLASSIFICATION ===
2009+
# Calculate phase shift (properly wrapped)
2010+
phase_shift_raw = theta_after - theta_before
2011+
if phase_shift_raw > math.pi:
2012+
phase_shift_raw -= 2 * math.pi
2013+
elif phase_shift_raw < -math.pi:
2014+
phase_shift_raw += 2 * math.pi
2015+
2016+
# Classify transition type
2017+
if regime_origin == "latent":
2018+
transition_type = "reactivation"
2019+
elif abs(phase_shift_raw) > 0.3:
2020+
transition_type = "phase_shift"
2021+
else:
2022+
transition_type = "regime_change"
2023+
2024+
# === STRUCTURAL SCALING FACTORS ===
2025+
vf_scaling = vf_after / vf_before if vf_before > 0 else 1.0
2026+
dnfr_damping = dnfr_after / dnfr_before if abs(dnfr_before) > 1e-9 else 1.0
2027+
2028+
# === EPI PRESERVATION ===
2029+
epi_preservation = None
2030+
if epi_before is not None and epi_before > 0:
2031+
epi_preservation = epi_after / epi_before
2032+
2033+
# === LATENCY DURATION ===
2034+
# Get from node if transitioning from silence
2035+
latency_duration = G.nodes[node].get("silence_duration", None)
2036+
19102037
return {
2038+
# === CORE (existing, preserved) ===
19112039
"operator": "Transition",
19122040
"glyph": "NAV",
1913-
"dnfr_change": abs(dnfr_after - dnfr_before),
2041+
"delta_theta": phase_shift_raw,
2042+
"delta_vf": vf_after - vf_before,
2043+
"delta_dnfr": dnfr_after - dnfr_before,
19142044
"dnfr_final": dnfr_after,
1915-
"vf_change": abs(vf_after - vf_before),
19162045
"vf_final": vf_after,
1917-
"theta_shift": abs(theta_after - theta_before),
19182046
"theta_final": theta_after,
1919-
# Transition complete when ΔNFR magnitude is bounded by νf magnitude
1920-
# indicating structural frequency dominates reorganization dynamics
19212047
"transition_complete": abs(dnfr_after) < abs(vf_after),
2048+
# Legacy compatibility
2049+
"dnfr_change": abs(dnfr_after - dnfr_before),
2050+
"vf_change": abs(vf_after - vf_before),
2051+
"theta_shift": abs(phase_shift_raw),
2052+
# === REGIME CLASSIFICATION (NEW) ===
2053+
"regime_origin": regime_origin,
2054+
"regime_destination": regime_destination,
2055+
"transition_type": transition_type,
2056+
# === PHASE METRICS (NEW) ===
2057+
"phase_shift_magnitude": abs(phase_shift_raw),
2058+
"phase_shift_signed": phase_shift_raw,
2059+
# === STRUCTURAL SCALING (NEW) ===
2060+
"vf_scaling_factor": vf_scaling,
2061+
"dnfr_damping_ratio": dnfr_damping,
2062+
"epi_preservation": epi_preservation,
2063+
# === LATENCY TRACKING (NEW) ===
2064+
"latency_duration": latency_duration,
19222065
}
19232066

19242067

2068+
def _detect_regime_from_state(epi: float, vf: float, latent: bool) -> str:
2069+
"""Detect structural regime from node state.
2070+
2071+
Helper function for transition_metrics to classify regime without
2072+
accessing the Transition operator directly.
2073+
2074+
Parameters
2075+
----------
2076+
epi : float
2077+
EPI value
2078+
vf : float
2079+
νf value
2080+
latent : bool
2081+
Latent flag
2082+
2083+
Returns
2084+
-------
2085+
str
2086+
Regime classification: "latent", "active", or "resonant"
2087+
2088+
Notes
2089+
-----
2090+
Matches logic in Transition._detect_regime (definitions.py).
2091+
"""
2092+
if latent or vf < 0.05:
2093+
return "latent"
2094+
elif epi > 0.5 and vf > 0.8:
2095+
return "resonant"
2096+
else:
2097+
return "active"
2098+
2099+
19252100
def recursivity_metrics(
19262101
G: TNFRGraph, node: NodeId, epi_before: float, vf_before: float
19272102
) -> dict[str, Any]:

0 commit comments

Comments
 (0)