tnfr 4.0.0__py3-none-any.whl → 4.3.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,57 +1,61 @@
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
- )
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.3.0"
12
+
13
+ # Re-exports de la API pública
14
+ from .dynamics import step, run, set_delta_nfr_hook, validate_canon
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
25
  from .metrics import (
26
26
  register_metrics_callbacks,
27
27
  Tg_global, Tg_by_node,
28
28
  latency_series, glifogram_series,
29
- glyph_top, glyph_dwell_stats,
29
+ glyph_top, glyph_dwell_stats, export_history,
30
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__ = [
31
+ from .trace import register_trace
32
+ from .program import play, seq, block, target, wait, THOL, TARGET, WAIT, ejemplo_canonico_basico
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
+
39
+ __all__ = [
39
40
  "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",
41
+ "step", "run", "set_delta_nfr_hook", "validate_canon",
42
+
43
+ "attach_standard_observer", "coherencia_global", "orden_kuramoto",
44
+ "GAMMA_REGISTRY", "eval_gamma", "kuramoto_R_psi",
45
+ "enforce_canonical_grammar", "on_applied_glifo",
46
+ "GLYPHS_CANONICAL", "glyph_angle", "glyph_unit",
47
+ "sigma_vector_node", "sigma_vector_global",
48
+ "push_sigma_snapshot", "sigma_series", "sigma_rose",
49
+ "register_sigma_callback",
50
+ "register_metrics_callbacks",
51
+ "register_trace",
52
+ "Tg_global", "Tg_by_node",
53
+ "latency_series", "glifogram_series",
52
54
  "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"]
55
+ "export_history",
56
+ "play", "seq", "block", "target", "wait", "THOL", "TARGET", "WAIT",
57
+ "cli_main", "build_graph", "get_preset", "NodeState",
58
+ "ejemplo_canonico_basico",
59
+ "__version__",
60
+ ]
61
+
tnfr/cli.py CHANGED
@@ -18,10 +18,12 @@ from .metrics import (
18
18
  latency_series,
19
19
  glifogram_series,
20
20
  glyph_top,
21
+ export_history,
21
22
  )
22
23
  from .trace import register_trace
23
24
  from .program import play, seq, block, wait, target
24
- from .dynamics import step, _update_history
25
+ from .dynamics import step, _update_history, default_glyph_selector, parametric_glyph_selector, validate_canon
26
+ from .gamma import GAMMA_REGISTRY
25
27
  from .scenarios import build_graph
26
28
  from .presets import get_preset
27
29
 
@@ -31,6 +33,24 @@ def _save_json(path: str, data: Any) -> None:
31
33
  json.dump(data, f, ensure_ascii=False, indent=2)
32
34
 
33
35
 
36
+ def _str2bool(s: str) -> bool:
37
+ s = s.lower()
38
+ if s in {"true", "1", "yes", "y"}:
39
+ return True
40
+ if s in {"false", "0", "no", "n"}:
41
+ return False
42
+ raise argparse.ArgumentTypeError("expected true/false")
43
+
44
+
45
+ def _args_to_dict(args: argparse.Namespace, prefix: str) -> Dict[str, Any]:
46
+ out: Dict[str, Any] = {}
47
+ pref = prefix.replace(".", "_")
48
+ for k, v in vars(args).items():
49
+ if k.startswith(pref) and v is not None:
50
+ out[k[len(pref):]] = v
51
+ return out
52
+
53
+
34
54
  def _load_sequence(path: str) -> List[Any]:
35
55
  with open(path, "r", encoding="utf-8") as f:
36
56
  text = f.read()
@@ -72,6 +92,20 @@ def _attach_callbacks(G: nx.Graph) -> None:
72
92
  def cmd_run(args: argparse.Namespace) -> int:
73
93
  G = build_graph(n=args.nodes, topology=args.topology, seed=args.seed)
74
94
  _attach_callbacks(G)
95
+ validate_canon(G)
96
+ gcanon = dict(DEFAULTS["GRAMMAR_CANON"])
97
+ gcanon.update(_args_to_dict(args, prefix="grammar."))
98
+ if hasattr(args, "grammar_canon") and args.grammar_canon is not None:
99
+ gcanon["enabled"] = bool(args.grammar_canon)
100
+ G.graph.setdefault("GRAMMAR_CANON", {}).update(gcanon)
101
+ if args.glyph_hysteresis_window is not None:
102
+ G.graph["GLYPH_HYSTERESIS_WINDOW"] = int(args.glyph_hysteresis_window)
103
+ G.graph["glyph_selector"] = default_glyph_selector if args.selector == "basic" else parametric_glyph_selector
104
+ G.graph["GAMMA"] = {
105
+ "type": args.gamma_type,
106
+ "beta": args.gamma_beta,
107
+ "R0": args.gamma_R0,
108
+ }
75
109
 
76
110
  if args.preset:
77
111
  program = get_preset(args.preset)
@@ -83,6 +117,8 @@ def cmd_run(args: argparse.Namespace) -> int:
83
117
 
84
118
  if args.save_history:
85
119
  _save_json(args.save_history, G.graph.get("history", {}))
120
+ if args.export_history_base:
121
+ export_history(G, args.export_history_base, fmt=args.export_format)
86
122
 
87
123
  if args.summary:
88
124
  tg = Tg_global(G, normalize=True)
@@ -96,6 +132,20 @@ def cmd_run(args: argparse.Namespace) -> int:
96
132
  def cmd_sequence(args: argparse.Namespace) -> int:
97
133
  G = build_graph(n=args.nodes, topology=args.topology, seed=args.seed)
98
134
  _attach_callbacks(G)
135
+ validate_canon(G)
136
+ gcanon = dict(DEFAULTS["GRAMMAR_CANON"])
137
+ gcanon.update(_args_to_dict(args, prefix="grammar."))
138
+ if hasattr(args, "grammar_canon") and args.grammar_canon is not None:
139
+ gcanon["enabled"] = bool(args.grammar_canon)
140
+ G.graph.setdefault("GRAMMAR_CANON", {}).update(gcanon)
141
+ if args.glyph_hysteresis_window is not None:
142
+ G.graph["GLYPH_HYSTERESIS_WINDOW"] = int(args.glyph_hysteresis_window)
143
+ G.graph["glyph_selector"] = default_glyph_selector if args.selector == "basic" else parametric_glyph_selector
144
+ G.graph["GAMMA"] = {
145
+ "type": args.gamma_type,
146
+ "beta": args.gamma_beta,
147
+ "R0": args.gamma_R0,
148
+ }
99
149
 
100
150
  if args.preset:
101
151
  program = get_preset(args.preset)
@@ -108,12 +158,22 @@ def cmd_sequence(args: argparse.Namespace) -> int:
108
158
 
109
159
  if args.save_history:
110
160
  _save_json(args.save_history, G.graph.get("history", {}))
161
+ if args.export_history_base:
162
+ export_history(G, args.export_history_base, fmt=args.export_format)
111
163
  return 0
112
164
 
113
165
 
114
166
  def cmd_metrics(args: argparse.Namespace) -> int:
115
167
  G = build_graph(n=args.nodes, topology=args.topology, seed=args.seed)
116
168
  _attach_callbacks(G)
169
+ validate_canon(G)
170
+ G.graph.setdefault("GRAMMAR_CANON", DEFAULTS["GRAMMAR_CANON"]).update({"enabled": bool(args.grammar_canon)})
171
+ G.graph["glyph_selector"] = default_glyph_selector if args.selector == "basic" else parametric_glyph_selector
172
+ G.graph["GAMMA"] = {
173
+ "type": args.gamma_type,
174
+ "beta": args.gamma_beta,
175
+ "R0": args.gamma_R0,
176
+ }
117
177
  for _ in range(int(args.steps or 200)):
118
178
  step(G)
119
179
 
@@ -146,7 +206,22 @@ def main(argv: Optional[List[str]] = None) -> int:
146
206
  p_run.add_argument("--seed", type=int, default=1)
147
207
  p_run.add_argument("--preset", type=str, default=None)
148
208
  p_run.add_argument("--save-history", dest="save_history", type=str, default=None)
209
+ p_run.add_argument("--export-history-base", dest="export_history_base", type=str, default=None)
210
+ p_run.add_argument("--export-format", dest="export_format", choices=["csv", "json"], default="json")
149
211
  p_run.add_argument("--summary", action="store_true")
212
+ p_run.add_argument("--no-canon", dest="grammar_canon", action="store_false", default=True, help="Desactiva gramática canónica")
213
+ p_run.add_argument("--grammar.enabled", dest="grammar_enabled", type=_str2bool, default=None)
214
+ p_run.add_argument("--grammar.zhir_requires_oz_window", dest="grammar_zhir_requires_oz_window", type=int, default=None)
215
+ p_run.add_argument("--grammar.zhir_dnfr_min", dest="grammar_zhir_dnfr_min", type=float, default=None)
216
+ p_run.add_argument("--grammar.thol_min_len", dest="grammar_thol_min_len", type=int, default=None)
217
+ p_run.add_argument("--grammar.thol_max_len", dest="grammar_thol_max_len", type=int, default=None)
218
+ p_run.add_argument("--grammar.thol_close_dnfr", dest="grammar_thol_close_dnfr", type=float, default=None)
219
+ p_run.add_argument("--grammar.si_high", dest="grammar_si_high", type=float, default=None)
220
+ p_run.add_argument("--glyph.hysteresis_window", dest="glyph_hysteresis_window", type=int, default=None)
221
+ p_run.add_argument("--selector", choices=["basic", "param"], default="basic")
222
+ p_run.add_argument("--gamma-type", choices=list(GAMMA_REGISTRY.keys()), default="none")
223
+ p_run.add_argument("--gamma-beta", type=float, default=0.0)
224
+ p_run.add_argument("--gamma-R0", type=float, default=0.0)
150
225
  p_run.set_defaults(func=cmd_run)
151
226
 
152
227
  p_seq = sub.add_parser("sequence", help="Ejecutar una secuencia (preset o YAML/JSON)")
@@ -156,6 +231,19 @@ def main(argv: Optional[List[str]] = None) -> int:
156
231
  p_seq.add_argument("--preset", type=str, default=None)
157
232
  p_seq.add_argument("--sequence-file", type=str, default=None)
158
233
  p_seq.add_argument("--save-history", dest="save_history", type=str, default=None)
234
+ p_seq.add_argument("--export-history-base", dest="export_history_base", type=str, default=None)
235
+ p_seq.add_argument("--export-format", dest="export_format", choices=["csv", "json"], default="json")
236
+ p_seq.add_argument("--gamma-type", choices=list(GAMMA_REGISTRY.keys()), default="none")
237
+ p_seq.add_argument("--gamma-beta", type=float, default=0.0)
238
+ p_seq.add_argument("--gamma-R0", type=float, default=0.0)
239
+ p_seq.add_argument("--grammar.enabled", dest="grammar_enabled", type=_str2bool, default=None)
240
+ p_seq.add_argument("--grammar.zhir_requires_oz_window", dest="grammar_zhir_requires_oz_window", type=int, default=None)
241
+ p_seq.add_argument("--grammar.zhir_dnfr_min", dest="grammar_zhir_dnfr_min", type=float, default=None)
242
+ p_seq.add_argument("--grammar.thol_min_len", dest="grammar_thol_min_len", type=int, default=None)
243
+ p_seq.add_argument("--grammar.thol_max_len", dest="grammar_thol_max_len", type=int, default=None)
244
+ p_seq.add_argument("--grammar.thol_close_dnfr", dest="grammar_thol_close_dnfr", type=float, default=None)
245
+ p_seq.add_argument("--grammar.si_high", dest="grammar_si_high", type=float, default=None)
246
+ p_seq.add_argument("--glyph.hysteresis_window", dest="glyph_hysteresis_window", type=int, default=None)
159
247
  p_seq.set_defaults(func=cmd_sequence)
160
248
 
161
249
  p_met = sub.add_parser("metrics", help="Correr breve y volcar métricas clave")
@@ -163,6 +251,11 @@ def main(argv: Optional[List[str]] = None) -> int:
163
251
  p_met.add_argument("--topology", choices=["ring", "complete", "erdos"], default="ring")
164
252
  p_met.add_argument("--steps", type=int, default=300)
165
253
  p_met.add_argument("--seed", type=int, default=1)
254
+ p_met.add_argument("--no-canon", dest="grammar_canon", action="store_false", default=True, help="Desactiva gramática canónica")
255
+ p_met.add_argument("--selector", choices=["basic", "param"], default="basic")
256
+ p_met.add_argument("--gamma-type", choices=list(GAMMA_REGISTRY.keys()), default="none")
257
+ p_met.add_argument("--gamma-beta", type=float, default=0.0)
258
+ p_met.add_argument("--gamma-R0", type=float, default=0.0)
166
259
  p_met.add_argument("--save", type=str, default=None)
167
260
  p_met.set_defaults(func=cmd_metrics)
168
261
 
tnfr/constants.py CHANGED
@@ -72,18 +72,25 @@ DEFAULTS: Dict[str, Any] = {
72
72
  # Criterios de estabilidad (para activar RE’MESH de red)
73
73
  "EPS_DNFR_STABLE": 1e-3,
74
74
  "EPS_DEPI_STABLE": 1e-3,
75
- "FRACTION_STABLE_REMESH": 0.80, # fracción de nodos estables requerida
76
- "REMESH_COOLDOWN_VENTANA": 20, # pasos mínimos entre RE’MESH
75
+ "FRACTION_STABLE_REMESH": 0.80, # fracción de nodos estables requerida
76
+ "REMESH_COOLDOWN_VENTANA": 20, # pasos mínimos entre RE’MESH
77
+ "REMESH_COOLDOWN_TS": 0.0, # cooldown adicional por tiempo simulado
77
78
  # Gating adicional basado en observadores (conmutador + ventana)
78
- "REMESH_REQUIRE_STABILITY": False, # si True, exige ventana de estabilidad multi-métrica
79
- "REMESH_STABILITY_WINDOW": 25, # tamaño de ventana para evaluar estabilidad
80
- "REMESH_MIN_PHASE_SYNC": 0.85, # media mínima de sincronía de fase en ventana
81
- "REMESH_MAX_GLYPH_DISR": 0.35, # media máxima de carga glífica disruptiva en ventana
82
- "REMESH_LOG_EVENTS": True, # guarda eventos y metadatos del RE’MESH
83
-
84
- # RE’MESH: memoria τ y mezcla α
85
- "REMESH_TAU": 8, # pasos hacia atrás
86
- "REMESH_ALPHA": 0.5, # mezcla con pasado
79
+ "REMESH_REQUIRE_STABILITY": False, # si True, exige ventana de estabilidad multi-métrica
80
+ "REMESH_STABILITY_WINDOW": 25, # tamaño de ventana para evaluar estabilidad
81
+ "REMESH_MIN_PHASE_SYNC": 0.85, # media mínima de sincronía de fase en ventana
82
+ "REMESH_MAX_GLYPH_DISR": 0.35, # media máxima de carga glífica disruptiva en ventana
83
+ "REMESH_MIN_SIGMA_MAG": 0.50, # magnitud mínima de σ en ventana
84
+ "REMESH_MIN_KURAMOTO_R": 0.80, # R de Kuramoto mínimo en ventana
85
+ "REMESH_MIN_SI_HI_FRAC": 0.50, # fracción mínima de nodos con Si alto
86
+ "REMESH_LOG_EVENTS": True, # guarda eventos y metadatos del RE’MESH
87
+
88
+ # RE’MESH: memoria τ y mezcla α (global/local)
89
+ "REMESH_TAU": 8, # compatibilidad: tau global por defecto
90
+ "REMESH_TAU_GLOBAL": 8, # pasos hacia atrás (escala global)
91
+ "REMESH_TAU_LOCAL": 4, # pasos hacia atrás (escala local)
92
+ "REMESH_ALPHA": 0.5, # mezcla con pasado
93
+ "REMESH_ALPHA_HARD": False, # si True ignora GLYPH_FACTORS['REMESH_alpha']
87
94
 
88
95
  # Histéresis glífica
89
96
  "GLYPH_HYSTERESIS_WINDOW": 7,
@@ -92,13 +99,13 @@ DEFAULTS: Dict[str, Any] = {
92
99
  "GLYPH_SELECTOR_MARGIN": 0.05,
93
100
 
94
101
  # Ventana para estimar la carga glífica en history/plots
95
- "GLYPH_LOAD_WINDOW": 50,
102
+ "GLYPH_LOAD_WINDOW": 50,
96
103
 
97
104
  # Tamaño de ventana para coherencia promedio W̄
98
105
  "WBAR_WINDOW": 25,
99
106
 
100
107
  # Factores suaves por glifo (operadores)
101
- "GLYPH_FACTORS": {
108
+ "GLYPH_FACTORS": {
102
109
  "AL_boost": 0.05, # A’L — pequeña emisión
103
110
  "EN_mix": 0.25, # E’N — mezcla con vecindad
104
111
  "IL_dnfr_factor": 0.7, # I’L — reduce ΔNFR
@@ -107,7 +114,7 @@ DEFAULTS: Dict[str, Any] = {
107
114
  "RA_epi_diff": 0.15, # R’A — difusión EPI
108
115
  "SHA_vf_factor": 0.85, # SH’A — baja νf
109
116
  "VAL_scale": 1.15, # VA’L — expande EPI
110
- "NUL_scale": 0.85, # NU’L — contrae EPI
117
+ "NUL_scale": 0.85, # NU’L — contrae EPI
111
118
  "THOL_accel": 0.10, # T’HOL — acelera (seg. deriv.) si hay umbral
112
119
  "ZHIR_theta_shift": 1.57079632679, # Z’HIR — desplazamiento ~π/2
113
120
  "NAV_jitter": 0.05, # NA’V — pequeña inestabilidad creativa
@@ -154,6 +161,7 @@ DEFAULTS: Dict[str, Any] = {
154
161
  "R0": 0.0,
155
162
  },
156
163
  "CALLBACKS_STRICT": False, # si True, un error en callback detiene; si False, se loguea y continúa
164
+ "VALIDATORS_STRICT": False, # si True, alerta si se clampa fuera de rango
157
165
  }
158
166
 
159
167
  # Gramática glífica canónica
@@ -174,11 +182,7 @@ DEFAULTS.setdefault("GRAMMAR_CANON", {
174
182
 
175
183
  def attach_defaults(G, override: bool = False) -> None:
176
184
  """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
185
+ inject_defaults(G, DEFAULTS, override=override)
182
186
 
183
187
 
184
188
  def inject_defaults(G, defaults: Dict[str, Any] = DEFAULTS, override: bool = False) -> None:
@@ -207,7 +211,10 @@ def merge_overrides(G, **overrides) -> None:
207
211
  ALIAS_VF = ("νf", "nu_f", "nu-f", "nu", "freq", "frequency")
208
212
  ALIAS_THETA = ("θ", "theta", "fase", "phi", "phase")
209
213
  ALIAS_DNFR = ("ΔNFR", "delta_nfr", "dnfr")
210
- ALIAS_EPI = ("EPI", "psi", "PSI", "value")
211
- ALIAS_SI = ("Si", "sense_index", "S_i", "sense", "meaning_index")
212
- ALIAS_dEPI = ("dEPI_dt", "dpsi_dt", "dEPI", "velocity")
213
- ALIAS_D2EPI = ("d2EPI_dt2", "d2psi_dt2", "d2EPI", "accel")
214
+ ALIAS_EPI = ("EPI", "psi", "PSI", "value")
215
+ ALIAS_SI = ("Si", "sense_index", "S_i", "sense", "meaning_index")
216
+ ALIAS_dEPI = ("dEPI_dt", "dpsi_dt", "dEPI", "velocity")
217
+ ALIAS_D2EPI = ("d2EPI_dt2", "d2psi_dt2", "d2EPI", "accel")
218
+ ALIAS_dVF = ("dνf_dt", "dvf_dt", "dnu_dt", "dvf")
219
+ ALIAS_D2VF = ("d2νf_dt2", "d2vf_dt2", "d2nu_dt2", "B")
220
+ ALIAS_dSI = ("δSi", "delta_Si", "dSi")
tnfr/dynamics.py CHANGED
@@ -19,7 +19,11 @@ import networkx as nx
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
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
22
+ from .constants import (
23
+ DEFAULTS,
24
+ ALIAS_VF, ALIAS_THETA, ALIAS_DNFR, ALIAS_EPI, ALIAS_SI,
25
+ ALIAS_dEPI, ALIAS_D2EPI, ALIAS_dVF, ALIAS_D2VF, ALIAS_dSI,
26
+ )
23
27
  from .gamma import eval_gamma
24
28
  from .helpers import (
25
29
  clamp, clamp01, list_mean, phase_distance,
@@ -187,7 +191,7 @@ def integrar_epi_euler(G, dt: float | None = None) -> None:
187
191
  update_epi_via_nodal_equation(G, dt=dt)
188
192
 
189
193
 
190
- def aplicar_clamps_canonicos(nd: Dict[str, Any], G=None) -> None:
194
+ def aplicar_clamps_canonicos(nd: Dict[str, Any], G=None, node=None) -> None:
191
195
  eps_min = float((G.graph.get("EPI_MIN") if G is not None else DEFAULTS["EPI_MIN"]))
192
196
  eps_max = float((G.graph.get("EPI_MAX") if G is not None else DEFAULTS["EPI_MAX"]))
193
197
  vf_min = float((G.graph.get("VF_MIN") if G is not None else DEFAULTS["VF_MIN"]))
@@ -197,6 +201,14 @@ def aplicar_clamps_canonicos(nd: Dict[str, Any], G=None) -> None:
197
201
  vf = _get_attr(nd, ALIAS_VF, 0.0)
198
202
  th = _get_attr(nd, ALIAS_THETA, 0.0)
199
203
 
204
+ strict = bool((G.graph.get("VALIDATORS_STRICT") if G is not None else DEFAULTS.get("VALIDATORS_STRICT", False)))
205
+ if strict and G is not None:
206
+ hist = G.graph.setdefault("history", {}).setdefault("clamp_alerts", [])
207
+ if epi < eps_min or epi > eps_max:
208
+ hist.append({"node": node, "attr": "EPI", "value": float(epi)})
209
+ if vf < vf_min or vf > vf_max:
210
+ hist.append({"node": node, "attr": "VF", "value": float(vf)})
211
+
200
212
  _set_attr(nd, ALIAS_EPI, clamp(epi, eps_min, eps_max))
201
213
  _set_attr(nd, ALIAS_VF, clamp(vf, vf_min, vf_max))
202
214
  if (G.graph.get("THETA_WRAP") if G is not None else DEFAULTS["THETA_WRAP"]):
@@ -204,6 +216,17 @@ def aplicar_clamps_canonicos(nd: Dict[str, Any], G=None) -> None:
204
216
  _set_attr(nd, ALIAS_THETA, ((th + math.pi) % (2*math.pi) - math.pi))
205
217
 
206
218
 
219
+ def validate_canon(G) -> None:
220
+ """Aplica clamps canónicos a todos los nodos de ``G``.
221
+
222
+ Envuelve fase y restringe ``EPI`` y ``νf`` a los rangos en ``G.graph``.
223
+ Si ``VALIDATORS_STRICT`` está activo, registra alertas en ``history``.
224
+ """
225
+ for n in G.nodes():
226
+ aplicar_clamps_canonicos(G.nodes[n], G, n)
227
+ return G
228
+
229
+
207
230
  def coordinar_fase_global_vecinal(G, fuerza_global: float | None = None, fuerza_vecinal: float | None = None) -> None:
208
231
  """
209
232
  Ajusta fase con mezcla GLOBAL+VECINAL.
@@ -212,6 +235,10 @@ def coordinar_fase_global_vecinal(G, fuerza_global: float | None = None, fuerza_
212
235
  """
213
236
  g = G.graph
214
237
  defaults = DEFAULTS
238
+ hist = g.setdefault("history", {})
239
+ hist_state = hist.setdefault("phase_state", [])
240
+ hist_R = hist.setdefault("phase_R", [])
241
+ hist_disr = hist.setdefault("phase_disr", [])
215
242
  # 0) Si hay fuerzas explícitas, usar y salir del modo adaptativo
216
243
  if (fuerza_global is not None) or (fuerza_vecinal is not None):
217
244
  kG = float(
@@ -273,20 +300,15 @@ def coordinar_fase_global_vecinal(G, fuerza_global: float | None = None, fuerza_
273
300
  kL = _step(kL, kL_t, kL_min, kL_max)
274
301
 
275
302
  # 5) Persistir en G.graph y log de serie
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
303
  hist_state.append(state)
287
304
  hist_R.append(float(R))
288
305
  hist_disr.append(float(disr))
289
306
 
307
+ g["PHASE_K_GLOBAL"] = kG
308
+ g["PHASE_K_LOCAL"] = kL
309
+ hist.setdefault("phase_kG", []).append(float(kG))
310
+ hist.setdefault("phase_kL", []).append(float(kL))
311
+
290
312
  # 6) Fase GLOBAL (centroide) para empuje
291
313
  X = list(math.cos(_get_attr(G.nodes[n], ALIAS_THETA, 0.0)) for n in G.nodes())
292
314
  Y = list(math.sin(_get_attr(G.nodes[n], ALIAS_THETA, 0.0)) for n in G.nodes())
@@ -345,6 +367,21 @@ def _norms_para_selector(G) -> dict:
345
367
  G.graph["_sel_norms"] = norms
346
368
  return norms
347
369
 
370
+
371
+ def _soft_grammar_prefilter(G, n, cand, dnfr, accel):
372
+ """Gramática suave: evita repeticiones antes de la canónica."""
373
+ gram = G.graph.get("GRAMMAR", DEFAULTS.get("GRAMMAR", {}))
374
+ gwin = int(gram.get("window", 3))
375
+ avoid = set(gram.get("avoid_repeats", []))
376
+ force_dn = float(gram.get("force_dnfr", 0.60))
377
+ force_ac = float(gram.get("force_accel", 0.60))
378
+ fallbacks = gram.get("fallbacks", {})
379
+ nd = G.nodes[n]
380
+ if cand in avoid and reciente_glifo(nd, cand, gwin):
381
+ if not (dnfr >= force_dn or accel >= force_ac):
382
+ cand = fallbacks.get(cand, cand)
383
+ return cand
384
+
348
385
  def parametric_glyph_selector(G, n) -> str:
349
386
  """Multiobjetivo: combina Si, |ΔNFR|_norm y |accel|_norm + histéresis.
350
387
  Reglas base:
@@ -403,6 +440,19 @@ def parametric_glyph_selector(G, n) -> str:
403
440
  prev = list(hist)[-1]
404
441
  if isinstance(prev, str) and prev in ("I’L","O’Z","Z’HIR","T’HOL","NA’V","R’A"):
405
442
  return prev
443
+
444
+ # Penalización por falta de avance en σ/Si si se repite glifo
445
+ prev = None
446
+ hist_prev = nd.get("hist_glifos")
447
+ if hist_prev:
448
+ prev = list(hist_prev)[-1]
449
+ if prev == cand:
450
+ delta_si = _get_attr(nd, ALIAS_dSI, 0.0)
451
+ h = G.graph.get("history", {})
452
+ sig = h.get("sense_sigma_mag", [])
453
+ delta_sigma = sig[-1] - sig[-2] if len(sig) >= 2 else 0.0
454
+ if delta_si <= 0.0 and delta_sigma <= 0.0:
455
+ score -= 0.05
406
456
 
407
457
  # Override suave guiado por score (solo si NO cayó la histéresis arriba)
408
458
  # Regla: score>=0.66 inclina a I’L; score<=0.33 inclina a O’Z/Z’HIR
@@ -412,22 +462,9 @@ def parametric_glyph_selector(G, n) -> str:
412
462
  elif score <= 0.33 and cand in ("NA’V","R’A","I’L"):
413
463
  cand = "O’Z" if dnfr >= dnfr_lo else "Z’HIR"
414
464
  except NameError:
415
- # por si 'score' no se definió (robustez), no forzamos nada
416
465
  pass
417
466
 
418
- # --- Gramática glífica suave: evita repeticiones cercanas salvo que el campo lo pida ---
419
- gram = G.graph.get("GRAMMAR", DEFAULTS.get("GRAMMAR", {}))
420
- gwin = int(gram.get("window", 3))
421
- avoid = set(gram.get("avoid_repeats", []))
422
- force_dn = float(gram.get("force_dnfr", 0.60))
423
- force_ac = float(gram.get("force_accel", 0.60))
424
- fallbacks = gram.get("fallbacks", {})
425
-
426
- if cand in avoid and reciente_glifo(nd, cand, gwin):
427
- # Solo permitimos repetir si el campo "insiste": dnfr o accel altos (ya normalizados)
428
- if not (dnfr >= force_dn or accel >= force_ac):
429
- cand = fallbacks.get(cand, "R’A")
430
-
467
+ cand = _soft_grammar_prefilter(G, n, cand, dnfr, accel)
431
468
  return cand
432
469
 
433
470
  # -------------------------
@@ -470,7 +507,7 @@ def step(G, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool
470
507
 
471
508
  # 5) Clamps
472
509
  for n in G.nodes():
473
- aplicar_clamps_canonicos(G.nodes[n], G)
510
+ aplicar_clamps_canonicos(G.nodes[n], G, n)
474
511
 
475
512
  # 6) Coordinación de fase
476
513
  coordinar_fase_global_vecinal(G, None, None)
@@ -478,7 +515,9 @@ def step(G, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool
478
515
  # 7) Observadores ligeros
479
516
  _update_history(G)
480
517
  # dynamics.py — dentro de step(), justo antes del punto 8)
481
- tau = int(G.graph.get("REMESH_TAU", DEFAULTS["REMESH_TAU"]))
518
+ tau_g = int(G.graph.get("REMESH_TAU_GLOBAL", G.graph.get("REMESH_TAU", DEFAULTS["REMESH_TAU_GLOBAL"])))
519
+ tau_l = int(G.graph.get("REMESH_TAU_LOCAL", G.graph.get("REMESH_TAU", DEFAULTS["REMESH_TAU_LOCAL"])))
520
+ tau = max(tau_g, tau_l)
482
521
  maxlen = max(2 * tau + 5, 64)
483
522
  epi_hist = G.graph.get("_epi_hist")
484
523
  if not isinstance(epi_hist, deque) or epi_hist.maxlen != maxlen:
@@ -519,11 +558,12 @@ def run(G, steps: int, *, dt: float | None = None, use_Si: bool = True, apply_gl
519
558
  # -------------------------
520
559
 
521
560
  def _update_history(G) -> None:
522
- hist = G.graph.setdefault("history", {
523
- "C_steps": [], "stable_frac": [],
524
- "phase_sync": [], "glyph_load_estab": [], "glyph_load_disr": [],
525
- "Si_mean": [], "Si_hi_frac": [], "Si_lo_frac": []
526
- })
561
+ hist = G.graph.setdefault("history", {})
562
+ for k in (
563
+ "C_steps", "stable_frac", "phase_sync", "glyph_load_estab", "glyph_load_disr",
564
+ "Si_mean", "Si_hi_frac", "Si_lo_frac", "delta_Si", "B"
565
+ ):
566
+ hist.setdefault(k, [])
527
567
 
528
568
  # Proxy de coherencia C(t)
529
569
  dnfr_mean = list_mean(abs(_get_attr(G.nodes[n], ALIAS_DNFR, 0.0)) for n in G.nodes())
@@ -543,11 +583,37 @@ def _update_history(G) -> None:
543
583
  eps_depi = float(G.graph.get("EPS_DEPI_STABLE", DEFAULTS["EPS_DEPI_STABLE"]))
544
584
  stables = 0
545
585
  total = max(1, G.number_of_nodes())
586
+ dt = float(G.graph.get("DT", DEFAULTS.get("DT", 1.0))) or 1.0
587
+ delta_si_acc = []
588
+ B_acc = []
546
589
  for n in G.nodes():
547
590
  nd = G.nodes[n]
548
591
  if abs(_get_attr(nd, ALIAS_DNFR, 0.0)) <= eps_dnfr and abs(_get_attr(nd, ALIAS_dEPI, 0.0)) <= eps_depi:
549
592
  stables += 1
593
+
594
+ # δSi por nodo
595
+ Si_curr = _get_attr(nd, ALIAS_SI, 0.0)
596
+ Si_prev = nd.get("_prev_Si", Si_curr)
597
+ dSi = Si_curr - Si_prev
598
+ nd["_prev_Si"] = Si_curr
599
+ _set_attr(nd, ALIAS_dSI, dSi)
600
+ delta_si_acc.append(dSi)
601
+
602
+ # Bifurcación B = ∂²νf/∂t²
603
+ vf_curr = _get_attr(nd, ALIAS_VF, 0.0)
604
+ vf_prev = nd.get("_prev_vf", vf_curr)
605
+ dvf_dt = (vf_curr - vf_prev) / dt
606
+ dvf_prev = nd.get("_prev_dvf", dvf_dt)
607
+ B = (dvf_dt - dvf_prev) / dt
608
+ nd["_prev_vf"] = vf_curr
609
+ nd["_prev_dvf"] = dvf_dt
610
+ _set_attr(nd, ALIAS_dVF, dvf_dt)
611
+ _set_attr(nd, ALIAS_D2VF, B)
612
+ B_acc.append(B)
613
+
550
614
  hist["stable_frac"].append(stables/total)
615
+ hist["delta_Si"].append(list_mean(delta_si_acc, 0.0))
616
+ hist["B"].append(list_mean(B_acc, 0.0))
551
617
  # --- nuevas series: sincronía de fase y carga glífica ---
552
618
  try:
553
619
  ps = sincronía_fase(G) # [0,1], más alto = más en fase