Skip to content

Commit 119859a

Browse files
authored
Merge pull request #2881 from fermga/copilot/expand-mutation-metrics-function
Expand mutation_metrics() with comprehensive canonical telemetry (46 metrics)
2 parents c1d9b3e + dcde92c commit 119859a

File tree

3 files changed

+697
-31
lines changed

3 files changed

+697
-31
lines changed

src/tnfr/operators/definitions.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3329,7 +3329,14 @@ def _collect_metrics(
33293329
"""Collect ZHIR-specific metrics."""
33303330
from .metrics import mutation_metrics
33313331

3332-
return mutation_metrics(G, node, state_before["theta"], state_before["epi"])
3332+
return mutation_metrics(
3333+
G,
3334+
node,
3335+
state_before["theta"],
3336+
state_before["epi"],
3337+
vf_before=state_before.get("vf"),
3338+
dnfr_before=state_before.get("dnfr"),
3339+
)
33333340

33343341

33353342
@register_operator

src/tnfr/operators/metrics.py

Lines changed: 279 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,12 +1542,23 @@ def self_organization_metrics(
15421542

15431543

15441544
def 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

Comments
 (0)