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 CHANGED
@@ -1,56 +1,57 @@
1
- from __future__ import annotations
2
- """
3
- TNFR — Teoría de la Naturaleza Fractal Resonante
4
- API pública del paquete.
5
-
6
- Ecuación nodal:
7
- ∂EPI/∂t = νf · ΔNFR(t)
8
- """
9
-
10
- __version__ = "3.0.3"
11
-
12
- import sys as _sys
13
-
14
- # ------------------------------------------------------------
15
- # 1) Crear alias para módulos con imports absolutos entre sí
16
- # (los submódulos usan cosas tipo `from constants import ...`)
17
- # Por eso registramos primero: constants → helpers → operators → observers
18
- # ------------------------------------------------------------
19
- from . import constants as _constants
20
- _sys.modules.setdefault("constants", _constants)
21
-
22
- from . import helpers as _helpers
23
- _sys.modules.setdefault("helpers", _helpers)
24
-
25
- from . import operators as _operators
26
- _sys.modules.setdefault("operators", _operators)
27
-
28
- from . import observers as _observers
29
- _sys.modules.setdefault("observers", _observers)
30
-
31
- # ------------------------------------------------------------
32
- # 2) IMPORTAR dynamics y ALIAS antes de ontosim
33
- # (porque ontosim hace `from dynamics import ...`)
34
- # ------------------------------------------------------------
35
- from . import dynamics as _dynamics
36
- _sys.modules.setdefault("dynamics", _dynamics)
37
-
38
- # ------------------------------------------------------------
39
- # 3) Ahora sí, importar ontosim y alias
40
- # ------------------------------------------------------------
41
- from . import ontosim as _ontosim
42
- _sys.modules.setdefault("ontosim", _ontosim)
43
-
44
- # ------------------------------------------------------------
45
- # 4) Re-exports de la API pública
46
- # ------------------------------------------------------------
47
- from .dynamics import step, run, set_delta_nfr_hook
48
- from .ontosim import preparar_red
49
- from .observers import attach_standard_observer, coherencia_global, orden_kuramoto
50
-
51
- __all__ = [
52
- "preparar_red",
53
- "step", "run", "set_delta_nfr_hook",
54
- "attach_standard_observer", "coherencia_global", "orden_kuramoto",
55
- "__version__",
56
- ]
1
+
2
+ from __future__ import annotations
3
+ """
4
+ TNFR Teoría de la Naturaleza Fractal Resonante
5
+ API pública del paquete.
6
+
7
+ Ecuación nodal:
8
+ ∂EPI/∂t = νf · ΔNFR(t)
9
+ """
10
+
11
+ __version__ = "4.0.0"
12
+
13
+ # Re-exports de la API pública
14
+ from .dynamics import step, run, set_delta_nfr_hook
15
+ from .ontosim import preparar_red
16
+ from .observers import attach_standard_observer, coherencia_global, orden_kuramoto
17
+ from .gamma import GAMMA_REGISTRY, eval_gamma, kuramoto_R_psi
18
+ from .grammar import enforce_canonical_grammar, on_applied_glifo
19
+ from .sense import (
20
+ GLYPHS_CANONICAL, glyph_angle, glyph_unit,
21
+ sigma_vector_node, sigma_vector_global,
22
+ push_sigma_snapshot, sigma_series, sigma_rose,
23
+ register_sigma_callback,
24
+ )
25
+ from .metrics import (
26
+ register_metrics_callbacks,
27
+ Tg_global, Tg_by_node,
28
+ latency_series, glifogram_series,
29
+ glyph_top, glyph_dwell_stats,
30
+ )
31
+ from .trace import register_trace
32
+ from .program import play, seq, block, target, wait, THOL, TARGET, WAIT
33
+ from .cli import main as cli_main
34
+ from .scenarios import build_graph
35
+ from .presets import get_preset
36
+ from .types import NodeState
37
+
38
+ __all__ = [
39
+ "preparar_red",
40
+ "step", "run", "set_delta_nfr_hook",
41
+ "attach_standard_observer", "coherencia_global", "orden_kuramoto",
42
+ "GAMMA_REGISTRY", "eval_gamma", "kuramoto_R_psi",
43
+ "enforce_canonical_grammar", "on_applied_glifo",
44
+ "GLYPHS_CANONICAL", "glyph_angle", "glyph_unit",
45
+ "sigma_vector_node", "sigma_vector_global",
46
+ "push_sigma_snapshot", "sigma_series", "sigma_rose",
47
+ "register_sigma_callback",
48
+ "register_metrics_callbacks",
49
+ "register_trace",
50
+ "Tg_global", "Tg_by_node",
51
+ "latency_series", "glifogram_series",
52
+ "glyph_top", "glyph_dwell_stats",
53
+ "play", "seq", "block", "target", "wait", "THOL", "TARGET", "WAIT",
54
+ "__version__",
55
+ ]
56
+
57
+ __all__ += ["cli_main", "build_graph", "get_preset", "NodeState"]
tnfr/cli.py ADDED
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+ import argparse
3
+ import json
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ try: # pragma: no cover - opcional
7
+ import yaml # type: ignore
8
+ except Exception: # pragma: no cover - yaml es opcional
9
+ yaml = None
10
+
11
+ import networkx as nx
12
+
13
+ from .constants import inject_defaults, DEFAULTS
14
+ from .sense import register_sigma_callback, sigma_series, sigma_rose
15
+ from .metrics import (
16
+ register_metrics_callbacks,
17
+ Tg_global,
18
+ latency_series,
19
+ glifogram_series,
20
+ glyph_top,
21
+ )
22
+ from .trace import register_trace
23
+ from .program import play, seq, block, wait, target
24
+ from .dynamics import step, _update_history
25
+ from .scenarios import build_graph
26
+ from .presets import get_preset
27
+
28
+
29
+ def _save_json(path: str, data: Any) -> None:
30
+ with open(path, "w", encoding="utf-8") as f:
31
+ json.dump(data, f, ensure_ascii=False, indent=2)
32
+
33
+
34
+ def _load_sequence(path: str) -> List[Any]:
35
+ with open(path, "r", encoding="utf-8") as f:
36
+ text = f.read()
37
+ if path.endswith(".yaml") or path.endswith(".yml"):
38
+ if not yaml:
39
+ raise RuntimeError("pyyaml no está instalado, usa JSON o instala pyyaml")
40
+ data = yaml.safe_load(text)
41
+ else:
42
+ data = json.loads(text)
43
+
44
+ def parse_token(tok: Any):
45
+ if isinstance(tok, str):
46
+ return tok
47
+ if isinstance(tok, dict):
48
+ if "WAIT" in tok:
49
+ return wait(int(tok["WAIT"]))
50
+ if "TARGET" in tok:
51
+ return target(tok["TARGET"])
52
+ if "THOL" in tok:
53
+ spec = tok["THOL"] or {}
54
+ b = [_parse_inner(x) for x in spec.get("body", [])]
55
+ return block(*b, repeat=int(spec.get("repeat", 1)), close=spec.get("close"))
56
+ raise ValueError(f"Token inválido: {tok}")
57
+
58
+ def _parse_inner(x: Any):
59
+ return parse_token(x)
60
+
61
+ return [parse_token(t) for t in data]
62
+
63
+
64
+ def _attach_callbacks(G: nx.Graph) -> None:
65
+ inject_defaults(G, DEFAULTS)
66
+ register_sigma_callback(G)
67
+ register_metrics_callbacks(G)
68
+ register_trace(G)
69
+ _update_history(G)
70
+
71
+
72
+ def cmd_run(args: argparse.Namespace) -> int:
73
+ G = build_graph(n=args.nodes, topology=args.topology, seed=args.seed)
74
+ _attach_callbacks(G)
75
+
76
+ if args.preset:
77
+ program = get_preset(args.preset)
78
+ play(G, program)
79
+ else:
80
+ steps = int(args.steps or 100)
81
+ for _ in range(steps):
82
+ step(G)
83
+
84
+ if args.save_history:
85
+ _save_json(args.save_history, G.graph.get("history", {}))
86
+
87
+ if args.summary:
88
+ tg = Tg_global(G, normalize=True)
89
+ lat = latency_series(G)
90
+ print("Top glifos por Tg:", glyph_top(G, k=5))
91
+ if lat["value"]:
92
+ print("Latencia media:", sum(lat["value"]) / max(1, len(lat["value"])) )
93
+ return 0
94
+
95
+
96
+ def cmd_sequence(args: argparse.Namespace) -> int:
97
+ G = build_graph(n=args.nodes, topology=args.topology, seed=args.seed)
98
+ _attach_callbacks(G)
99
+
100
+ if args.preset:
101
+ program = get_preset(args.preset)
102
+ elif args.sequence_file:
103
+ program = _load_sequence(args.sequence_file)
104
+ else:
105
+ program = seq("A’L", "E’N", "I’L", block("O’Z", "Z’HIR", "I’L", repeat=1), "R’A", "SH’A")
106
+
107
+ play(G, program)
108
+
109
+ if args.save_history:
110
+ _save_json(args.save_history, G.graph.get("history", {}))
111
+ return 0
112
+
113
+
114
+ def cmd_metrics(args: argparse.Namespace) -> int:
115
+ G = build_graph(n=args.nodes, topology=args.topology, seed=args.seed)
116
+ _attach_callbacks(G)
117
+ for _ in range(int(args.steps or 200)):
118
+ step(G)
119
+
120
+ tg = Tg_global(G, normalize=True)
121
+ lat = latency_series(G)
122
+ rose = sigma_rose(G)
123
+ glifo = glifogram_series(G)
124
+
125
+ out = {
126
+ "Tg_global": tg,
127
+ "latency_mean": (sum(lat["value"]) / max(1, len(lat["value"])) ) if lat["value"] else 0.0,
128
+ "rose": rose,
129
+ "glifogram": {k: v[:10] for k, v in glifo.items()},
130
+ }
131
+ if args.save:
132
+ _save_json(args.save, out)
133
+ else:
134
+ print(json.dumps(out, ensure_ascii=False, indent=2))
135
+ return 0
136
+
137
+
138
+ def main(argv: Optional[List[str]] = None) -> int:
139
+ p = argparse.ArgumentParser(prog="tnfr")
140
+ sub = p.add_subparsers(dest="cmd")
141
+
142
+ p_run = sub.add_parser("run", help="Correr escenario libre o preset y opcionalmente exportar history")
143
+ p_run.add_argument("--nodes", type=int, default=24)
144
+ p_run.add_argument("--topology", choices=["ring", "complete", "erdos"], default="ring")
145
+ p_run.add_argument("--steps", type=int, default=200)
146
+ p_run.add_argument("--seed", type=int, default=1)
147
+ p_run.add_argument("--preset", type=str, default=None)
148
+ p_run.add_argument("--save-history", dest="save_history", type=str, default=None)
149
+ p_run.add_argument("--summary", action="store_true")
150
+ p_run.set_defaults(func=cmd_run)
151
+
152
+ p_seq = sub.add_parser("sequence", help="Ejecutar una secuencia (preset o YAML/JSON)")
153
+ p_seq.add_argument("--nodes", type=int, default=24)
154
+ p_seq.add_argument("--topology", choices=["ring", "complete", "erdos"], default="ring")
155
+ p_seq.add_argument("--seed", type=int, default=1)
156
+ p_seq.add_argument("--preset", type=str, default=None)
157
+ p_seq.add_argument("--sequence-file", type=str, default=None)
158
+ p_seq.add_argument("--save-history", dest="save_history", type=str, default=None)
159
+ p_seq.set_defaults(func=cmd_sequence)
160
+
161
+ p_met = sub.add_parser("metrics", help="Correr breve y volcar métricas clave")
162
+ p_met.add_argument("--nodes", type=int, default=24)
163
+ p_met.add_argument("--topology", choices=["ring", "complete", "erdos"], default="ring")
164
+ p_met.add_argument("--steps", type=int, default=300)
165
+ p_met.add_argument("--seed", type=int, default=1)
166
+ p_met.add_argument("--save", type=str, default=None)
167
+ p_met.set_defaults(func=cmd_metrics)
168
+
169
+ args = p.parse_args(argv)
170
+ if not hasattr(args, "func"):
171
+ p.print_help()
172
+ return 1
173
+ return int(args.func(args))
174
+
175
+
176
+ if __name__ == "__main__": # pragma: no cover
177
+ raise SystemExit(main())
tnfr/constants.py CHANGED
@@ -10,7 +10,7 @@ from typing import Dict, Any
10
10
  # -------------------------
