tnfr 3.5.0__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,23 +1,57 @@
1
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__ = "3.0.3"
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
12
 
13
13
  # Re-exports de la API pública
14
14
  from .dynamics import step, run, set_delta_nfr_hook
15
15
  from .ontosim import preparar_red
16
16
  from .observers import attach_standard_observer, coherencia_global, orden_kuramoto
17
-
18
- __all__ = [
19
- "preparar_red",
20
- "step", "run", "set_delta_nfr_hook",
21
- "attach_standard_observer", "coherencia_global", "orden_kuramoto",
22
- "__version__",
23
- ]
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
@@ -18,7 +18,9 @@ import networkx as nx
18
18
 
19
19
  from .observers import sincronía_fase, carga_glifica, orden_kuramoto, sigma_vector
20
20
  from .operators import aplicar_remesh_si_estabilizacion_global
21
+ from .grammar import select_and_apply_with_grammar
21
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
22
24
  from .helpers import (
23
25
  clamp, clamp01, list_mean, phase_distance,
24
26
  _get_attr, _set_attr, media_vecinal, fase_media,
@@ -52,6 +54,7 @@ def _write_dnfr_metadata(G, *, weights: dict, hook_name: str, note: str | None =
52
54
 
53
55
 
54
56
  def default_compute_delta_nfr(G) -> None:
57
+ """Calcula ΔNFR mezclando gradientes de fase, EPI y νf según pesos."""
55
58
  w = G.graph.get("DNFR_WEIGHTS", DEFAULTS["DNFR_WEIGHTS"]) # dict
56
59
  w_phase = float(w.get("phase", 0.34))
57
60
  w_epi = float(w.get("epi", 0.33))
@@ -121,7 +124,23 @@ def dnfr_epi_vf_mixed(G) -> None:
121
124
  # Ecuación nodal
122
125
  # -------------------------
123
126
 
124
- 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
+ """
125
144
  if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)):
126
145
  raise TypeError("G must be a networkx graph instance")
127
146
  if dt is None:
@@ -132,16 +151,22 @@ def update_epi_via_nodal_equation(G, *, dt: float = None) -> None:
132
151
  if dt < 0:
133
152
  raise ValueError("dt must be non-negative")
134
153
  dt = float(dt)
154
+ if t is None:
155
+ t = float(G.graph.get("_t", 0.0))
156
+ else:
157
+ t = float(t)
135
158
  for n in G.nodes():
136
159
  nd = G.nodes[n]
137
160
  vf = _get_attr(nd, ALIAS_VF, 0.0)
138
161
  dnfr = _get_attr(nd, ALIAS_DNFR, 0.0)
139
162
  dEPI_dt_prev = _get_attr(nd, ALIAS_dEPI, 0.0)
140
163
  dEPI_dt = vf * dnfr
164
+ dEPI_dt += eval_gamma(G, n, t)
141
165
  epi = _get_attr(nd, ALIAS_EPI, 0.0) + dt * dEPI_dt
142
166
  _set_attr(nd, ALIAS_EPI, epi)
143
167
  _set_attr(nd, ALIAS_dEPI, dEPI_dt)
144
168
  _set_attr(nd, ALIAS_D2EPI, (dEPI_dt - dEPI_dt_prev) / dt if dt != 0 else 0.0)
169
+ G.graph["_t"] = t + dt
145
170
 
146
171
 
147
172
  # -------------------------
@@ -432,9 +457,13 @@ def step(G, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool
432
457
  selector = G.graph.get("glyph_selector", default_glyph_selector)
433
458
  from .operators import aplicar_glifo
434
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))
435
461
  for n in G.nodes():
436
- g = selector(G, n)
437
- 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)
438
467
 
439
468
  # 4) Ecuación nodal
440
469
  update_epi_via_nodal_equation(G, dt=dt)
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
tnfr/grammar.py ADDED
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+ from typing import Dict, Any, Set
3
+
4
+ from .constants import (
5
+ DEFAULTS,
6
+ ALIAS_SI, ALIAS_DNFR, ALIAS_EPI,
7
+ )
8
+ from .helpers import _get_attr, clamp01, reciente_glifo
9
+ from collections import deque
10
+
11
+ # Glifos nominales (para evitar typos)
12
+ AL = "A’L"; EN = "E’N"; IL = "I’L"; OZ = "O’Z"; UM = "U’M"; RA = "R’A"; SHA = "SH’A"; VAL = "VA’L"; NUL = "NU’L"; THOL = "T’HOL"; ZHIR = "Z’HIR"; NAV = "NA’V"; REMESH = "RE’MESH"
13
+
14
+ # -------------------------
15
+ # Estado de gramática por nodo
16
+ # -------------------------
17
+
18
+ def _gram_state(nd: Dict[str, Any]) -> Dict[str, Any]:
19
+ """Crea/retorna el estado de gramática nodal.
20
+ Campos:
21
+ - thol_open (bool)
22
+ - thol_len (int)
23
+ """
24
+ st = nd.setdefault("_GRAM", {"thol_open": False, "thol_len": 0})
25
+ st.setdefault("thol_open", False)
26
+ st.setdefault("thol_len", 0)
27
+ return st
28
+
29
+ # -------------------------
30
+ # Compatibilidades canónicas (siguiente permitido)
31
+ # -------------------------
32
+ CANON_COMPAT: Dict[str, Set[str]] = {
33
+ # Inicio / apertura
34
+ AL: {EN, RA, NAV, VAL, UM},
35
+ EN: {IL, UM, RA, NAV},
36
+ # Estabilización / difusión / acople
37
+ IL: {RA, VAL, UM, SHA},
38
+ UM: {RA, IL, VAL, NAV},
39
+ RA: {IL, VAL, UM, NAV},
40
+ VAL: {UM, RA, IL, NAV},
41
+ # Disonancia → transición → mutación
42
+ OZ: {ZHIR, NAV},
43
+ ZHIR: {IL, NAV},
44
+ NAV: {OZ, ZHIR, RA, IL, UM},
45
+ # Cierres / latencias
46
+ SHA: {AL, EN},
47
+ NUL: {AL, IL},
48
+ # Bloques autoorganizativos
49
+ THOL: {OZ, ZHIR, NAV, RA, IL, UM, SHA, NUL},
50
+ }
51
+
52
+ # Fallbacks canónicos si una transición no está permitida
53
+ CANON_FALLBACK: Dict[str, str] = {
54
+ AL: EN, EN: IL, IL: RA, UM: RA, RA: IL, VAL: RA, OZ: ZHIR, ZHIR: IL, NAV: RA, SHA: AL, NUL: AL, THOL: NAV,
55
+ }
56
+
57
+ # -------------------------
58
+ # Cierres T’HOL y precondiciones Z’HIR
59
+ # -------------------------
60
+
61
+ def _dnfr_norm(G, nd) -> float:
62
+ # Normalizador robusto: usa historial de |ΔNFR| máx guardado por dynamics (si existe)
63
+ norms = G.graph.get("_sel_norms") or {}
64
+ dmax = float(norms.get("dnfr_max", 1.0)) or 1.0
65
+ return clamp01(abs(_get_attr(nd, ALIAS_DNFR, 0.0)) / dmax)
66
+
67
+
68
+ def _si(G, nd) -> float:
69
+ return clamp01(_get_attr(nd, ALIAS_SI, 0.5))
70
+
71
+ # -------------------------
72
+ # Núcleo: forzar gramática sobre un candidato
73
+ # -------------------------
74
+
75
+ def enforce_canonical_grammar(G, n, cand: str) -> str:
76
+ """Valida/ajusta el glifo candidato según la gramática canónica.
77
+
78
+ Reglas clave:
79
+ - Compatibilidades de transición glífica (recorrido TNFR).
80
+ - O’Z→Z’HIR: la mutación requiere disonancia reciente o |ΔNFR| alto.
81
+ - T’HOL[...]: obliga cierre con SH’A o NU’L cuando el campo se estabiliza
82
+ o se alcanza el largo del bloque; mantiene estado por nodo.
83
+
84
+ Devuelve el glifo efectivo a aplicar.
85
+ """
86
+ nd = G.nodes[n]
87
+ st = _gram_state(nd)
88
+ cfg = G.graph.get("GRAMMAR_CANON", DEFAULTS.get("GRAMMAR_CANON", {}))
89
+
90
+ # 0) Si vienen glifos fuera del alfabeto, no tocamos
91
+ if cand not in CANON_COMPAT:
92
+ return cand
93
+
94
+ # 1) Precondición O’Z→Z’HIR: mutación requiere disonancia reciente o campo fuerte
95
+ if cand == ZHIR:
96
+ win = int(cfg.get("zhir_requires_oz_window", 3))
97
+ dn_min = float(cfg.get("zhir_dnfr_min", 0.05))
98
+ if not reciente_glifo(nd, OZ, win) and _dnfr_norm(G, nd) < dn_min:
99
+ cand = OZ # forzamos paso por O’Z
100
+
101
+ # 2) Si estamos dentro de T’HOL, control de cierre obligado
102
+ if st.get("thol_open", False):
103
+ st["thol_len"] = int(st.get("thol_len", 0))
104
+ st["thol_len"] += 1
105
+ minlen = int(cfg.get("thol_min_len", 2))
106
+ maxlen = int(cfg.get("thol_max_len", 6))
107
+ close_dn = float(cfg.get("thol_close_dnfr", 0.15))
108
+ if st["thol_len"] >= maxlen or (st["thol_len"] >= minlen and _dnfr_norm(G, nd) <= close_dn):
109
+ cand = NUL if _si(G, nd) >= float(cfg.get("si_high", 0.66)) else SHA
110
+
111
+ # 3) Compatibilidades: si el anterior restringe el siguiente
112
+ prev = None
113
+ hist = nd.get("hist_glifos")
114
+ if hist:
115
+ try:
116
+ prev = list(hist)[-1]
117
+ except Exception:
118
+ prev = None
119
+ if prev in CANON_COMPAT and cand not in CANON_COMPAT[prev]:
120
+ cand = CANON_FALLBACK.get(prev, cand)
121
+
122
+ return cand
123
+
124
+ # -------------------------
125
+ # Post-selección: actualizar estado de gramática
126
+ # -------------------------
127
+
128
+ def on_applied_glifo(G, n, applied: str) -> None:
129
+ nd = G.nodes[n]
130
+ st = _gram_state(nd)
131
+ if applied == THOL:
132
+ st["thol_open"] = True
133
+ st["thol_len"] = 0
134
+ elif applied in (SHA, NUL):
135
+ st["thol_open"] = False
136
+ st["thol_len"] = 0
137
+ else:
138
+ pass
139
+
140
+ # -------------------------
141
+ # Integración con dynamics.step: helper de selección+aplicación
142
+ # -------------------------
143
+
144
+ def select_and_apply_with_grammar(G, n, selector, window: int) -> None:
145
+ from .operators import aplicar_glifo
146
+ cand = selector(G, n)
147
+ cand = enforce_canonical_grammar(G, n, cand)
148
+ aplicar_glifo(G, n, cand, window=window)
149
+ on_applied_glifo(G, n, cand)