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/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