11
11
  # Parámetros canónicos
12
12
  # -------------------------
13
- DEFAULTS: Dict[str, Any] = {
13
+ DEFAULTS: Dict[str, Any] = {
14
14
  # Discretización
15
15
  "DT": 1.0,
16
16
 
@@ -147,22 +147,52 @@ DEFAULTS: Dict[str, Any] = {
147
147
  "dnfr_hi": 0.50, "dnfr_lo": 0.10,
148
148
  "accel_hi": 0.50, "accel_lo": 0.10
149
149
  },
150
- # Callbacks Γ(R)
151
- "CALLBACKS_STRICT": False, # si True, un error en callback detiene; si False, se loguea y continúa
152
- }
150
+ # Callbacks Γ(R)
151
+ "GAMMA": {
152
+ "type": "none", # 'none' | 'kuramoto_linear' | 'kuramoto_bandpass'
153
+ "beta": 0.0,
154
+ "R0": 0.0,
155
+ },
156
+ "CALLBACKS_STRICT": False, # si True, un error en callback detiene; si False, se loguea y continúa
157
+ }
158
+
159
+ # Gramática glífica canónica
160
+ DEFAULTS.setdefault("GRAMMAR_CANON", {
161
+ "enabled": True, # activar la gramática canónica
162
+ "zhir_requires_oz_window": 3, # cuántos pasos atrás buscamos O’Z
163
+ "zhir_dnfr_min": 0.05, # si |ΔNFR|_norm < este valor, no permitimos Z’HIR sin O’Z
164
+ "thol_min_len": 2,
165
+ "thol_max_len": 6,
166
+ "thol_close_dnfr": 0.15, # si el campo calma, cerramos con SH’A/NU’L
167
+ "si_high": 0.66, # umbral para elegir NU’L vs SH’A al cerrar
168
+ })
153
169
 
