tnfr 3.0.3__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tnfr might be problematic. Click here for more details.
- tnfr/__init__.py +57 -56
- tnfr/cli.py +177 -0
- tnfr/constants.py +41 -11
- tnfr/dynamics.py +87 -31
- tnfr/gamma.py +91 -0
- tnfr/grammar.py +149 -0
- tnfr/helpers.py +43 -15
- tnfr/main.py +20 -10
- tnfr/metrics.py +211 -0
- tnfr/observers.py +19 -7
- tnfr/ontosim.py +12 -9
- tnfr/operators.py +23 -6
- tnfr/presets.py +24 -0
- tnfr/program.py +168 -0
- tnfr/scenarios.py +28 -0
- tnfr/sense.py +215 -0
- tnfr/trace.py +145 -0
- tnfr/types.py +17 -0
- tnfr-4.0.0.dist-info/METADATA +101 -0
- tnfr-4.0.0.dist-info/RECORD +24 -0
- tnfr-4.0.0.dist-info/entry_points.txt +2 -0
- tnfr-3.0.3.dist-info/licenses/LICENSE.txt → tnfr-4.0.0.dist-info/licenses/LICENSE.md +1 -1
- tnfr-3.0.3.dist-info/METADATA +0 -35
- tnfr-3.0.3.dist-info/RECORD +0 -13
- {tnfr-3.0.3.dist-info → tnfr-4.0.0.dist-info}/WHEEL +0 -0
- {tnfr-3.0.3.dist-info → tnfr-4.0.0.dist-info}/top_level.txt +0 -0
tnfr/presets.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from .program import seq, block, wait
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
_PRESETS = {
|
|
6
|
+
"arranque_resonante": seq("A’L", "E’N", "I’L", "R’A", "VA’L", "U’M", wait(3), "SH’A"),
|
|
7
|
+
"mutacion_contenida": seq("A’L", "E’N", block("O’Z", "Z’HIR", "I’L", repeat=2), "R’A", "SH’A"),
|
|
8
|
+
"exploracion_acople": seq(
|
|
9
|
+
"A’L",
|
|
10
|
+
"E’N",
|
|
11
|
+
"I’L",
|
|
12
|
+
"VA’L",
|
|
13
|
+
"U’M",
|
|
14
|
+
block("O’Z", "NA’V", "I’L", repeat=1),
|
|
15
|
+
"R’A",
|
|
16
|
+
"SH’A",
|
|
17
|
+
),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_preset(name: str):
|
|
22
|
+
if name not in _PRESETS:
|
|
23
|
+
raise KeyError(f"Preset no encontrado: {name}")
|
|
24
|
+
return _PRESETS[name]
|
tnfr/program.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
"""program.py — API de secuencias canónicas con T’HOL como primera clase."""
|
|
3
|
+
from typing import Any, Callable, Iterable, List, Optional, Sequence, Tuple, Union
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
|
|
7
|
+
from .constants import DEFAULTS
|
|
8
|
+
from .helpers import register_callback
|
|
9
|
+
from .grammar import enforce_canonical_grammar, on_applied_glifo
|
|
10
|
+
from .operators import aplicar_glifo
|
|
11
|
+
from .sense import GLYPHS_CANONICAL
|
|
12
|
+
|
|
13
|
+
# Tipos básicos
|
|
14
|
+
Glyph = str
|
|
15
|
+
Node = Any
|
|
16
|
+
AdvanceFn = Callable[[Any], None] # normalmente dynamics.step
|
|
17
|
+
|
|
18
|
+
# ---------------------
|
|
19
|
+
# Construcciones del DSL
|
|
20
|
+
# ---------------------
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class WAIT:
|
|
24
|
+
steps: int = 1
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class TARGET:
|
|
28
|
+
nodes: Optional[Iterable[Node]] = None # None = todos los nodos
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class THOL:
|
|
32
|
+
body: Sequence[Any]
|
|
33
|
+
repeat: int = 1 # cuántas veces repetir el cuerpo
|
|
34
|
+
force_close: Optional[Glyph] = None # None → cierre automático (gramática); 'SH’A' o 'NU’L' para forzar
|
|
35
|
+
|
|
36
|
+
Token = Union[Glyph, WAIT, TARGET, THOL]
|
|
37
|
+
|
|
38
|
+
# ---------------------
|
|
39
|
+
# Utilidades internas
|
|
40
|
+
# ---------------------
|
|
41
|
+
|
|
42
|
+
@contextmanager
|
|
43
|
+
def _forced_selector(G, glyph: Glyph):
|
|
44
|
+
"""Sobrescribe temporalmente el selector glífico para forzar `glyph`.
|
|
45
|
+
Pasa por la gramática canónica antes de aplicar.
|
|
46
|
+
"""
|
|
47
|
+
prev = G.graph.get("glyph_selector")
|
|
48
|
+
def selector_forced(_G, _n):
|
|
49
|
+
return glyph
|
|
50
|
+
G.graph["glyph_selector"] = selector_forced
|
|
51
|
+
try:
|
|
52
|
+
yield
|
|
53
|
+
finally:
|
|
54
|
+
if prev is None:
|
|
55
|
+
G.graph.pop("glyph_selector", None)
|
|
56
|
+
else:
|
|
57
|
+
G.graph["glyph_selector"] = prev
|
|
58
|
+
|
|
59
|
+
def _window(G) -> int:
|
|
60
|
+
return int(G.graph.get("GLYPH_HYSTERESIS_WINDOW", DEFAULTS.get("GLYPH_HYSTERESIS_WINDOW", 1)))
|
|
61
|
+
|
|
62
|
+
def _all_nodes(G):
|
|
63
|
+
return list(G.nodes())
|
|
64
|
+
|
|
65
|
+
# ---------------------
|
|
66
|
+
# Núcleo de ejecución
|
|
67
|
+
# ---------------------
|
|
68
|
+
|
|
69
|
+
def _apply_glyph_to_targets(G, g: Glyph, nodes: Optional[Iterable[Node]] = None):
|
|
70
|
+
nodes = list(nodes) if nodes is not None else _all_nodes(G)
|
|
71
|
+
w = _window(G)
|
|
72
|
+
# Pasamos por la gramática antes de aplicar
|
|
73
|
+
for n in nodes:
|
|
74
|
+
g_eff = enforce_canonical_grammar(G, n, g)
|
|
75
|
+
aplicar_glifo(G, n, g_eff, window=w)
|
|
76
|
+
on_applied_glifo(G, n, g_eff)
|
|
77
|
+
|
|
78
|
+
def _advance(G, step_fn: Optional[AdvanceFn] = None):
|
|
79
|
+
if step_fn is None:
|
|
80
|
+
from .dynamics import step as step_fn
|
|
81
|
+
step_fn(G)
|
|
82
|
+
|
|
83
|
+
# ---------------------
|
|
84
|
+
# Compilación de secuencia → lista de operaciones atómicas
|
|
85
|
+
# ---------------------
|
|
86
|
+
|
|
87
|
+
def _flatten(seq: Sequence[Token], current_target: Optional[TARGET] = None) -> List[Tuple[str, Any]]:
|
|
88
|
+
"""Devuelve lista de operaciones (op, payload).
|
|
89
|
+
op ∈ { 'GLYPH', 'WAIT', 'TARGET' }.
|
|
90
|
+
"""
|
|
91
|
+
ops: List[Tuple[str, Any]] = []
|
|
92
|
+
for item in seq:
|
|
93
|
+
if isinstance(item, TARGET):
|
|
94
|
+
ops.append(("TARGET", item))
|
|
95
|
+
elif isinstance(item, WAIT):
|
|
96
|
+
ops.append(("WAIT", item.steps))
|
|
97
|
+
elif isinstance(item, THOL):
|
|
98
|
+
# abrir bloque T’HOL
|
|
99
|
+
ops.append(("GLYPH", "T’HOL"))
|
|
100
|
+
for _ in range(max(1, int(item.repeat))):
|
|
101
|
+
ops.extend(_flatten(item.body, current_target))
|
|
102
|
+
# cierre explícito si se pidió; si no, la gramática puede cerrarlo
|
|
103
|
+
if item.force_close in ("SH’A", "NU’L"):
|
|
104
|
+
ops.append(("GLYPH", item.force_close))
|
|
105
|
+
else:
|
|
106
|
+
# item debería ser un glifo
|
|
107
|
+
g = str(item)
|
|
108
|
+
if g not in GLYPHS_CANONICAL:
|
|
109
|
+
# Permitimos glifos no listados (compat futuros), pero no forzamos
|
|
110
|
+
pass
|
|
111
|
+
ops.append(("GLYPH", g))
|
|
112
|
+
return ops
|
|
113
|
+
|
|
114
|
+
# ---------------------
|
|
115
|
+
# API pública
|
|
116
|
+
# ---------------------
|
|
117
|
+
|
|
118
|
+
def play(G, sequence: Sequence[Token], step_fn: Optional[AdvanceFn] = None) -> None:
|
|
119
|
+
"""Ejecuta una secuencia canónica sobre el grafo `G`.
|
|
120
|
+
|
|
121
|
+
Reglas:
|
|
122
|
+
- Usa `TARGET(nodes=...)` para cambiar el subconjunto de aplicación.
|
|
123
|
+
- `WAIT(k)` avanza k pasos con el selector vigente (no fuerza glifo).
|
|
124
|
+
- `THOL([...], repeat=r, force_close=…)` abre un bloque autoorganizativo,
|
|
125
|
+
repite el cuerpo y (opcional) fuerza cierre con SH’A/NU’L.
|
|
126
|
+
- Los glifos se aplican pasando por `enforce_canonical_grammar`.
|
|
127
|
+
"""
|
|
128
|
+
ops = _flatten(sequence)
|
|
129
|
+
curr_target: Optional[Iterable[Node]] = None
|
|
130
|
+
|
|
131
|
+
# Traza de programa en history
|
|
132
|
+
if "history" not in G.graph:
|
|
133
|
+
G.graph["history"] = {}
|
|
134
|
+
trace = G.graph["history"].setdefault("program_trace", [])
|
|
135
|
+
|
|
136
|
+
for op, payload in ops:
|
|
137
|
+
if op == "TARGET":
|
|
138
|
+
curr_target = list(payload.nodes) if payload.nodes is not None else None
|
|
139
|
+
trace.append({"t": float(G.graph.get("_t", 0.0)), "op": "TARGET", "n": len(curr_target or _all_nodes(G))})
|
|
140
|
+
continue
|
|
141
|
+
if op == "WAIT":
|
|
142
|
+
for _ in range(max(1, int(payload))):
|
|
143
|
+
_advance(G, step_fn)
|
|
144
|
+
trace.append({"t": float(G.graph.get("_t", 0.0)), "op": "WAIT", "k": int(payload)})
|
|
145
|
+
continue
|
|
146
|
+
if op == "GLYPH":
|
|
147
|
+
g = str(payload)
|
|
148
|
+
# aplicar + avanzar 1 paso del sistema
|
|
149
|
+
_apply_glyph_to_targets(G, g, curr_target)
|
|
150
|
+
_advance(G, step_fn)
|
|
151
|
+
trace.append({"t": float(G.graph.get("_t", 0.0)), "op": "GLYPH", "g": g})
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
# ---------------------
|
|
155
|
+
# Helpers para construir secuencias de manera cómoda
|
|
156
|
+
# ---------------------
|
|
157
|
+
|
|
158
|
+
def seq(*tokens: Token) -> List[Token]:
|
|
159
|
+
return list(tokens)
|
|
160
|
+
|
|
161
|
+
def block(*tokens: Token, repeat: int = 1, close: Optional[Glyph] = None) -> THOL:
|
|
162
|
+
return THOL(body=list(tokens), repeat=repeat, force_close=close)
|
|
163
|
+
|
|
164
|
+
def target(nodes: Optional[Iterable[Node]] = None) -> TARGET:
|
|
165
|
+
return TARGET(nodes=nodes)
|
|
166
|
+
|
|
167
|
+
def wait(steps: int = 1) -> WAIT:
|
|
168
|
+
return WAIT(steps=max(1, int(steps)))
|
tnfr/scenarios.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
3
|
+
import random
|
|
4
|
+
import networkx as nx
|
|
5
|
+
|
|
6
|
+
from .constants import inject_defaults, DEFAULTS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_graph(n: int = 24, topology: str = "ring", seed: int | None = 1):
|
|
10
|
+
rng = random.Random(seed)
|
|
11
|
+
if topology == "ring":
|
|
12
|
+
G = nx.cycle_graph(n)
|
|
13
|
+
elif topology == "complete":
|
|
14
|
+
G = nx.complete_graph(n)
|
|
15
|
+
elif topology == "erdos":
|
|
16
|
+
G = nx.gnp_random_graph(n, 3.0 / n, seed=seed)
|
|
17
|
+
else:
|
|
18
|
+
G = nx.path_graph(n)
|
|
19
|
+
|
|
20
|
+
for i in G.nodes():
|
|
21
|
+
nd = G.nodes[i]
|
|
22
|
+
nd.setdefault("EPI", rng.uniform(0.1, 0.3))
|
|
23
|
+
nd.setdefault("νf", rng.uniform(0.8, 1.2))
|
|
24
|
+
nd.setdefault("θ", rng.uniform(-3.1416, 3.1416))
|
|
25
|
+
nd.setdefault("Si", rng.uniform(0.4, 0.7))
|
|
26
|
+
|
|
27
|
+
inject_defaults(G, DEFAULTS)
|
|
28
|
+
return G
|
tnfr/sense.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Dict, Any, List, Tuple
|
|
3
|
+
import math
|
|
4
|
+
from collections import Counter
|
|
5
|
+
|
|
6
|
+
from .constants import DEFAULTS, ALIAS_SI, ALIAS_EPI
|
|
7
|
+
from .helpers import _get_attr, clamp01, register_callback
|
|
8
|
+
|
|
9
|
+
# -------------------------
|
|
10
|
+
# Canon: orden circular de glifos y ángulos
|
|
11
|
+
# -------------------------
|
|
12
|
+
GLYPHS_CANONICAL: List[str] = [
|
|
13
|
+
"A’L", # 0
|
|
14
|
+
"E’N", # 1
|
|
15
|
+
"I’L", # 2
|
|
16
|
+
"U’M", # 3
|
|
17
|
+
"R’A", # 4
|
|
18
|
+
"VA’L", # 5
|
|
19
|
+
"O’Z", # 6
|
|
20
|
+
"Z’HIR",# 7
|
|
21
|
+
"NA’V", # 8
|
|
22
|
+
"T’HOL",# 9
|
|
23
|
+
"NU’L", #10
|
|
24
|
+
"SH’A", #11
|
|
25
|
+
"RE’MESH" #12
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
_SIGMA_ANGLES: Dict[str, float] = {g: (2.0*math.pi * i / len(GLYPHS_CANONICAL)) for i, g in enumerate(GLYPHS_CANONICAL)}
|
|
29
|
+
|
|
30
|
+
# -------------------------
|
|
31
|
+
# Config por defecto
|
|
32
|
+
# -------------------------
|
|
33
|
+
DEFAULTS.setdefault("SIGMA", {
|
|
34
|
+
"enabled": True,
|
|
35
|
+
"weight": "Si", # "Si" | "EPI" | "1"
|
|
36
|
+
"smooth": 0.0, # EMA sobre el vector global (0=off)
|
|
37
|
+
"history_key": "sigma_global", # dónde guardar en G.graph['history']
|
|
38
|
+
"per_node": False, # si True, guarda trayectoria σ por nodo (más pesado)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
# -------------------------
|
|
42
|
+
# Utilidades básicas
|
|
43
|
+
# -------------------------
|
|
44
|
+
|
|
45
|
+
def glyph_angle(g: str) -> float:
|
|
46
|
+
return float(_SIGMA_ANGLES.get(g, 0.0))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def glyph_unit(g: str) -> complex:
|
|
50
|
+
a = glyph_angle(g)
|
|
51
|
+
return complex(math.cos(a), math.sin(a))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _weight(G, n, mode: str) -> float:
|
|
55
|
+
nd = G.nodes[n]
|
|
56
|
+
if mode == "Si":
|
|
57
|
+
return clamp01(_get_attr(nd, ALIAS_SI, 0.5))
|
|
58
|
+
if mode == "EPI":
|
|
59
|
+
return max(0.0, float(_get_attr(nd, ALIAS_EPI, 0.0)))
|
|
60
|
+
return 1.0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _last_glifo(nd: Dict[str, Any]) -> str | None:
|
|
64
|
+
hist = nd.get("hist_glifos")
|
|
65
|
+
if not hist:
|
|
66
|
+
return None
|
|
67
|
+
try:
|
|
68
|
+
return list(hist)[-1]
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# -------------------------
|
|
74
|
+
# σ por nodo y σ global
|
|
75
|
+
# -------------------------
|
|
76
|
+
|
|
77
|
+
def sigma_vector_node(G, n, weight_mode: str | None = None) -> Dict[str, float] | None:
|
|
78
|
+
nd = G.nodes[n]
|
|
79
|
+
g = _last_glifo(nd)
|
|
80
|
+
if g is None:
|
|
81
|
+
return None
|
|
82
|
+
w = _weight(G, n, weight_mode or G.graph.get("SIGMA", DEFAULTS["SIGMA"]).get("weight", "Si"))
|
|
83
|
+
z = glyph_unit(g) * w
|
|
84
|
+
x, y = z.real, z.imag
|
|
85
|
+
mag = math.hypot(x, y)
|
|
86
|
+
ang = math.atan2(y, x) if mag > 0 else glyph_angle(g)
|
|
87
|
+
return {"x": float(x), "y": float(y), "mag": float(mag), "angle": float(ang), "glifo": g, "w": float(w)}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def sigma_vector_global(G, weight_mode: str | None = None) -> Dict[str, float]:
|
|
91
|
+
"""Vector global del plano del sentido σ.
|
|
92
|
+
|
|
93
|
+
Mapea el último glifo de cada nodo a un vector unitario en S¹, ponderado
|
|
94
|
+
por `Si` (o `EPI`/1), y promedia para obtener:
|
|
95
|
+
- componentes (x, y), magnitud |σ| y ángulo arg(σ).
|
|
96
|
+
|
|
97
|
+
Interpretación TNFR: |σ| mide cuán alineada está la red en su
|
|
98
|
+
**recorrido glífico**; arg(σ) indica la **dirección funcional** dominante
|
|
99
|
+
(p. ej., torno a I’L/RA para consolidación/distribución, O’Z/Z’HIR para cambio).
|
|
100
|
+
"""
|
|
101
|
+
cfg = G.graph.get("SIGMA", DEFAULTS["SIGMA"])
|
|
102
|
+
weight_mode = weight_mode or cfg.get("weight", "Si")
|
|
103
|
+
acc = complex(0.0, 0.0)
|
|
104
|
+
cnt = 0
|
|
105
|
+
for n in G.nodes():
|
|
106
|
+
v = sigma_vector_node(G, n, weight_mode)
|
|
107
|
+
if v is None:
|
|
108
|
+
continue
|
|
109
|
+
acc += complex(v["x"], v["y"])
|
|
110
|
+
cnt += 1
|
|
111
|
+
if cnt == 0:
|
|
112
|
+
return {"x": 1.0, "y": 0.0, "mag": 1.0, "angle": 0.0, "n": 0}
|
|
113
|
+
x, y = acc.real / max(1, cnt), acc.imag / max(1, cnt)
|
|
114
|
+
mag = math.hypot(x, y)
|
|
115
|
+
ang = math.atan2(y, x)
|
|
116
|
+
return {"x": float(x), "y": float(y), "mag": float(mag), "angle": float(ang), "n": cnt}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# -------------------------
|
|
120
|
+
# Historia / series
|
|
121
|
+
# -------------------------
|
|
122
|
+
|
|
123
|
+
def _ensure_history(G):
|
|
124
|
+
if "history" not in G.graph:
|
|
125
|
+
G.graph["history"] = {}
|
|
126
|
+
return G.graph["history"]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def push_sigma_snapshot(G, t: float | None = None) -> None:
|
|
130
|
+
cfg = G.graph.get("SIGMA", DEFAULTS["SIGMA"])
|
|
131
|
+
if not cfg.get("enabled", True):
|
|
132
|
+
return
|
|
133
|
+
hist = _ensure_history(G)
|
|
134
|
+
key = cfg.get("history_key", "sigma_global")
|
|
135
|
+
|
|
136
|
+
# Global
|
|
137
|
+
sv = sigma_vector_global(G, cfg.get("weight", "Si"))
|
|
138
|
+
|
|
139
|
+
# Suavizado exponencial (EMA) opcional
|
|
140
|
+
alpha = float(cfg.get("smooth", 0.0))
|
|
141
|
+
if alpha > 0 and hist.get(key):
|
|
142
|
+
prev = hist[key][-1]
|
|
143
|
+
x = (1-alpha)*prev["x"] + alpha*sv["x"]
|
|
144
|
+
y = (1-alpha)*prev["y"] + alpha*sv["y"]
|
|
145
|
+
mag = math.hypot(x, y)
|
|
146
|
+
ang = math.atan2(y, x)
|
|
147
|
+
sv = {"x": x, "y": y, "mag": mag, "angle": ang, "n": sv.get("n", 0)}
|
|
148
|
+
|
|
149
|
+
sv["t"] = float(G.graph.get("_t", 0.0) if t is None else t)
|
|
150
|
+
|
|
151
|
+
hist.setdefault(key, []).append(sv)
|
|
152
|
+
|
|
153
|
+
# Conteo de glifos por paso (útil para rosa glífica)
|
|
154
|
+
counts = Counter()
|
|
155
|
+
for n in G.nodes():
|
|
156
|
+
g = _last_glifo(G.nodes[n])
|
|
157
|
+
if g:
|
|
158
|
+
counts[g] += 1
|
|
159
|
+
hist.setdefault("sigma_counts", []).append({"t": sv["t"], **counts})
|
|
160
|
+
|
|
161
|
+
# Trayectoria por nodo (opcional)
|
|
162
|
+
if cfg.get("per_node", False):
|
|
163
|
+
per = hist.setdefault("sigma_per_node", {})
|
|
164
|
+
for n in G.nodes():
|
|
165
|
+
nd = G.nodes[n]
|
|
166
|
+
g = _last_glifo(nd)
|
|
167
|
+
if not g:
|
|
168
|
+
continue
|
|
169
|
+
a = glyph_angle(g)
|
|
170
|
+
d = per.setdefault(n, [])
|
|
171
|
+
d.append({"t": sv["t"], "g": g, "angle": a})
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# -------------------------
|
|
175
|
+
# Registro como callback automático (after_step)
|
|
176
|
+
# -------------------------
|
|
177
|
+
|
|
178
|
+
def register_sigma_callback(G) -> None:
|
|
179
|
+
register_callback(G, when="after_step", func=push_sigma_snapshot, name="sigma_snapshot")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# -------------------------
|
|
183
|
+
# Series de utilidad
|
|
184
|
+
# -------------------------
|
|
185
|
+
|
|
186
|
+
def sigma_series(G, key: str | None = None) -> Dict[str, List[float]]:
|
|
187
|
+
cfg = G.graph.get("SIGMA", DEFAULTS["SIGMA"])
|
|
188
|
+
key = key or cfg.get("history_key", "sigma_global")
|
|
189
|
+
hist = G.graph.get("history", {})
|
|
190
|
+
xs = hist.get(key, [])
|
|
191
|
+
if not xs:
|
|
192
|
+
return {"t": [], "angle": [], "mag": []}
|
|
193
|
+
return {
|
|
194
|
+
"t": [float(x.get("t", i)) for i, x in enumerate(xs)],
|
|
195
|
+
"angle": [float(x["angle"]) for x in xs],
|
|
196
|
+
"mag": [float(x["mag"]) for x in xs],
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def sigma_rose(G, steps: int | None = None) -> Dict[str, int]:
|
|
201
|
+
"""Histograma de glifos en los últimos `steps` pasos (o todos)."""
|
|
202
|
+
hist = G.graph.get("history", {})
|
|
203
|
+
counts = hist.get("sigma_counts", [])
|
|
204
|
+
if not counts:
|
|
205
|
+
return {g: 0 for g in GLYPHS_CANONICAL}
|
|
206
|
+
if steps is None or steps >= len(counts):
|
|
207
|
+
agg = Counter()
|
|
208
|
+
for row in counts:
|
|
209
|
+
agg.update({k: v for k, v in row.items() if k != "t"})
|
|
210
|
+
out = {g: int(agg.get(g, 0)) for g in GLYPHS_CANONICAL}
|
|
211
|
+
return out
|
|
212
|
+
agg = Counter()
|
|
213
|
+
for row in counts[-int(steps):]:
|
|
214
|
+
agg.update({k: v for k, v in row.items() if k != "t"})
|
|
215
|
+
return {g: int(agg.get(g, 0)) for g in GLYPHS_CANONICAL}
|
tnfr/trace.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
from collections import Counter
|
|
4
|
+
|
|
5
|
+
from .constants import DEFAULTS
|
|
6
|
+
from .helpers import register_callback
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from .gamma import kuramoto_R_psi
|
|
10
|
+
except Exception: # pragma: no cover
|
|
11
|
+
def kuramoto_R_psi(G):
|
|
12
|
+
return 0.0, 0.0
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from .sense import sigma_vector_global
|
|
16
|
+
except Exception: # pragma: no cover
|
|
17
|
+
def sigma_vector_global(G, *args, **kwargs):
|
|
18
|
+
return {"x": 1.0, "y": 0.0, "mag": 1.0, "angle": 0.0, "n": 0}
|
|
19
|
+
|
|
20
|
+
# -------------------------
|
|
21
|
+
# Defaults
|
|
22
|
+
# -------------------------
|
|
23
|
+
DEFAULTS.setdefault("TRACE", {
|
|
24
|
+
"enabled": True,
|
|
25
|
+
"capture": ["gamma", "grammar", "selector", "dnfr_mix", "callbacks", "thol_state", "sigma", "kuramoto", "glifo_counts"],
|
|
26
|
+
"history_key": "trace_meta",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
# -------------------------
|
|
30
|
+
# Helpers
|
|
31
|
+
# -------------------------
|
|
32
|
+
|
|
33
|
+
def _ensure_history(G):
|
|
34
|
+
if "history" not in G.graph:
|
|
35
|
+
G.graph["history"] = {}
|
|
36
|
+
return G.graph["history"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _last_glifo(nd: Dict[str, Any]) -> str | None:
|
|
40
|
+
h = nd.get("hist_glifos")
|
|
41
|
+
if not h:
|
|
42
|
+
return None
|
|
43
|
+
try:
|
|
44
|
+
return list(h)[-1]
|
|
45
|
+
except Exception:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# -------------------------
|
|
50
|
+
# Snapshots
|
|
51
|
+
# -------------------------
|
|
52
|
+
|
|
53
|
+
def _trace_before(G, *args, **kwargs):
|
|
54
|
+
if not G.graph.get("TRACE", DEFAULTS["TRACE"]).get("enabled", True):
|
|
55
|
+
return
|
|
56
|
+
cfg = G.graph.get("TRACE", DEFAULTS["TRACE"])
|
|
57
|
+
capture: List[str] = list(cfg.get("capture", []))
|
|
58
|
+
hist = _ensure_history(G)
|
|
59
|
+
key = cfg.get("history_key", "trace_meta")
|
|
60
|
+
|
|
61
|
+
meta: Dict[str, Any] = {"t": float(G.graph.get("_t", 0.0)), "phase": "before"}
|
|
62
|
+
|
|
63
|
+
if "gamma" in capture:
|
|
64
|
+
meta["gamma"] = dict(G.graph.get("GAMMA", {}))
|
|
65
|
+
|
|
66
|
+
if "grammar" in capture:
|
|
67
|
+
meta["grammar"] = dict(G.graph.get("GRAMMAR_CANON", {}))
|
|
68
|
+
|
|
69
|
+
if "selector" in capture:
|
|
70
|
+
sel = G.graph.get("glyph_selector")
|
|
71
|
+
meta["selector"] = getattr(sel, "__name__", str(sel)) if sel else None
|
|
72
|
+
|
|
73
|
+
if "dnfr_mix" in capture:
|
|
74
|
+
# tratar de capturar varias convenciones posibles
|
|
75
|
+
mix = G.graph.get("DNFR_MIX") or G.graph.get("DELTA_NFR_MIX") or G.graph.get("NFR_MIX")
|
|
76
|
+
meta["dnfr_mix"] = mix if isinstance(mix, dict) else {"value": mix}
|
|
77
|
+
|
|
78
|
+
if "callbacks" in capture:
|
|
79
|
+
# si el motor guarda los callbacks, exponer nombres por fase
|
|
80
|
+
cb = G.graph.get("_callbacks")
|
|
81
|
+
if isinstance(cb, dict):
|
|
82
|
+
out = {k: [getattr(f, "__name__", "fn") for (_, f, *_rest) in v] if isinstance(v, list) else None for k, v in cb.items()}
|
|
83
|
+
meta["callbacks"] = out
|
|
84
|
+
|
|
85
|
+
if "thol_state" in capture:
|
|
86
|
+
# cuántos nodos tienen bloque T’HOL abierto
|
|
87
|
+
th_open = 0
|
|
88
|
+
for n in G.nodes():
|
|
89
|
+
st = G.nodes[n].get("_GRAM", {})
|
|
90
|
+
if st.get("thol_open", False):
|
|
91
|
+
th_open += 1
|
|
92
|
+
meta["thol_open_nodes"] = th_open
|
|
93
|
+
|
|
94
|
+
hist.setdefault(key, []).append(meta)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _trace_after(G, *args, **kwargs):
|
|
98
|
+
if not G.graph.get("TRACE", DEFAULTS["TRACE"]).get("enabled", True):
|
|
99
|
+
return
|
|
100
|
+
cfg = G.graph.get("TRACE", DEFAULTS["TRACE"])
|
|
101
|
+
capture: List[str] = list(cfg.get("capture", []))
|
|
102
|
+
hist = _ensure_history(G)
|
|
103
|
+
key = cfg.get("history_key", "trace_meta")
|
|
104
|
+
|
|
105
|
+
meta: Dict[str, Any] = {"t": float(G.graph.get("_t", 0.0)), "phase": "after"}
|
|
106
|
+
|
|
107
|
+
if "kuramoto" in capture:
|
|
108
|
+
R, psi = kuramoto_R_psi(G)
|
|
109
|
+
meta["kuramoto"] = {"R": float(R), "psi": float(psi)}
|
|
110
|
+
|
|
111
|
+
if "sigma" in capture:
|
|
112
|
+
sv = sigma_vector_global(G)
|
|
113
|
+
meta["sigma"] = {"x": float(sv.get("x", 1.0)), "y": float(sv.get("y", 0.0)), "mag": float(sv.get("mag", 1.0)), "angle": float(sv.get("angle", 0.0))}
|
|
114
|
+
|
|
115
|
+
if "glifo_counts" in capture:
|
|
116
|
+
cnt = Counter()
|
|
117
|
+
for n in G.nodes():
|
|
118
|
+
g = _last_glifo(G.nodes[n])
|
|
119
|
+
if g:
|
|
120
|
+
cnt[g] += 1
|
|
121
|
+
meta["glifos"] = dict(cnt)
|
|
122
|
+
|
|
123
|
+
hist.setdefault(key, []).append(meta)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# -------------------------
|
|
127
|
+
# API
|
|
128
|
+
# -------------------------
|
|
129
|
+
|
|
130
|
+
def register_trace(G) -> None:
|
|
131
|
+
"""Activa snapshots before/after step y vuelca metadatos operativos en history.
|
|
132
|
+
|
|
133
|
+
Guarda en G.graph['history'][TRACE.history_key] una lista de entradas {'phase': 'before'|'after', ...} con:
|
|
134
|
+
- gamma: especificación activa de Γi(R)
|
|
135
|
+
- grammar: configuración de gramática canónica
|
|
136
|
+
- selector: nombre del selector glífico
|
|
137
|
+
- dnfr_mix: mezcla (si el motor la expone en G.graph)
|
|
138
|
+
- callbacks: callbacks registrados por fase (si están en G.graph['_callbacks'])
|
|
139
|
+
- thol_open_nodes: cuántos nodos tienen bloque T’HOL abierto
|
|
140
|
+
- kuramoto: (R, ψ) de la red
|
|
141
|
+
- sigma: vector global del plano del sentido
|
|
142
|
+
- glifos: conteos por glifo tras el paso
|
|
143
|
+
"""
|
|
144
|
+
register_callback(G, when="before_step", func=_trace_before, name="trace_before")
|
|
145
|
+
register_callback(G, when="after_step", func=_trace_after, name="trace_after")
|
tnfr/types.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class NodeState:
|
|
8
|
+
EPI: float = 0.0
|
|
9
|
+
vf: float = 1.0 # νf
|
|
10
|
+
theta: float = 0.0 # θ
|
|
11
|
+
Si: float = 0.5
|
|
12
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
def to_attrs(self) -> Dict[str, Any]:
|
|
15
|
+
d = {"EPI": self.EPI, "νf": self.vf, "θ": self.theta, "Si": self.Si}
|
|
16
|
+
d.update(self.extra)
|
|
17
|
+
return d
|