Vibrational metabolism functions for THOL (Self-organization) operator.
Implements canonical pattern digestion: capturing external network signals and transforming them into internal structural reorganization (ΔNFR and sub-EPIs).
From "El pulso que nos atraviesa" (TNFR Manual, §2.2.10):
"THOL es el glifo de la autoorganización activa. No necesita intervención
externa, ni programación, ni control — su función es reorganizar la forma
desde dentro, en respuesta a la coherencia vibracional del campo."
"THOL no es una propiedad, es una dinámica. No es un atributo de lo vivo,
es lo que hace que algo esté vivo. La autoorganización no es espontaneidad
aleatoria, es resonancia estructurada desde el interior del nodo."This module operationalizes vibrational metabolism:
sub-EPI = base_internal + network_contribution + complexity_bonus
Where:
"""Vibrational metabolism functions for THOL (Self-organization) operator.
Implements canonical pattern digestion: capturing external network signals
and transforming them into internal structural reorganization (ΔNFR and sub-EPIs).
TNFR Canonical Principle
-------------------------
From "El pulso que nos atraviesa" (TNFR Manual, §2.2.10):
"THOL es el glifo de la autoorganización activa. No necesita intervención
externa, ni programación, ni control — su función es reorganizar la forma
desde dentro, en respuesta a la coherencia vibracional del campo."
"THOL no es una propiedad, es una dinámica. No es un atributo de lo vivo,
es lo que hace que algo esté vivo. La autoorganización no es espontaneidad
aleatoria, es resonancia estructurada desde el interior del nodo."
This module operationalizes vibrational metabolism:
1. **Capture**: Sample network environment (EPI gradient, phase variance, coupling)
2. **Metabolize**: Transform external patterns into internal structure (sub-EPIs)
3. **Integrate**: Sub-EPIs reflect both internal acceleration AND network context
Metabolic Formula
-----------------
sub-EPI = base_internal + network_contribution + complexity_bonus
Where:
- base_internal: parent_epi * scaling_factor (internal bifurcation)
- network_contribution: epi_gradient * weight (external pressure)
- complexity_bonus: phase_variance * weight (field complexity)
"""
from __future__ import annotations
import math
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from ..types import NodeId, TNFRGraph
from ..alias import get_attr
from ..constants.aliases import ALIAS_EPI, ALIAS_THETA
from ..mathematics.unified_numerical import np
from ..utils.numeric import angle_diff
__all__ = [
"capture_network_signals",
"metabolize_signals_into_subepi",
"propagate_subepi_to_network",
"compute_cascade_depth",
"compute_hierarchical_depth",
"compute_propagation_radius",
"compute_subepi_collective_coherence",
"compute_metabolic_activity_index",
]
def capture_network_signals(G: TNFRGraph, node: NodeId) -> dict[str, Any] | None:
"""Capture external vibrational patterns from coupled neighbors.
This function implements the "perception" phase of THOL's vibrational metabolism.
It samples the network environment around the target node, computing structural
gradients, phase variance, and coupling strength.
Parameters
----------
G : TNFRGraph
Graph containing the node and its network context
node : NodeId
Node performing metabolic capture
Returns
-------
dict | None
Network signal structure containing:
- epi_gradient: Difference between mean neighbor EPI and node EPI
- phase_variance: Variance of neighbor phases (instability indicator)
- neighbor_count: Number of coupled neighbors
- coupling_strength_mean: Average phase alignment with neighbors
- mean_neighbor_epi: Mean EPI value of neighbors
Returns None if node has no neighbors (isolated metabolism).
Notes
-----
TNFR Principle: THOL doesn't operate in vacuum—it metabolizes the network's
vibrational field. EPI gradient represents "structural pressure" from environment.
Phase variance indicates "complexity" of external patterns to digest.
Examples
--------
>>> # Node with coherent neighbors (low variance)
>>> signals = capture_network_signals(G, node)
>>> signals["phase_variance"] # Low = stable field
0.02
>>> # Node in dissonant field (high variance)
>>> signals = capture_network_signals(G_dissonant, node)
>>> signals["phase_variance"] # High = complex field
0.45
"""
from ..metrics.phase_compatibility import compute_phase_coupling_strength
neighbors = list(G.neighbors(node))
if not neighbors:
return None
node_epi = float(get_attr(G.nodes[node], ALIAS_EPI, 0.0))
node_theta = float(get_attr(G.nodes[node], ALIAS_THETA, 0.0))
# Aggregate neighbor states
neighbor_epis = []
neighbor_thetas = []
coupling_strengths = []
for n in neighbors:
n_epi = float(get_attr(G.nodes[n], ALIAS_EPI, 0.0))
n_theta = float(get_attr(G.nodes[n], ALIAS_THETA, 0.0))
neighbor_epis.append(n_epi)
neighbor_thetas.append(n_theta)
# Coupling strength using canonical phase compatibility formula
# (unified across UM, RA, THOL operators - see phase_compatibility module)
coupling_strength = compute_phase_coupling_strength(node_theta, n_theta)
coupling_strengths.append(coupling_strength)
# Compute structural gradients
mean_neighbor_epi = float(np.mean(neighbor_epis))
epi_gradient = mean_neighbor_epi - node_epi
# Phase variance (complexity/dissonance indicator)
phase_variance = float(np.var(neighbor_thetas))
# Mean coupling strength
coupling_strength_mean = float(np.mean(coupling_strengths))
return {
"epi_gradient": epi_gradient,
"phase_variance": phase_variance,
"neighbor_count": len(neighbors),
"coupling_strength_mean": coupling_strength_mean,
"mean_neighbor_epi": mean_neighbor_epi,
}
def metabolize_signals_into_subepi(
parent_epi: float,
signals: dict[str, Any] | None,
d2_epi: float,
scaling_factor: float = 0.25,
gradient_weight: float = 0.15,
complexity_weight: float = 0.10,
) -> float:
"""Transform external signals into sub-EPI structure through metabolism.
This function implements the "digestion" phase of THOL's vibrational metabolism.
It combines internal acceleration (d²EPI/dt²) with external network pressure
to compute the magnitude of emergent sub-EPI.
Parameters
----------
parent_epi : float
Current EPI magnitude of parent node
signals : dict | None
Network signals captured from environment (from capture_network_signals).
If None, falls back to internal bifurcation only.
d2_epi : float
Internal structural acceleration (∂²EPI/∂t²)
scaling_factor : float, default 0.25
Canonical THOL sub-EPI scaling (0.25 = 25% of parent)
gradient_weight : float, default 0.15
Weight for external EPI gradient contribution
complexity_weight : float, default 0.10
Weight for phase variance complexity bonus
Returns
-------
float
Metabolized sub-EPI magnitude, bounded to [0, 1.0]
Notes
-----
TNFR Metabolic Formula:
sub-EPI = base_internal + network_contribution + complexity_bonus
Where:
- base_internal: parent_epi * scaling_factor (internal bifurcation)
- network_contribution: epi_gradient * weight (external pressure)
- complexity_bonus: phase_variance * weight (field complexity)
This reflects canonical principle: "THOL reorganizes external experience
into internal structure without external instruction" (Manual TNFR, p. 112).
Examples
--------
>>> # Internal bifurcation only (isolated node)
>>> metabolize_signals_into_subepi(0.60, None, d2_epi=0.15)
0.15
>>> # Metabolizing network pressure
>>> signals = {"epi_gradient": 0.20, "phase_variance": 0.10, ...}
>>> metabolize_signals_into_subepi(0.60, signals, d2_epi=0.15)
0.21 # Enhanced by network context
"""
# Base: Internal bifurcation (existing behavior)
base_sub_epi = parent_epi * scaling_factor
# If isolated, return internal bifurcation only
if signals is None:
return float(np.clip(base_sub_epi, 0.0, 1.0))
# Network contribution: EPI gradient pressure
network_contribution = signals["epi_gradient"] * gradient_weight
# Complexity bonus: Phase variance indicates rich field to metabolize
complexity_bonus = signals["phase_variance"] * complexity_weight
# Combine internal + external
metabolized_epi = base_sub_epi + network_contribution + complexity_bonus
# Structural bounds [0, 1]
return float(np.clip(metabolized_epi, 0.0, 1.0))
def propagate_subepi_to_network(
G: TNFRGraph,
parent_node: NodeId,
sub_epi_record: dict[str, Any],
) -> list[tuple[NodeId, float]]:
"""Propagate emergent sub-EPI to coupled neighbors through resonance.
Implements canonical THOL network dynamics: bifurcation creates structures
that propagate through coupled nodes, triggering potential cascades.
Parameters
----------
G : TNFRGraph
Graph containing the network
parent_node : NodeId
Node where sub-EPI originated (bifurcation source)
sub_epi_record : dict
Sub-EPI record from bifurcation, containing:
- "epi": sub-EPI magnitude
- "vf": inherited structural frequency
- "timestamp": creation time
Returns
-------
list of (NodeId, float)
list of (neighbor_id, injected_epi) tuples showing propagation results.
Empty list if no propagation occurred.
Notes
-----
TNFR Principle: "Sub-EPIs propagate to coupled neighbors, triggering their
own bifurcations when ∂²EPI/∂t² > τ" (canonical THOL dynamics).
**AGENTS.md Invariant #2**: No coupling is valid without explicit phase
verification. This function enforces phase compatibility before propagation:
- Computes coupling_strength = 1.0 - (|Δθ| / π) using angle_diff()
- Rejects neighbors with coupling_strength < threshold (antiphase blocked)
- Ensures resonance physics: only phase-aligned nodes receive sub-EPIs
Propagation mechanism:
1. Select neighbors with sufficient coupling (phase alignment)
2. Compute attenuation based on coupling strength
3. Inject attenuated sub-EPI influence into neighbor's EPI
4. Record propagation in graph telemetry
Attenuation prevents unbounded growth while enabling cascades.
Examples
--------
>>> # Create coupled network
>>> G = nx.Graph()
>>> G.add_node(0, epi=0.50, vf=1.0, theta=0.1)
>>> G.add_node(1, epi=0.40, vf=1.0, theta=0.12) # Phase-aligned
>>> G.add_edge(0, 1)
>>> sub_epi = {"epi": 0.15, "vf": 1.1, "timestamp": 10}
>>> propagations = propagate_subepi_to_network(G, node=0, sub_epi_record=sub_epi)
>>> len(propagations) # Number of neighbors reached
1
>>> propagations[0] # (neighbor_id, injected_epi)
(1, 0.105) # 70% attenuation
"""
from ..alias import set_attr
neighbors = list(G.neighbors(parent_node))
if not neighbors:
return []
# Configuration
min_coupling_strength = float(G.graph.get("THOL_MIN_COUPLING_FOR_PROPAGATION", 0.5))
attenuation_factor = float(G.graph.get("THOL_PROPAGATION_ATTENUATION", 0.7))
parent_theta = float(get_attr(G.nodes[parent_node], ALIAS_THETA, 0.0))
sub_epi_magnitude = sub_epi_record["epi"]
propagations = []
for neighbor in neighbors:
neighbor_theta = float(get_attr(G.nodes[neighbor], ALIAS_THETA, 0.0))
# INVARIANT #2: Phase verification before coupling
# Compute coupling strength based on phase alignment
# coupling_strength ∈ [0, 1]: 1 = in-phase, 0 = antiphase
phase_diff = abs(angle_diff(neighbor_theta, parent_theta))
coupling_strength = 1.0 - (phase_diff / math.pi)
# Propagate only if sufficiently coupled (phase-aligned)
# Antiphase neighbors (Δθ ≈ π) have coupling_strength ≈ 0, blocked by threshold
if coupling_strength >= min_coupling_strength:
# Attenuate sub-EPI based on distance and coupling
attenuated_epi = sub_epi_magnitude * attenuation_factor * coupling_strength
# Inject into neighbor's EPI
neighbor_epi = float(get_attr(G.nodes[neighbor], ALIAS_EPI, 0.0))
new_neighbor_epi = neighbor_epi + attenuated_epi
# Boundary check
from ..dynamics.structural_clip import structural_clip
epi_max = float(G.graph.get("EPI_MAX", 1.0))
new_neighbor_epi = structural_clip(new_neighbor_epi, lo=0.0, hi=epi_max)
set_attr(G.nodes[neighbor], ALIAS_EPI, new_neighbor_epi)
propagations.append((neighbor, attenuated_epi))
# Update neighbor's EPI history for potential subsequent bifurcation
history = G.nodes[neighbor].get("epi_history", [])
history.append(new_neighbor_epi)
G.nodes[neighbor]["epi_history"] = history[-10:] # Keep last 10
return propagations
def compute_cascade_depth(G: TNFRGraph, node: NodeId) -> int:
"""Compute maximum hierarchical depth of bifurcation cascade.
Recursively measures how many levels of nested sub-EPIs exist,
where each sub-EPI can itself bifurcate into deeper levels.
With architectural refactor: sub-EPIs are now independent NFR nodes,
enabling true recursive depth computation.
Parameters
----------
G : TNFRGraph
Graph containing bifurcation history
node : NodeId
Root node of cascade analysis
Returns
-------
int
Maximum cascade depth (0 if no bifurcation occurred)
Examples
--------
>>> # Single-level bifurcation
>>> compute_cascade_depth(G, node)
1
>>> # Multi-level cascade (sub-EPIs bifurcated further)
>>> compute_cascade_depth(G_complex, node)
3
Notes
-----
TNFR Principle: Cascade depth measures the hierarchical complexity
of emergent self-organization. Depth = 1 indicates direct bifurcation;
depth > 1 indicates recursive, multi-scale emergence.
ARCHITECTURAL: Now supports true recursive bifurcation with independent
sub-nodes. If a node has no independent sub-nodes, falls back to
metadata-based depth for backward compatibility.
"""
# Primary path: Check for independent sub-nodes (new architecture)
sub_nodes = G.nodes[node].get("sub_nodes", [])
if sub_nodes:
# Recursive computation with actual nodes
max_depth = 0
for sub_node_id in sub_nodes:
if sub_node_id in G.nodes:
# Recurse into child's depth
child_depth = compute_cascade_depth(G, sub_node_id)
max_depth = max(max_depth, 1 + child_depth)
else:
# Child node exists in list but not in graph (shouldn't happen)
max_depth = max(max_depth, 1)
return max_depth
# Fallback path: Legacy metadata-based depth
sub_epis = G.nodes[node].get("sub_epis", [])
if not sub_epis:
return 0
max_depth = 1
for sub in sub_epis:
# Check if sub-EPI has node_id (new architecture with metadata)
if "node_id" in sub and sub["node_id"] in G.nodes:
# Recurse into independent node
child_depth = compute_cascade_depth(G, sub["node_id"])
max_depth = max(max_depth, 1 + child_depth)
else:
# Legacy metadata-only mode
nested_depth = sub.get("cascade_depth", 0)
max_depth = max(max_depth, 1 + nested_depth)
return max_depth
def compute_hierarchical_depth(G: TNFRGraph, node: NodeId) -> int:
"""Compute maximum bifurcation depth from node using recursive traversal.
Traverses sub-EPI hierarchy recursively to find the maximum bifurcation_level
across all nested branches. This provides accurate hierarchical telemetry for
nested THOL bifurcations, supporting operational fractality analysis.
Parameters
----------
G : TNFRGraph
Network graph
node : NodeId
Root node to measure depth from
Returns
-------
int
Maximum bifurcation depth (0 = no bifurcations, 1 = single-level, etc.)
Notes
-----
TNFR Principle: Hierarchical depth reflects operational fractality
(Invariant #3) - the ability of sub-EPIs to bifurcate recursively,
creating multi-scale emergent structures.
This function recursively traverses all branches to find the deepest
bifurcation_level, providing precise depth tracking for:
- Debugging complex nested structures
- Validating depth limits
- Analyzing bifurcation patterns
- Performance monitoring
Examples
--------
>>> # Node with no bifurcations
>>> compute_hierarchical_depth(G, node)
0
>>> # Node with single-level bifurcation
>>> compute_hierarchical_depth(G, node_with_subs)
1
>>> # Node with 2-level nested bifurcation
>>> compute_hierarchical_depth(G, node_nested)
2
"""
sub_epis = G.nodes[node].get("sub_epis", [])
if not sub_epis:
return 0
# Recursively find the maximum bifurcation_level across all branches
max_level = 0
for sub_epi in sub_epis:
# Get this sub-EPI's bifurcation level
level = sub_epi.get("bifurcation_level", 1)
max_level = max(max_level, level)
# Recurse into sub-node if it exists to find deeper levels
sub_node_id = sub_epi.get("node_id")
if sub_node_id and sub_node_id in G.nodes:
# Recursively check this sub-node's depth
sub_depth = compute_hierarchical_depth(G, sub_node_id)
# If sub-node has bifurcations, its depth represents deeper levels
if sub_depth > 0:
max_level = max(max_level, sub_depth)
return max_level
def compute_propagation_radius(G: TNFRGraph) -> int:
"""Count total unique nodes affected by THOL cascades.
Parameters
----------
G : TNFRGraph
Graph with THOL propagation history
Returns
-------
int
Number of nodes reached by at least one propagation event
Notes
-----
TNFR Principle: Propagation radius measures the spatial extent
of cascade effects across the network. High radius indicates
network-wide self-organization.
Examples
--------
>>> # Local cascade (few nodes)
>>> compute_propagation_radius(G_local)
3
>>> # Network-wide cascade
>>> compute_propagation_radius(G_wide)
15
"""
propagations = G.graph.get("thol_propagations", [])
affected_nodes = set()
for prop in propagations:
affected_nodes.add(prop["source_node"])
for target, _ in prop["propagations"]:
affected_nodes.add(target)
return len(affected_nodes)
def compute_subepi_collective_coherence(G: TNFRGraph, node: NodeId) -> float:
"""Calculate coherence of sub-EPI ensemble.
Measures how structurally aligned the emergent sub-EPIs are.
Low variance = high coherence = stable emergence.
Parameters
----------
G : TNFRGraph
Graph containing the node
node : NodeId
Node with sub-EPIs to analyze
Returns
-------
float
Coherence metric [0, 1] where 1 = perfect alignment
Notes
-----
Uses variance-based coherence:
C_sub = 1 / (1 + var(sub_epi_magnitudes))
TNFR Principle: Coherent bifurcation produces sub-EPIs with similar
structural magnitudes, indicating controlled emergence vs chaotic
fragmentation.
Examples
--------
>>> # Coherent bifurcation (similar sub-EPIs)
>>> compute_subepi_collective_coherence(G, node)
0.85
>>> # Chaotic fragmentation (varied sub-EPIs)
>>> compute_subepi_collective_coherence(G_chaotic, node)
0.23
"""
sub_epis = G.nodes[node].get("sub_epis", [])
if len(sub_epis) < 2:
return 0.0 # Need ≥2 sub-EPIs to measure coherence
epi_values = [sub["epi"] for sub in sub_epis]
variance = float(np.var(epi_values))
# Coherence: inverse relationship with variance
coherence = 1.0 / (1.0 + variance)
return coherence
def compute_metabolic_activity_index(G: TNFRGraph, node: NodeId) -> float:
"""Measure proportion of sub-EPIs generated through network metabolism.
Parameters
----------
G : TNFRGraph
Graph containing the node
node : NodeId
Node to analyze
Returns
-------
float
Ratio [0, 1] of metabolized sub-EPIs to total sub-EPIs
1.0 = all sub-EPIs included network context
0.0 = all sub-EPIs were purely internal bifurcations
Notes
-----
TNFR Principle: Metabolic activity measures how much network context
influenced bifurcation. High index indicates external pressure drove
emergence; low index indicates internal acceleration dominated.
Examples
--------
>>> # Network-driven bifurcation
>>> compute_metabolic_activity_index(G_coupled, node)
0.90
>>> # Internal-only bifurcation
>>> compute_metabolic_activity_index(G_isolated, node)
0.0
"""
sub_epis = G.nodes[node].get("sub_epis", [])
if not sub_epis:
return 0.0
metabolized_count = sum(1 for sub in sub_epis if sub.get("metabolized", False))
return metabolized_count / len(sub_epis)