154
170
 
155
171
  # -------------------------
156
172
  # Utilidades
157
173
  # -------------------------
158
174
 
159
- def attach_defaults(G, override: bool = False) -> None:
160
- """Escribe DEFAULTS en G.graph (sin sobreescribir si override=False)."""
161
- G.graph.setdefault("_tnfr_defaults_attached", False)
162
- for k, v in DEFAULTS.items():
163
- if override or k not in G.graph:
164
- G.graph[k] = v
165
- G.graph["_tnfr_defaults_attached"] = True
175
+ def attach_defaults(G, override: bool = False) -> None:
176
+ """Escribe DEFAULTS en G.graph (sin sobreescribir si override=False)."""
177
+ G.graph.setdefault("_tnfr_defaults_attached", False)
178
+ for k, v in DEFAULTS.items():
179
+ if override or k not in G.graph:
180
+ G.graph[k] = v
181
+ G.graph["_tnfr_defaults_attached"] = True
182
+
183
+
184
+ def inject_defaults(G, defaults: Dict[str, Any] = DEFAULTS, override: bool = False) -> None:
185
+ """Alias de conveniencia para inyectar ``DEFAULTS`` en ``G.graph``.
186
+
187
+ Permite pasar un diccionario de *defaults* alternativo y mantiene la
188
+ semántica de ``attach_defaults`` existente. Si ``override`` es ``True`` se
189
+ sobreescriben valores ya presentes.
190
+ """
191
+ G.graph.setdefault("_tnfr_defaults_attached", False)
192
+ for k, v in defaults.items():
193
+ if override or k not in G.graph:
194
+ G.graph[k] = v
195
+ G.graph["_tnfr_defaults_attached"] = True
166
196
 
