Skip to content

Commit 6c1198d

Browse files
authored
Merge pull request #2886 from fermga/copilot/implement-canonical-transition-logic
Implement canonical NAV transition logic - regime detection, structural transformations, telemetry
2 parents 607b2bd + 15d4958 commit 6c1198d

File tree

3 files changed

+685
-1
lines changed

3 files changed

+685
-1
lines changed

src/tnfr/operators/definitions.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3735,6 +3735,247 @@ class Transition(Operator):
37353735
name: ClassVar[str] = TRANSITION
37363736
glyph: ClassVar[Glyph] = Glyph.NAV
37373737

3738+
def __call__(self, G: TNFRGraph, node: Any, **kw: Any) -> None:
3739+
"""Apply NAV with regime detection and controlled transition.
3740+
3741+
Implements TNFR.pdf §2.3.11 canonical transition logic:
3742+
1. Detect current structural regime (latent/active/resonant)
3743+
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)
3746+
3747+
Parameters
3748+
----------
3749+
G : TNFRGraph
3750+
Graph storing TNFR nodes and structural operator history.
3751+
node : Any
3752+
Identifier or object representing the target node within ``G``.
3753+
**kw : Any
3754+
Additional keyword arguments:
3755+
- phase_shift (float): Override default phase shift per regime
3756+
- vf_factor (float): Override νf scaling for active regime (default: 1.0)
3757+
- Other args forwarded to grammar layer via parent __call__
3758+
3759+
Notes
3760+
-----
3761+
Regime-specific transformations (TNFR.pdf §2.3.11):
3762+
3763+
**Latent → Active** (νf < 0.05 or latent flag):
3764+
- νf × 1.2 (20% increase for gradual reactivation)
3765+
- θ + 0.1 rad (small phase shift)
3766+
- ΔNFR × 0.7 (30% reduction for smooth transition)
3767+
3768+
**Active** (baseline state):
3769+
- νf × vf_factor (default 1.0, configurable)
3770+
- θ + 0.2 rad (standard phase shift)
3771+
- ΔNFR × 0.8 (20% reduction)
3772+
3773+
**Resonant → Active** (EPI > 0.5 AND νf > 0.8):
3774+
- νf × 0.95 (5% reduction for stability)
3775+
- θ + 0.15 rad (careful phase shift)
3776+
- ΔNFR × 0.9 (10% reduction, gentle)
3777+
3778+
Telemetry stored in G.graph["_nav_transitions"] tracks:
3779+
- regime_origin, vf_before/after, theta_before/after, dnfr_before/after
3780+
"""
3781+
from ..alias import get_attr, set_attr
3782+
from ..constants.aliases import ALIAS_DNFR, ALIAS_EPI, ALIAS_THETA, ALIAS_VF
3783+
3784+
# 1. Detect current regime
3785+
current_regime = self._detect_regime(G, node)
3786+
3787+
# 2. Handle latency reactivation if applicable
3788+
if G.nodes[node].get("latent", False):
3789+
self._handle_latency_transition(G, node)
3790+
3791+
# 3. Apply grammar base (delegates to parent which calls apply_glyph_with_grammar)
3792+
super().__call__(G, node, **kw)
3793+
3794+
# 4. Execute structural transition
3795+
self._apply_structural_transition(G, node, current_regime, **kw)
3796+
3797+
def _detect_regime(self, G: TNFRGraph, node: Any) -> str:
3798+
"""Detect current structural regime: latent/active/resonant.
3799+
3800+
Parameters
3801+
----------
3802+
G : TNFRGraph
3803+
Graph containing the node.
3804+
node : Any
3805+
Target node.
3806+
3807+
Returns
3808+
-------
3809+
str
3810+
Regime classification: "latent", "active", or "resonant"
3811+
3812+
Notes
3813+
-----
3814+
Classification criteria:
3815+
- **Latent**: latent flag set OR νf < 0.05 (minimal reorganization capacity)
3816+
- **Resonant**: EPI > 0.5 AND νf > 0.8 (high form + high frequency)
3817+
- **Active**: Default (baseline operational state)
3818+
"""
3819+
from ..alias import get_attr
3820+
from ..constants.aliases import ALIAS_EPI, ALIAS_VF
3821+
3822+
epi = float(get_attr(G.nodes[node], ALIAS_EPI, 0.0))
3823+
vf = float(get_attr(G.nodes[node], ALIAS_VF, 0.0))
3824+
latent = G.nodes[node].get("latent", False)
3825+
3826+
if latent or vf < 0.05:
3827+
return "latent"
3828+
elif epi > 0.5 and vf > 0.8:
3829+
return "resonant"
3830+
else:
3831+
return "active"
3832+
3833+
def _handle_latency_transition(self, G: TNFRGraph, node: Any) -> None:
3834+
"""Handle transition from latent state (SHA → NAV flow).
3835+
3836+
Similar to Emission._check_reactivation but for NAV-specific transitions.
3837+
Validates silence duration and clears latency attributes.
3838+
3839+
Parameters
3840+
----------
3841+
G : TNFRGraph
3842+
Graph containing the node.
3843+
node : Any
3844+
Target node being reactivated.
3845+
3846+
Warnings
3847+
--------
3848+
- Warns if node transitioning after extended silence (duration > MAX_SILENCE_DURATION)
3849+
- Warns if EPI drifted significantly during silence (> 1% tolerance)
3850+
3851+
Notes
3852+
-----
3853+
Clears latency-related attributes:
3854+
- latent (flag)
3855+
- latency_start_time (ISO timestamp)
3856+
- preserved_epi (EPI snapshot from SHA)
3857+
- silence_duration (computed duration)
3858+
"""
3859+
from datetime import datetime, timezone
3860+
3861+
# Verify silence duration if timestamp available
3862+
if "latency_start_time" in G.nodes[node]:
3863+
start = datetime.fromisoformat(G.nodes[node]["latency_start_time"])
3864+
duration = (datetime.now(timezone.utc) - start).total_seconds()
3865+
G.nodes[node]["silence_duration"] = duration
3866+
3867+
max_silence = G.graph.get("MAX_SILENCE_DURATION", float("inf"))
3868+
if duration > max_silence:
3869+
warnings.warn(
3870+
f"Node {node} transitioning after extended silence "
3871+
f"(duration: {duration:.2f}s, max: {max_silence:.2f}s)",
3872+
stacklevel=4,
3873+
)
3874+
3875+
# Check EPI preservation integrity
3876+
preserved_epi = G.nodes[node].get("preserved_epi")
3877+
if preserved_epi is not None:
3878+
from ..alias import get_attr
3879+
from ..constants.aliases import ALIAS_EPI
3880+
3881+
current_epi = float(get_attr(G.nodes[node], ALIAS_EPI, 0.0))
3882+
epi_drift = abs(current_epi - preserved_epi)
3883+
3884+
# Allow small numerical drift (1% tolerance)
3885+
if epi_drift > 0.01 * abs(preserved_epi):
3886+
warnings.warn(
3887+
f"Node {node} EPI drifted during silence "
3888+
f"(preserved: {preserved_epi:.3f}, current: {current_epi:.3f}, "
3889+
f"drift: {epi_drift:.3f})",
3890+
stacklevel=4,
3891+
)
3892+
3893+
# Clear latency state
3894+
del G.nodes[node]["latent"]
3895+
if "latency_start_time" in G.nodes[node]:
3896+
del G.nodes[node]["latency_start_time"]
3897+
if "preserved_epi" in G.nodes[node]:
3898+
del G.nodes[node]["preserved_epi"]
3899+
if "silence_duration" in G.nodes[node]:
3900+
# Keep silence_duration for telemetry
3901+
pass
3902+
3903+
def _apply_structural_transition(
3904+
self, G: TNFRGraph, node: Any, regime: str, **kw: Any
3905+
) -> None:
3906+
"""Apply structural transformation based on regime origin.
3907+
3908+
Parameters
3909+
----------
3910+
G : TNFRGraph
3911+
Graph containing the node.
3912+
node : Any
3913+
Target node.
3914+
regime : str
3915+
Origin regime: "latent", "active", or "resonant"
3916+
**kw : Any
3917+
Optional overrides:
3918+
- phase_shift (float): Custom phase shift
3919+
- vf_factor (float): Custom νf scaling for active regime
3920+
3921+
Notes
3922+
-----
3923+
Applies regime-specific transformations to θ, νf, and ΔNFR following
3924+
TNFR.pdf §2.3.11. All changes use canonical alias system (set_attr)
3925+
to ensure proper attribute resolution.
3926+
3927+
Telemetry appended to G.graph["_nav_transitions"] for analysis.
3928+
"""
3929+
from ..alias import get_attr, set_attr
3930+
from ..constants.aliases import ALIAS_DNFR, ALIAS_THETA, ALIAS_VF
3931+
3932+
# Get current state
3933+
theta = float(get_attr(G.nodes[node], ALIAS_THETA, 0.0))
3934+
vf = float(get_attr(G.nodes[node], ALIAS_VF, 1.0))
3935+
dnfr = float(get_attr(G.nodes[node], ALIAS_DNFR, 0.0))
3936+
3937+
# Apply regime-specific adjustments
3938+
if regime == "latent":
3939+
# Latent → Active: gradual reactivation
3940+
vf_new = vf * 1.2 # 20% increase
3941+
theta_shift = kw.get("phase_shift", 0.1) # Small phase shift
3942+
theta_new = (theta + theta_shift) % (2 * math.pi)
3943+
dnfr_new = dnfr * 0.7 # 30% reduction for smooth transition
3944+
elif regime == "active":
3945+
# Active: standard transition
3946+
vf_new = vf * kw.get("vf_factor", 1.0) # Configurable
3947+
theta_shift = kw.get("phase_shift", 0.2) # Standard shift
3948+
theta_new = (theta + theta_shift) % (2 * math.pi)
3949+
dnfr_new = dnfr * 0.8 # 20% reduction
3950+
else: # resonant
3951+
# Resonant → Active: careful transition (high energy state)
3952+
vf_new = vf * 0.95 # 5% reduction for stability
3953+
theta_shift = kw.get("phase_shift", 0.15) # Careful phase shift
3954+
theta_new = (theta + theta_shift) % (2 * math.pi)
3955+
dnfr_new = dnfr * 0.9 # 10% reduction, gentle
3956+
3957+
# Apply changes via canonical alias system
3958+
set_attr(G.nodes[node], ALIAS_VF, vf_new)
3959+
set_attr(G.nodes[node], ALIAS_THETA, theta_new)
3960+
set_attr(G.nodes[node], ALIAS_DNFR, dnfr_new)
3961+
3962+
# Telemetry tracking
3963+
if "_nav_transitions" not in G.graph:
3964+
G.graph["_nav_transitions"] = []
3965+
G.graph["_nav_transitions"].append(
3966+
{
3967+
"node": node,
3968+
"regime_origin": regime,
3969+
"vf_before": vf,
3970+
"vf_after": vf_new,
3971+
"theta_before": theta,
3972+
"theta_after": theta_new,
3973+
"dnfr_before": dnfr,
3974+
"dnfr_after": dnfr_new,
3975+
"phase_shift": theta_new - theta,
3976+
}
3977+
)
3978+
37383979
def _validate_preconditions(self, G: TNFRGraph, node: Any) -> None:
37393980
"""Validate NAV-specific preconditions."""
37403981
from .preconditions import validate_transition

src/tnfr/validation/sequence_validator.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,17 @@ def __init__(self) -> None:
3535
"invalid_transition_sequence": {
3636
"pattern": ["transition"],
3737
"not_preceded_by": [
38+
"emission", # AL → NAV (activate-transition)
39+
"silence", # SHA → NAV (latency-transition)
40+
"coherence", # IL → NAV (stabilize-transition)
3841
"dissonance",
3942
"mutation",
4043
"resonance",
4144
"transition",
45+
"reception", # EN → NAV (integrate-transition)
4246
],
4347
"severity": InvariantSeverity.ERROR,
44-
"message": "Transition requires structural perturbation (dissonance, mutation) or coherent propagation (resonance)",
48+
"message": "Transition requires prior structural context (emission, coherence, perturbation, or propagation)",
4549
},
4650
"resonance_without_coupling": {
4751
"pattern": ["resonance"],

0 commit comments

Comments
 (0)