@@ -1542,12 +1542,23 @@ def self_organization_metrics(
15421542
15431543
15441544def mutation_metrics (
1545- G : TNFRGraph , node : NodeId , theta_before : float , epi_before : float
1545+ G : TNFRGraph ,
1546+ node : NodeId ,
1547+ theta_before : float ,
1548+ epi_before : float ,
1549+ vf_before : float | None = None ,
1550+ dnfr_before : float | None = None ,
15461551) -> dict [str , Any ]:
1547- """ZHIR - Mutation metrics: phase transition, structural regime change .
1552+ """ZHIR - Comprehensive mutation metrics with canonical structural indicators .
15481553
1549- Collects mutation-specific metrics including canonical threshold verification
1550- (∂EPI/∂t > ξ) as per TNFR physics (AGENTS.md §11, TNFR.pdf §2.2.11).
1554+ Collects extended metrics reflecting canonical ZHIR effects:
1555+ - Threshold verification (∂EPI/∂t > ξ)
1556+ - Phase transformation quality (θ → θ')
1557+ - Bifurcation potential (∂²EPI/∂t² > τ)
1558+ - Structural identity preservation
1559+ - Network impact and propagation
1560+ - Destabilizer context (R4 Extended)
1561+ - Grammar validation status
15511562
15521563 Parameters
15531564 ----------
@@ -1559,71 +1570,309 @@ def mutation_metrics(
15591570 Phase value before operator application
15601571 epi_before : float
15611572 EPI value before operator application
1573+ vf_before : float, optional
1574+ νf before mutation (for frequency shift tracking)
1575+ dnfr_before : float, optional
1576+ ΔNFR before mutation (for pressure tracking)
15621577
15631578 Returns
15641579 -------
15651580 dict
1566- Mutation-specific metrics including:
1567- - Phase change indicators (theta_shift, phase_change)
1568- - EPI change (delta_epi)
1569- - **Threshold verification**: depi_dt, threshold_met, threshold_ratio
1570- - Structural justification flags
1581+ Comprehensive mutation metrics organized by category:
1582+
1583+ **Core metrics (existing):**
1584+
1585+ - operator, glyph: Identification
1586+ - theta_shift, theta_final: Phase changes
1587+ - delta_epi, epi_final: EPI changes
1588+ - phase_change: Boolean indicator
1589+
1590+ **Threshold verification (ENHANCED):**
1591+
1592+ - depi_dt: Structural velocity (∂EPI/∂t)
1593+ - threshold_xi: Configured threshold
1594+ - threshold_met: Boolean (∂EPI/∂t > ξ)
1595+ - threshold_ratio: depi_dt / ξ
1596+ - threshold_exceeded_by: max(0, depi_dt - ξ)
1597+
1598+ **Phase transformation (ENHANCED):**
1599+
1600+ - theta_regime_before: Initial phase regime [0-3]
1601+ - theta_regime_after: Final phase regime [0-3]
1602+ - regime_changed: Boolean regime transition
1603+ - theta_shift_direction: +1 (forward) or -1 (backward)
1604+ - phase_transformation_magnitude: Normalized shift [0, 1]
1605+
1606+ **Bifurcation analysis (NEW):**
1607+
1608+ - d2epi: Structural acceleration
1609+ - bifurcation_threshold_tau: Configured τ
1610+ - bifurcation_potential: Boolean (∂²EPI/∂t² > τ)
1611+ - bifurcation_score: Quantitative potential [0, 1]
1612+ - bifurcation_triggered: Boolean (event recorded)
1613+ - bifurcation_event_count: Number of bifurcation events
1614+
1615+ **Structural preservation (NEW):**
1616+
1617+ - epi_kind_before: Identity before mutation
1618+ - epi_kind_after: Identity after mutation
1619+ - identity_preserved: Boolean (must be True)
1620+ - delta_vf: Change in structural frequency
1621+ - vf_final: Final νf
1622+ - delta_dnfr: Change in reorganization pressure
1623+ - dnfr_final: Final ΔNFR
1624+
1625+ **Network impact (NEW):**
1626+
1627+ - neighbor_count: Number of neighbors
1628+ - impacted_neighbors: Count with phase shift detected
1629+ - network_impact_radius: Ratio of impacted neighbors
1630+ - phase_coherence_neighbors: Phase alignment after mutation
1631+
1632+ **Destabilizer context (NEW - R4 Extended):**
1633+
1634+ - destabilizer_type: "strong"/"moderate"/"weak"/None
1635+ - destabilizer_operator: Glyph that enabled mutation
1636+ - destabilizer_distance: Operators since destabilizer
1637+ - recent_history: Last 4 operators
1638+
1639+ **Grammar validation (NEW):**
1640+
1641+ - grammar_u4b_satisfied: Boolean (IL precedence + destabilizer)
1642+ - il_precedence_found: Boolean (IL in history)
1643+ - destabilizer_recent: Boolean (within window)
1644+
1645+ Examples
1646+ --------
1647+ >>> from tnfr.structural import create_nfr, run_sequence
1648+ >>> from tnfr.operators.definitions import Coherence, Dissonance, Mutation
1649+ >>>
1650+ >>> G, node = create_nfr("test", epi=0.5, vf=1.2)
1651+ >>> G.graph["COLLECT_OPERATOR_METRICS"] = True
1652+ >>>
1653+ >>> # Apply canonical sequence (IL → OZ → ZHIR)
1654+ >>> run_sequence(G, node, [Coherence(), Dissonance(), Mutation()])
1655+ >>>
1656+ >>> # Retrieve comprehensive metrics
1657+ >>> metrics = G.graph["operator_metrics"][-1]
1658+ >>> print(f"Threshold met: {metrics['threshold_met']}")
1659+ >>> print(f"Bifurcation score: {metrics['bifurcation_score']:.2f}")
1660+ >>> print(f"Identity preserved: {metrics['identity_preserved']}")
1661+ >>> print(f"Grammar satisfied: {metrics['grammar_u4b_satisfied']}")
1662+
1663+ See Also
1664+ --------
1665+ operators.definitions.Mutation : ZHIR operator implementation
1666+ dynamics.bifurcation.compute_bifurcation_score : Bifurcation scoring
1667+ operators.preconditions.validate_mutation : Precondition validation with context tracking
15711668 """
1669+ import math
1670+
1671+ # === GET POST-MUTATION STATE ===
15721672 theta_after = _get_node_attr (G , node , ALIAS_THETA )
15731673 epi_after = _get_node_attr (G , node , ALIAS_EPI )
1674+ vf_after = _get_node_attr (G , node , ALIAS_VF )
1675+ dnfr_after = _get_node_attr (G , node , ALIAS_DNFR )
1676+ d2epi = _get_node_attr (G , node , ALIAS_D2EPI , 0.0 )
15741677
1678+ # === THRESHOLD VERIFICATION ===
15751679 # Compute ∂EPI/∂t from history
1576- epi_history = G .nodes [node ].get ("epi_history" ) or G .nodes [node ].get ("_epi_history" , [])
1680+ epi_history = G .nodes [node ].get ("epi_history" ) or G .nodes [node ].get (
1681+ "_epi_history" , []
1682+ )
15771683 if len (epi_history ) >= 2 :
15781684 depi_dt = abs (epi_history [- 1 ] - epi_history [- 2 ])
15791685 else :
15801686 depi_dt = 0.0
15811687
1582- # Check threshold
15831688 xi = float (G .graph .get ("ZHIR_THRESHOLD_XI" , 0.1 ))
15841689 threshold_met = depi_dt >= xi
15851690 threshold_ratio = depi_dt / xi if xi > 0 else 0.0
15861691
1692+ # === PHASE TRANSFORMATION ===
15871693 # Extract transformation telemetry from glyph storage
15881694 theta_shift_stored = G .nodes [node ].get ("_zhir_theta_shift" , None )
15891695 regime_changed = G .nodes [node ].get ("_zhir_regime_changed" , False )
1590- regime_before = G .nodes [node ].get ("_zhir_regime_before" , None )
1591- regime_after = G .nodes [node ].get ("_zhir_regime_after" , None )
1696+ regime_before_stored = G .nodes [node ].get ("_zhir_regime_before" , None )
1697+ regime_after_stored = G .nodes [node ].get ("_zhir_regime_after" , None )
15921698 fixed_mode = G .nodes [node ].get ("_zhir_fixed_mode" , False )
1593-
1594- # Compute theta shift magnitude
1595- theta_shift_magnitude = abs (theta_after - theta_before )
1596-
1699+
1700+ # Compute theta shift
1701+ theta_shift = theta_after - theta_before
1702+ theta_shift_magnitude = abs (theta_shift )
1703+
1704+ # Compute regimes if not stored
1705+ regime_before = (
1706+ regime_before_stored
1707+ if regime_before_stored is not None
1708+ else int (theta_before // (math .pi / 2 ))
1709+ )
1710+ regime_after = (
1711+ regime_after_stored
1712+ if regime_after_stored is not None
1713+ else int (theta_after // (math .pi / 2 ))
1714+ )
1715+
1716+ # Normalized phase transformation magnitude [0, 1]
1717+ phase_transformation_magnitude = min (theta_shift_magnitude / math .pi , 1.0 )
1718+
1719+ # === BIFURCATION ANALYSIS ===
1720+ tau = float (
1721+ G .graph .get (
1722+ "BIFURCATION_THRESHOLD_TAU" , G .graph .get ("ZHIR_BIFURCATION_THRESHOLD" , 0.5 )
1723+ )
1724+ )
1725+ bifurcation_potential = d2epi > tau
1726+
1727+ # Compute bifurcation score using canonical formula
1728+ from ..dynamics .bifurcation import compute_bifurcation_score
1729+
1730+ bifurcation_score = compute_bifurcation_score (
1731+ d2epi = d2epi , dnfr = dnfr_after , vf = vf_after , epi = epi_after , tau = tau
1732+ )
1733+
1734+ # Check if bifurcation was triggered (event recorded)
1735+ bifurcation_events = G .graph .get ("zhir_bifurcation_events" , [])
1736+ bifurcation_triggered = len (bifurcation_events ) > 0
1737+ bifurcation_event_count = len (bifurcation_events )
1738+
1739+ # === STRUCTURAL PRESERVATION ===
1740+ epi_kind_before = G .nodes [node ].get ("_epi_kind_before" )
1741+ epi_kind_after = G .nodes [node ].get ("epi_kind" )
1742+ identity_preserved = (
1743+ epi_kind_before == epi_kind_after if epi_kind_before is not None else True
1744+ )
1745+
1746+ # Track frequency and pressure changes
1747+ delta_vf = vf_after - vf_before if vf_before is not None else 0.0
1748+ delta_dnfr = dnfr_after - dnfr_before if dnfr_before is not None else 0.0
1749+
1750+ # === NETWORK IMPACT ===
1751+ neighbors = list (G .neighbors (node ))
1752+ neighbor_count = len (neighbors )
1753+
1754+ # Count neighbors that experienced phase shifts
1755+ # This is a simplified heuristic - we check if neighbors have recent phase changes
1756+ impacted_neighbors = 0
1757+ phase_impact_threshold = 0.1
1758+
1759+ if neighbor_count > 0 :
1760+ # Check neighbors for phase alignment/disruption
1761+ for n in neighbors :
1762+ neighbor_theta = _get_node_attr (G , n , ALIAS_THETA )
1763+ # Simplified: check if neighbor is in similar phase regime after mutation
1764+ phase_diff = abs (neighbor_theta - theta_after )
1765+ # If phase diff is large, neighbor might be impacted
1766+ if phase_diff > phase_impact_threshold :
1767+ # Check if neighbor has changed recently (has history)
1768+ neighbor_theta_history = G .nodes [n ].get ("theta_history" , [])
1769+ if len (neighbor_theta_history ) >= 2 :
1770+ neighbor_change = abs (
1771+ neighbor_theta_history [- 1 ] - neighbor_theta_history [- 2 ]
1772+ )
1773+ if neighbor_change > 0.05 : # Neighbor experienced change
1774+ impacted_neighbors += 1
1775+
1776+ # Phase coherence with neighbors after mutation
1777+ from ..metrics .phase_coherence import compute_phase_alignment
1778+
1779+ phase_coherence = compute_phase_alignment (G , node , radius = 1 )
1780+ else :
1781+ phase_coherence = 0.0
1782+
1783+ # === DESTABILIZER CONTEXT (R4 Extended) ===
1784+ mutation_context = G .nodes [node ].get ("_mutation_context" , {})
1785+ destabilizer_type = mutation_context .get ("destabilizer_type" )
1786+ destabilizer_operator = mutation_context .get ("destabilizer_operator" )
1787+ destabilizer_distance = mutation_context .get ("destabilizer_distance" )
1788+ recent_history = mutation_context .get ("recent_history" , [])
1789+
1790+ # === GRAMMAR VALIDATION (U4b) ===
1791+ # Check if U4b satisfied (IL precedence + recent destabilizer)
1792+ glyph_history = G .nodes [node ].get ("glyph_history" , [])
1793+
1794+ # Look for IL in history
1795+ il_precedence_found = any ("IL" in str (g ) for g in glyph_history )
1796+
1797+ # Check if destabilizer is recent (within ~3 operators)
1798+ destabilizer_recent = (
1799+ destabilizer_distance is not None and destabilizer_distance <= 3
1800+ )
1801+
1802+ grammar_u4b_satisfied = il_precedence_found and destabilizer_recent
1803+
1804+ # === RETURN COMPREHENSIVE METRICS ===
15971805 return {
1806+ # === CORE (existing) ===
15981807 "operator" : "Mutation" ,
15991808 "glyph" : "ZHIR" ,
1600- # Phase transformation metrics (ENHANCED)
16011809 "theta_shift" : theta_shift_magnitude ,
1602- "theta_shift_signed" : theta_shift_stored if theta_shift_stored is not None else (theta_after - theta_before ),
1810+ "theta_shift_signed" : (
1811+ theta_shift_stored if theta_shift_stored is not None else theta_shift
1812+ ),
16031813 "theta_before" : theta_before ,
16041814 "theta_after" : theta_after ,
16051815 "theta_final" : theta_after ,
16061816 "phase_change" : theta_shift_magnitude > 0.5 , # Configurable threshold
1607- # NEW: Regime change detection
1608- "theta_regime_change" : regime_changed ,
1609- "regime_before" : regime_before if regime_before is not None else int (theta_before // (3.14159 / 2 )),
1610- "regime_after" : regime_after if regime_after is not None else int (theta_after // (3.14159 / 2 )),
16111817 "transformation_mode" : "fixed" if fixed_mode else "canonical" ,
1612- # EPI metrics
1613- "delta_epi" : epi_after - epi_before ,
1614- "epi_before" : epi_before ,
1615- "epi_after" : epi_after ,
1616- "epi_final" : epi_after ,
1617- # Threshold verification metrics
1818+ # === THRESHOLD VERIFICATION (ENHANCED) ===
16181819 "depi_dt" : depi_dt ,
16191820 "threshold_xi" : xi ,
16201821 "threshold_met" : threshold_met ,
16211822 "threshold_ratio" : threshold_ratio ,
16221823 "threshold_exceeded_by" : max (0.0 , depi_dt - xi ),
1623- # Structural justification flags from preconditions
16241824 "threshold_warning" : G .nodes [node ].get ("_zhir_threshold_warning" , False ),
16251825 "threshold_validated" : G .nodes [node ].get ("_zhir_threshold_met" , False ),
16261826 "threshold_unknown" : G .nodes [node ].get ("_zhir_threshold_unknown" , False ),
1827+ # === PHASE TRANSFORMATION (ENHANCED) ===
1828+ "theta_regime_before" : regime_before ,
1829+ "theta_regime_after" : regime_after ,
1830+ "regime_changed" : regime_changed or (regime_before != regime_after ),
1831+ "theta_regime_change" : regime_changed or (regime_before != regime_after ), # Backwards compat
1832+ "regime_before" : regime_before , # Backwards compat
1833+ "regime_after" : regime_after , # Backwards compat
1834+ "theta_shift_direction" : math .copysign (1.0 , theta_shift ),
1835+ "phase_transformation_magnitude" : phase_transformation_magnitude ,
1836+ # === BIFURCATION ANALYSIS (NEW) ===
1837+ "d2epi" : d2epi ,
1838+ "bifurcation_threshold_tau" : tau ,
1839+ "bifurcation_potential" : bifurcation_potential ,
1840+ "bifurcation_score" : bifurcation_score ,
1841+ "bifurcation_triggered" : bifurcation_triggered ,
1842+ "bifurcation_event_count" : bifurcation_event_count ,
1843+ # === EPI METRICS ===
1844+ "delta_epi" : epi_after - epi_before ,
1845+ "epi_before" : epi_before ,
1846+ "epi_after" : epi_after ,
1847+ "epi_final" : epi_after ,
1848+ # === STRUCTURAL PRESERVATION (NEW) ===
1849+ "epi_kind_before" : epi_kind_before ,
1850+ "epi_kind_after" : epi_kind_after ,
1851+ "identity_preserved" : identity_preserved ,
1852+ "delta_vf" : delta_vf ,
1853+ "vf_before" : vf_before if vf_before is not None else vf_after ,
1854+ "vf_final" : vf_after ,
1855+ "delta_dnfr" : delta_dnfr ,
1856+ "dnfr_before" : dnfr_before if dnfr_before is not None else dnfr_after ,
1857+ "dnfr_final" : dnfr_after ,
1858+ # === NETWORK IMPACT (NEW) ===
1859+ "neighbor_count" : neighbor_count ,
1860+ "impacted_neighbors" : impacted_neighbors ,
1861+ "network_impact_radius" : (
1862+ impacted_neighbors / neighbor_count if neighbor_count > 0 else 0.0
1863+ ),
1864+ "phase_coherence_neighbors" : phase_coherence ,
1865+ # === DESTABILIZER CONTEXT (NEW - R4 Extended) ===
1866+ "destabilizer_type" : destabilizer_type ,
1867+ "destabilizer_operator" : destabilizer_operator ,
1868+ "destabilizer_distance" : destabilizer_distance ,
1869+ "recent_history" : recent_history ,
1870+ # === GRAMMAR VALIDATION (NEW) ===
1871+ "grammar_u4b_satisfied" : grammar_u4b_satisfied ,
1872+ "il_precedence_found" : il_precedence_found ,
1873+ "destabilizer_recent" : destabilizer_recent ,
1874+ # === METADATA ===
1875+ "metrics_version" : "2.0_canonical" ,
16271876 }
16281877
16291878
0 commit comments