Skip to content

Commit 2a3c499

Browse files
authored
Merge pull request #2816 from fermga/copilot/refactor-sub-epis-into-nodes
Refactor sub-EPIs as independent nodes for operational fractality
2 parents c154e56 + 4d45914 commit 2a3c499

File tree

3 files changed

+491
-12
lines changed

3 files changed

+491
-12
lines changed

src/tnfr/operators/definitions.py

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2561,6 +2561,10 @@ def _spawn_sub_epi(
25612561
This implements canonical THOL: "reorganizes external experience into
25622562
internal structure without external instruction".
25632563
2564+
ARCHITECTURAL: Sub-EPIs are created as independent NFR nodes to enable
2565+
operational fractality - recursive operator application, hierarchical metrics,
2566+
and multi-level bifurcation.
2567+
25642568
Parameters
25652569
----------
25662570
G : TNFRGraph
@@ -2573,12 +2577,13 @@ def _spawn_sub_epi(
25732577
Bifurcation threshold that was exceeded
25742578
"""
25752579
from ..alias import get_attr, set_attr
2576-
from ..constants.aliases import ALIAS_EPI, ALIAS_VF
2580+
from ..constants.aliases import ALIAS_EPI, ALIAS_VF, ALIAS_THETA
25772581
from .metabolism import capture_network_signals, metabolize_signals_into_subepi
25782582

25792583
# Get current node state
25802584
parent_epi = float(get_attr(G.nodes[node], ALIAS_EPI, 0.0))
25812585
parent_vf = float(get_attr(G.nodes[node], ALIAS_VF, 1.0))
2586+
parent_theta = float(get_attr(G.nodes[node], ALIAS_THETA, 0.0))
25822587

25832588
# Check if vibrational metabolism is enabled
25842589
metabolic_enabled = G.graph.get("THOL_METABOLIC_ENABLED", True)
@@ -2606,24 +2611,33 @@ def _spawn_sub_epi(
26062611
complexity_weight=complexity_weight,
26072612
)
26082613

2609-
# Store sub-EPI in node's sub_epis list
2610-
sub_epis = G.nodes[node].get("sub_epis", [])
2611-
26122614
# Get current timestamp from glyph history length
26132615
timestamp = len(G.nodes[node].get("glyph_history", []))
26142616

2615-
# Store sub-EPI with metabolic metadata
2617+
# ARCHITECTURAL: Create sub-EPI as independent NFR node
2618+
# This enables operational fractality - recursive operators, hierarchical metrics
2619+
sub_node_id = self._create_sub_node(
2620+
G,
2621+
parent_node=node,
2622+
sub_epi=sub_epi_value,
2623+
parent_vf=parent_vf,
2624+
parent_theta=parent_theta,
2625+
)
2626+
2627+
# Store sub-EPI metadata for telemetry and backward compatibility
26162628
sub_epi_record = {
26172629
"epi": sub_epi_value,
26182630
"vf": parent_vf,
26192631
"timestamp": timestamp,
26202632
"d2_epi": d2_epi,
26212633
"tau": tau,
2622-
# NEW: Metabolic metadata
2634+
"node_id": sub_node_id, # Reference to independent node
26232635
"metabolized": network_signals is not None and metabolic_enabled,
26242636
"network_signals": network_signals,
26252637
}
26262638

2639+
# Keep metadata list for telemetry/metrics backward compatibility
2640+
sub_epis = G.nodes[node].get("sub_epis", [])
26272641
sub_epis.append(sub_epi_record)
26282642
G.nodes[node]["sub_epis"] = sub_epis
26292643

@@ -2649,6 +2663,82 @@ def _spawn_sub_epi(
26492663
}
26502664
)
26512665

2666+
def _create_sub_node(
2667+
self,
2668+
G: TNFRGraph,
2669+
parent_node: Any,
2670+
sub_epi: float,
2671+
parent_vf: float,
2672+
parent_theta: float,
2673+
) -> str:
2674+
"""Create sub-EPI as independent NFR node for operational fractality.
2675+
2676+
Sub-nodes are full TNFR nodes that can have operators applied, bifurcate
2677+
recursively, and contribute to hierarchical metrics.
2678+
2679+
Parameters
2680+
----------
2681+
G : TNFRGraph
2682+
Graph containing the parent node
2683+
parent_node : Any
2684+
Parent node identifier
2685+
sub_epi : float
2686+
EPI value for the sub-node
2687+
parent_vf : float
2688+
Parent's structural frequency (inherited with damping)
2689+
parent_theta : float
2690+
Parent's phase (inherited)
2691+
2692+
Returns
2693+
-------
2694+
str
2695+
Identifier of the newly created sub-node
2696+
"""
2697+
from ..constants import EPI_PRIMARY, VF_PRIMARY, THETA_PRIMARY, DNFR_PRIMARY
2698+
from ..dynamics import set_delta_nfr_hook
2699+
2700+
# Generate unique sub-node ID
2701+
sub_nodes_list = G.nodes[parent_node].get("sub_nodes", [])
2702+
sub_index = len(sub_nodes_list)
2703+
sub_node_id = f"{parent_node}_sub_{sub_index}"
2704+
2705+
# Get parent hierarchy level
2706+
parent_hierarchy_level = G.nodes[parent_node].get("hierarchy_level", 0)
2707+
2708+
# Inherit parent's vf with slight damping (canonical: 95%)
2709+
sub_vf = parent_vf * 0.95
2710+
2711+
# Create the sub-node with full TNFR state
2712+
G.add_node(
2713+
sub_node_id,
2714+
**{
2715+
EPI_PRIMARY: float(sub_epi),
2716+
VF_PRIMARY: float(sub_vf),
2717+
THETA_PRIMARY: float(parent_theta),
2718+
DNFR_PRIMARY: 0.0,
2719+
"parent_node": parent_node,
2720+
"hierarchy_level": parent_hierarchy_level + 1,
2721+
"epi_history": [float(sub_epi)], # Initialize history for future bifurcation
2722+
"glyph_history": [],
2723+
},
2724+
)
2725+
2726+
# Ensure ΔNFR hook is set for the sub-node
2727+
# (inherits from graph-level hook, but ensure it's activated)
2728+
if hasattr(G, "graph") and "_delta_nfr_hook" in G.graph:
2729+
# Hook already set at graph level, will apply to sub-node automatically
2730+
pass
2731+
2732+
# Track sub-node in parent
2733+
sub_nodes_list.append(sub_node_id)
2734+
G.nodes[parent_node]["sub_nodes"] = sub_nodes_list
2735+
2736+
# Track hierarchy in graph metadata
2737+
hierarchy = G.graph.setdefault("hierarchy", {})
2738+
hierarchy.setdefault(parent_node, []).append(sub_node_id)
2739+
2740+
return sub_node_id
2741+
26522742
def _validate_preconditions(self, G: TNFRGraph, node: Any) -> None:
26532743
"""Validate THOL-specific preconditions."""
26542744
from .preconditions import validate_self_organization

src/tnfr/operators/metabolism.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,9 @@ def compute_cascade_depth(G: TNFRGraph, node: NodeId) -> int:
341341
Recursively measures how many levels of nested sub-EPIs exist,
342342
where each sub-EPI can itself bifurcate into deeper levels.
343343
344+
With architectural refactor: sub-EPIs are now independent NFR nodes,
345+
enabling true recursive depth computation.
346+
344347
Parameters
345348
----------
346349
G : TNFRGraph
@@ -368,19 +371,42 @@ def compute_cascade_depth(G: TNFRGraph, node: NodeId) -> int:
368371
TNFR Principle: Cascade depth measures the hierarchical complexity
369372
of emergent self-organization. Depth = 1 indicates direct bifurcation;
370373
depth > 1 indicates recursive, multi-scale emergence.
374+
375+
ARCHITECTURAL: Now supports true recursive bifurcation with independent
376+
sub-nodes. If a node has no independent sub-nodes, falls back to
377+
metadata-based depth for backward compatibility.
371378
"""
379+
# Primary path: Check for independent sub-nodes (new architecture)
380+
sub_nodes = G.nodes[node].get("sub_nodes", [])
381+
if sub_nodes:
382+
# Recursive computation with actual nodes
383+
max_depth = 0
384+
for sub_node_id in sub_nodes:
385+
if sub_node_id in G.nodes:
386+
# Recurse into child's depth
387+
child_depth = compute_cascade_depth(G, sub_node_id)
388+
max_depth = max(max_depth, 1 + child_depth)
389+
else:
390+
# Child node exists in list but not in graph (shouldn't happen)
391+
max_depth = max(max_depth, 1)
392+
return max_depth
393+
394+
# Fallback path: Legacy metadata-based depth
372395
sub_epis = G.nodes[node].get("sub_epis", [])
373396
if not sub_epis:
374397
return 0
375398

376-
# For now: depth = 1 (direct children)
377-
# TODO: If sub-EPIs become independent nodes, recurse
378399
max_depth = 1
379-
380400
for sub in sub_epis:
381-
# If sub-EPI spawned its own children (future enhancement)
382-
nested_depth = sub.get("cascade_depth", 0)
383-
max_depth = max(max_depth, 1 + nested_depth)
401+
# Check if sub-EPI has node_id (new architecture with metadata)
402+
if "node_id" in sub and sub["node_id"] in G.nodes:
403+
# Recurse into independent node
404+
child_depth = compute_cascade_depth(G, sub["node_id"])
405+
max_depth = max(max_depth, 1 + child_depth)
406+
else:
407+
# Legacy metadata-only mode
408+
nested_depth = sub.get("cascade_depth", 0)
409+
max_depth = max(max_depth, 1 + nested_depth)
384410

385411
return max_depth
386412

0 commit comments

Comments
 (0)