167
197
 
168
198
  def merge_overrides(G, **overrides) -> None:
tnfr/dynamics.py CHANGED
@@ -13,11 +13,15 @@ Incluye:
13
13
  from __future__ import annotations
14
14
  from typing import Dict, Any, Iterable
15
15
  import math
16
-
17
- from observers import sincronía_fase, carga_glifica, orden_kuramoto, sigma_vector
18
- from operators import aplicar_remesh_si_estabilizacion_global
19
- from constants import DEFAULTS, ALIAS_VF, ALIAS_THETA, ALIAS_DNFR, ALIAS_EPI, ALIAS_SI, ALIAS_dEPI, ALIAS_D2EPI
20
- from helpers import (
16
+ from collections import deque
17
+ import networkx as nx
18
+
19
+ from .observers import sincronía_fase, carga_glifica, orden_kuramoto, sigma_vector
20
+ from .operators import aplicar_remesh_si_estabilizacion_global
21
+ from .grammar import select_and_apply_with_grammar
22
+ from .constants import DEFAULTS, ALIAS_VF, ALIAS_THETA, ALIAS_DNFR, ALIAS_EPI, ALIAS_SI, ALIAS_dEPI, ALIAS_D2EPI
23
+ from .gamma import eval_gamma
24
+ from .helpers import (
21
25
  clamp, clamp01, list_mean, phase_distance,
22
26
  _get_attr, _set_attr, media_vecinal, fase_media,
23
27
  invoke_callbacks, reciente_glifo
@@ -50,6 +54,7 @@ def _write_dnfr_metadata(G, *, weights: dict, hook_name: str, note: str | None =
50
54
 
51
55
 
52
56
  def default_compute_delta_nfr(G) -> None:
57
+ """Calcula ΔNFR mezclando gradientes de fase, EPI y νf según pesos."""
53
58
  w = G.graph.get("DNFR_WEIGHTS", DEFAULTS["DNFR_WEIGHTS"]) # dict
54
59
  w_phase = float(w.get("phase", 0.34))
55
60
  w_epi = float(w.get("epi", 0.33))
@@ -119,19 +124,49 @@ def dnfr_epi_vf_mixed(G) -> None:
119
124
  # Ecuación nodal
120
125
  # -------------------------
121
126
 
122
- def update_epi_via_nodal_equation(G, *, dt: float = None) -> None:
127
+ def update_epi_via_nodal_equation(G, *, dt: float = None, t: float | None = None) -> None:
128
+ """Ecuación nodal TNFR.
129
+
130
+ Implementa la forma extendida de la ecuación nodal:
131
+ ∂EPI/∂t = νf · ΔNFR(t) + Γi(R)
132
+
133
+ Donde:
134
+ - EPI es la Estructura Primaria de Información del nodo.
135
+ - νf es la frecuencia estructural del nodo (Hz_str).
136
+ - ΔNFR(t) es el gradiente nodal (necesidad de reorganización),
137
+ típicamente una mezcla de componentes (p. ej. fase θ, EPI, νf).
138
+ - Γi(R) es el acoplamiento de red opcional en función del orden de Kuramoto R
139
+ (ver gamma.py), usado para modular la integración en red.
140
+
141
+ Referencias TNFR: ecuación nodal (manual), glosario νf/ΔNFR/EPI, operador Γ.
142
+ Efectos secundarios: cachea dEPI y actualiza EPI por integración explícita.
143
+ """
144
+ if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)):
145
+ raise TypeError("G must be a networkx graph instance")
123
146
  if dt is None:
124
147
  dt = float(G.graph.get("DT", DEFAULTS["DT"]))
148
+ else:
149
+ if not isinstance(dt, (int, float)):
150
+ raise TypeError("dt must be a number")
151
+ if dt < 0:
152
+ raise ValueError("dt must be non-negative")
153
+ dt = float(dt)
154
+ if t is None:
155
+ t = float(G.graph.get("_t", 0.0))
156
+ else:
157
+ t = float(t)
125
158
  for n in G.nodes():
126
159
  nd = G.nodes[n]
127
160
  vf = _get_attr(nd, ALIAS_VF, 0.0)
128
161
  dnfr = _get_attr(nd, ALIAS_DNFR, 0.0)
129
162
  dEPI_dt_prev = _get_attr(nd, ALIAS_dEPI, 0.0)
130
163
  dEPI_dt = vf * dnfr
164
+ dEPI_dt += eval_gamma(G, n, t)
131
165
  epi = _get_attr(nd, ALIAS_EPI, 0.0) + dt * dEPI_dt
132
166
  _set_attr(nd, ALIAS_EPI, epi)
133
167
  _set_attr(nd, ALIAS_dEPI, dEPI_dt)
134
168
  _set_attr(nd, ALIAS_D2EPI, (dEPI_dt - dEPI_dt_prev) / dt if dt != 0 else 0.0)
169
+ G.graph["_t"] = t + dt
135
170
 
136
171
 
137
172
  # -------------------------
@@ -175,20 +210,30 @@ def coordinar_fase_global_vecinal(G, fuerza_global: float | None = None, fuerza_
175
210
  Si no se pasan fuerzas explícitas, adapta kG/kL según estado (disonante / transición / estable).
176
211
  Estado se decide por R (Kuramoto) y carga glífica disruptiva reciente.
177
212
  """
213
+ g = G.graph
214
+ defaults = DEFAULTS
178
215
  # 0) Si hay fuerzas explícitas, usar y salir del modo adaptativo
179
216
  if (fuerza_global is not None) or (fuerza_vecinal is not None):
180
- kG = float(fuerza_global if fuerza_global is not None else G.graph.get("PHASE_K_GLOBAL", DEFAULTS["PHASE_K_GLOBAL"]))
181
- kL = float(fuerza_vecinal if fuerza_vecinal is not None else G.graph.get("PHASE_K_LOCAL", DEFAULTS["PHASE_K_LOCAL"]))
217
+ kG = float(
218
+ fuerza_global
219
+ if fuerza_global is not None
220
+ else g.get("PHASE_K_GLOBAL", defaults["PHASE_K_GLOBAL"])
221
+ )
222
+ kL = float(
223
+ fuerza_vecinal
224
+ if fuerza_vecinal is not None
225
+ else g.get("PHASE_K_LOCAL", defaults["PHASE_K_LOCAL"])
226
+ )
182
227
  else:
183
228
  # 1) Lectura de configuración
184
- cfg = G.graph.get("PHASE_ADAPT", DEFAULTS.get("PHASE_ADAPT", {}))
185
- kG = float(G.graph.get("PHASE_K_GLOBAL", DEFAULTS["PHASE_K_GLOBAL"]))
186
- kL = float(G.graph.get("PHASE_K_LOCAL", DEFAULTS["PHASE_K_LOCAL"]))
229
+ cfg = g.get("PHASE_ADAPT", defaults.get("PHASE_ADAPT", {}))
230
+ kG = float(g.get("PHASE_K_GLOBAL", defaults["PHASE_K_GLOBAL"]))
231
+ kL = float(g.get("PHASE_K_LOCAL", defaults["PHASE_K_LOCAL"]))
187
232
 
188
233
  if bool(cfg.get("enabled", False)):
189
234
  # 2) Métricas actuales (no dependemos de history)
190
235
  R = orden_kuramoto(G)
191
- win = int(G.graph.get("GLYPH_LOAD_WINDOW", DEFAULTS["GLYPH_LOAD_WINDOW"]))
236
+ win = int(g.get("GLYPH_LOAD_WINDOW", defaults["GLYPH_LOAD_WINDOW"]))
192
237
  dist = carga_glifica(G, window=win)
193
238
  disr = float(dist.get("_disruptivos", 0.0)) if dist else 0.0
194
239
 
@@ -216,7 +261,9 @@ def coordinar_fase_global_vecinal(G, fuerza_global: float | None = None, fuerza_
216
261
  kG_t = 0.5 * (kG_min + kG_max)
217
262
  kL_t = 0.5 * (kL_min + kL_max)
218
263
 
219
- up = float(cfg.get("up", 0.10)); down = float(cfg.get("down", 0.07))
264
+ up = float(cfg.get("up", 0.10))
265
+ down = float(cfg.get("down", 0.07))
266
+
220
267
  def _step(curr, target, mn, mx):
221
268
  gain = up if target > curr else down
222
269
  nxt = curr + gain * (target - curr)
@@ -226,14 +273,19 @@ def coordinar_fase_global_vecinal(G, fuerza_global: float | None = None, fuerza_
226
273
  kL = _step(kL, kL_t, kL_min, kL_max)
227
274
 
228
275
  # 5) Persistir en G.graph y log de serie
229
- G.graph["PHASE_K_GLOBAL"] = kG
230
- G.graph["PHASE_K_LOCAL"] = kL
231
- hist = G.graph.setdefault("history", {})
232
- hist.setdefault("phase_kG", []).append(float(kG))
233
- hist.setdefault("phase_kL", []).append(float(kL))
234
- hist.setdefault("phase_state", []).append(state)
235
- hist.setdefault("phase_R", []).append(float(R))
236
- hist.setdefault("phase_disr", []).append(float(disr))
276
+ g["PHASE_K_GLOBAL"] = kG
277
+ g["PHASE_K_LOCAL"] = kL
278
+ hist = g.setdefault("history", {})
279
+ hist_kG = hist.setdefault("phase_kG", [])
280
+ hist_kL = hist.setdefault("phase_kL", [])
281
+ hist_state = hist.setdefault("phase_state", [])
282
+ hist_R = hist.setdefault("phase_R", [])
283
+ hist_disr = hist.setdefault("phase_disr", [])
284
+ hist_kG.append(float(kG))
285
+ hist_kL.append(float(kL))
286
+ hist_state.append(state)
287
+ hist_R.append(float(R))
288
+ hist_disr.append(float(disr))
237
289
 
238
290
  # 6) Fase GLOBAL (centroide) para empuje
239
291
  X = list(math.cos(_get_attr(G.nodes[n], ALIAS_THETA, 0.0)) for n in G.nodes())
@@ -394,7 +446,7 @@ def step(G, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool
394
446
 
395
447
  # 2) (opcional) Si
396
448
  if use_Si:
397
- from helpers import compute_Si
449
+ from .helpers import compute_Si
398
450
  compute_Si(G, inplace=True)
399
451
 
400
452
  # 2b) Normalizadores para selector paramétrico (por paso)
@@ -403,11 +455,15 @@ def step(G, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool
403
455
  # 3) Selección glífica + aplicación
404
456
  if apply_glyphs:
405
457
  selector = G.graph.get("glyph_selector", default_glyph_selector)
406
- from operators import aplicar_glifo
458
+ from .operators import aplicar_glifo
407
459
  window = int(G.graph.get("GLYPH_HYSTERESIS_WINDOW", DEFAULTS["GLYPH_HYSTERESIS_WINDOW"]))
460
+ use_canon = bool(G.graph.get("GRAMMAR_CANON", DEFAULTS.get("GRAMMAR_CANON", {})).get("enabled", False))
408
461
  for n in G.nodes():
409
- g = selector(G, n)
410
- aplicar_glifo(G, n, g, window=window)
462
+ if use_canon:
463
+ select_and_apply_with_grammar(G, n, selector, window)
464
+ else:
465
+ g = selector(G, n)
466
+ aplicar_glifo(G, n, g, window=window)
411
467
 
412
468
  # 4) Ecuación nodal
413
469
  update_epi_via_nodal_equation(G, dt=dt)
@@ -422,13 +478,13 @@ def step(G, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool
422
478
  # 7) Observadores ligeros
423
479
  _update_history(G)
424
480
  # dynamics.py — dentro de step(), justo antes del punto 8)
425
- epi_hist = G.graph.setdefault("_epi_hist", [])
426
- epi_hist.append({n: _get_attr(G.nodes[n], ALIAS_EPI, 0.0) for n in G.nodes()})
427
- # recorta el buffer para que no crezca sin límite
428
481
  tau = int(G.graph.get("REMESH_TAU", DEFAULTS["REMESH_TAU"]))
429
- maxlen = max(2*tau + 5, 64)
430
- if len(epi_hist) > maxlen:
431
- del epi_hist[:-maxlen]
482
+ maxlen = max(2 * tau + 5, 64)
483
+ epi_hist = G.graph.get("_epi_hist")
484
+ if not isinstance(epi_hist, deque) or epi_hist.maxlen != maxlen:
485
+ epi_hist = deque(list(epi_hist or [])[-maxlen:], maxlen=maxlen)
486
+ G.graph["_epi_hist"] = epi_hist
487
+ epi_hist.append({n: _get_attr(G.nodes[n], ALIAS_EPI, 0.0) for n in G.nodes()})
432
488
 
433
489
  # 8) RE’MESH condicionado
434
490
  aplicar_remesh_si_estabilizacion_global(G)
tnfr/gamma.py ADDED
@@ -0,0 +1,91 @@
1
+ """gamma.py — TNFR canónica
2
+
3
+ Γi(R): acoplamientos de red para la ecuación nodal extendida
4
+ ∂EPI/∂t = νf · ΔNFR(t) + Γi(R)
5
+
6
+ Provee:
7
+ - kuramoto_R_psi(G): (R, ψ) orden de Kuramoto en la red
8
+ - GAMMA_REGISTRY: registro de acoplamientos canónicos
9
+ - eval_gamma(G, node, t): evalúa Γ para cada nodo según G.graph['GAMMA']
10
+ """
11
+ from __future__ import annotations
12
+ from typing import Dict, Any, Tuple
13
+ import math
14
+ import cmath
15
+
16
+ from .constants import ALIAS_THETA
17
+
18
+
19
+ def _get_attr(nd: Dict[str, Any], aliases, default: float = 0.0) -> float:
20
+ """Obtiene el primer atributo presente en nd según aliases."""
21
+ for k in aliases:
22
+ if k in nd:
23
+ return nd[k]
24
+ return default
25
+
26
+
27
+ def kuramoto_R_psi(G) -> Tuple[float, float]:
28
+ """Devuelve (R, ψ) del orden de Kuramoto usando θ de todos los nodos."""
29
+ acc = 0 + 0j
30
+ n = 0
31
+ for node in G.nodes():
32
+ nd = G.nodes[node]
33
+ th = _get_attr(nd, ALIAS_THETA, 0.0)
34
+ acc += cmath.exp(1j * th)
35
+ n += 1
36
+ if n == 0:
37
+ return 0.0, 0.0
38
+ z = acc / n
39
+ return abs(z), math.atan2(z.imag, z.real)
40
+
41
+
42
+ # -----------------
43
+ # Γi(R) canónicos
44
+ # -----------------
45
+
46
+
47
+ def gamma_none(G, node, t, cfg: Dict[str, Any]) -> float:
48
+ return 0.0
49
+
50
+
51
+ def gamma_kuramoto_linear(G, node, t, cfg: Dict[str, Any]) -> float:
52
+ """Acoplamiento lineal de Kuramoto para Γi(R).
53
+
54
+ Fórmula: Γ = β · (R - R0) · cos(θ_i - ψ)
55
+ - R ∈ [0,1] es el orden global de fase.
56
+ - ψ es la fase media (dirección de coordinación).
57
+ - β, R0 son parámetros (ganancia/umbral).
58
+
59
+ Uso: refuerza integración cuando la red ya exhibe coherencia de fase (R>R0).
60
+ """
61
+ beta = float(cfg.get("beta", 0.0))
62
+ R0 = float(cfg.get("R0", 0.0))
63
+ R, psi = kuramoto_R_psi(G)
64
+ th_i = _get_attr(G.nodes[node], ALIAS_THETA, 0.0)
65
+ return beta * (R - R0) * math.cos(th_i - psi)
66
+
67
+
68
+ def gamma_kuramoto_bandpass(G, node, t, cfg: Dict[str, Any]) -> float:
69
+ """Γ = β · R(1-R) · sign(cos(θ_i - ψ))"""
70
+ beta = float(cfg.get("beta", 0.0))
71
+ R, psi = kuramoto_R_psi(G)
72
+ th_i = _get_attr(G.nodes[node], ALIAS_THETA, 0.0)
73
+ sgn = 1.0 if math.cos(th_i - psi) >= 0.0 else -1.0
74
+ return beta * R * (1.0 - R) * sgn
75
+
76
+
77
+ GAMMA_REGISTRY = {
78
+ "none": gamma_none,
79
+ "kuramoto_linear": gamma_kuramoto_linear,
80
+ "kuramoto_bandpass": gamma_kuramoto_bandpass,
81
+ }
82
+
83
+
84
+ def eval_gamma(G, node, t) -> float:
85
+ """Evalúa Γi para `node` según la especificación en G.graph['GAMMA']."""
86
+ spec = G.graph.get("GAMMA", {"type": "none"})
87
+ fn = GAMMA_REGISTRY.get(spec.get("type", "none"), gamma_none)
88
+ try:
89
+ return float(fn(G, node, t, spec))
90
+ except Exception:
91
+ return 0.0