tnfr 4.5.0__py3-none-any.whl → 4.5.2__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.

Files changed (78) hide show
  1. tnfr/__init__.py +91 -89
  2. tnfr/alias.py +546 -0
  3. tnfr/cache.py +578 -0
  4. tnfr/callback_utils.py +388 -0
  5. tnfr/cli/__init__.py +75 -0
  6. tnfr/cli/arguments.py +177 -0
  7. tnfr/cli/execution.py +288 -0
  8. tnfr/cli/utils.py +36 -0
  9. tnfr/collections_utils.py +300 -0
  10. tnfr/config.py +19 -28
  11. tnfr/constants/__init__.py +174 -0
  12. tnfr/constants/core.py +159 -0
  13. tnfr/constants/init.py +31 -0
  14. tnfr/constants/metric.py +110 -0
  15. tnfr/constants_glyphs.py +98 -0
  16. tnfr/dynamics/__init__.py +658 -0
  17. tnfr/dynamics/dnfr.py +733 -0
  18. tnfr/dynamics/integrators.py +267 -0
  19. tnfr/dynamics/sampling.py +31 -0
  20. tnfr/execution.py +201 -0
  21. tnfr/flatten.py +283 -0
  22. tnfr/gamma.py +302 -88
  23. tnfr/glyph_history.py +290 -0
  24. tnfr/grammar.py +285 -96
  25. tnfr/graph_utils.py +84 -0
  26. tnfr/helpers/__init__.py +71 -0
  27. tnfr/helpers/numeric.py +87 -0
  28. tnfr/immutable.py +178 -0
  29. tnfr/import_utils.py +228 -0
  30. tnfr/initialization.py +197 -0
  31. tnfr/io.py +246 -0
  32. tnfr/json_utils.py +162 -0
  33. tnfr/locking.py +37 -0
  34. tnfr/logging_utils.py +116 -0
  35. tnfr/metrics/__init__.py +41 -0
  36. tnfr/metrics/coherence.py +829 -0
  37. tnfr/metrics/common.py +151 -0
  38. tnfr/metrics/core.py +101 -0
  39. tnfr/metrics/diagnosis.py +234 -0
  40. tnfr/metrics/export.py +137 -0
  41. tnfr/metrics/glyph_timing.py +189 -0
  42. tnfr/metrics/reporting.py +148 -0
  43. tnfr/metrics/sense_index.py +120 -0
  44. tnfr/metrics/trig.py +181 -0
  45. tnfr/metrics/trig_cache.py +109 -0
  46. tnfr/node.py +214 -159
  47. tnfr/observers.py +126 -128
  48. tnfr/ontosim.py +134 -134
  49. tnfr/operators/__init__.py +420 -0
  50. tnfr/operators/jitter.py +203 -0
  51. tnfr/operators/remesh.py +485 -0
  52. tnfr/presets.py +46 -14
  53. tnfr/rng.py +254 -0
  54. tnfr/selector.py +210 -0
  55. tnfr/sense.py +284 -131
  56. tnfr/structural.py +207 -79
  57. tnfr/tokens.py +60 -0
  58. tnfr/trace.py +329 -94
  59. tnfr/types.py +43 -17
  60. tnfr/validators.py +70 -24
  61. tnfr/value_utils.py +59 -0
  62. tnfr-4.5.2.dist-info/METADATA +379 -0
  63. tnfr-4.5.2.dist-info/RECORD +67 -0
  64. tnfr/cli.py +0 -322
  65. tnfr/constants.py +0 -277
  66. tnfr/dynamics.py +0 -814
  67. tnfr/helpers.py +0 -264
  68. tnfr/main.py +0 -47
  69. tnfr/metrics.py +0 -597
  70. tnfr/operators.py +0 -525
  71. tnfr/program.py +0 -176
  72. tnfr/scenarios.py +0 -34
  73. tnfr-4.5.0.dist-info/METADATA +0 -109
  74. tnfr-4.5.0.dist-info/RECORD +0 -28
  75. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/WHEEL +0 -0
  76. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/entry_points.txt +0 -0
  77. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/licenses/LICENSE.md +0 -0
  78. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/top_level.txt +0 -0
tnfr/dynamics.py DELETED
@@ -1,814 +0,0 @@
1
- """
2
- dynamics.py — TNFR canónica
3
-
4
- Bucle de dinámica con la ecuación nodal y utilidades:
5
- ∂EPI/∂t = νf · ΔNFR(t)
6
- Incluye:
7
- - default_compute_delta_nfr (mezcla de fase/EPI/νf)
8
- - update_epi_via_nodal_equation (Euler explícito)
9
- - aplicar_dnfr_campo, integrar_epi_euler, aplicar_clamps_canonicos
10
- - coordinar_fase_global_vecinal
11
- - default_glyph_selector, step, run
12
- """
13
- from __future__ import annotations
14
- from typing import Dict, Any, Iterable, Literal
15
- import math
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 (
22
- enforce_canonical_grammar,
23
- on_applied_glifo,
24
- AL,
25
- EN,
26
- )
27
- from .constants import (
28
- DEFAULTS,
29
- ALIAS_VF, ALIAS_THETA, ALIAS_DNFR, ALIAS_EPI, ALIAS_SI,
30
- ALIAS_dEPI, ALIAS_D2EPI, ALIAS_dVF, ALIAS_D2VF, ALIAS_dSI,
31
- ALIAS_EPI_KIND,
32
- )
33
- from .gamma import eval_gamma
34
- from .helpers import (
35
- clamp, clamp01, list_mean, phase_distance,
36
- _get_attr, _set_attr, _get_attr_str, _set_attr_str, media_vecinal, fase_media,
37
- invoke_callbacks, reciente_glifo
38
- )
39
-
40
- # -------------------------
41
- # ΔNFR por defecto (campo) + utilidades de hook/metadata
42
- # -------------------------
43
-
44
- def _write_dnfr_metadata(G, *, weights: dict, hook_name: str, note: str | None = None) -> None:
45
- """Escribe en G.graph un bloque _DNFR_META con la mezcla y el nombre del hook.
46
-
47
- `weights` puede incluir componentes arbitrarias (phase/epi/vf/topo/etc.)."""
48
- total = sum(float(v) for v in weights.values())
49
- if total <= 0:
50
- # si no hay pesos, normalizamos a componentes iguales
51
- n = max(1, len(weights))
52
- weights = {k: 1.0 / n for k in weights}
53
- total = 1.0
54
- meta = {
55
- "hook": hook_name,
56
- "weights_raw": dict(weights),
57
- "weights_norm": {k: float(v) / total for k, v in weights.items()},
58
- "components": [k for k, v in weights.items() if float(v) != 0.0],
59
- "doc": "ΔNFR = Σ w_i·g_i",
60
- }
61
- if note:
62
- meta["note"] = str(note)
63
- G.graph["_DNFR_META"] = meta
64
- G.graph["_dnfr_hook_name"] = hook_name # string friendly
65
-
66
-
67
- def default_compute_delta_nfr(G) -> None:
68
- """Calcula ΔNFR mezclando gradientes de fase, EPI, νf y un término topológico."""
69
- w = G.graph.get("DNFR_WEIGHTS", DEFAULTS["DNFR_WEIGHTS"]) # dict
70
- w_phase = float(w.get("phase", 0.34))
71
- w_epi = float(w.get("epi", 0.33))
72
- w_vf = float(w.get("vf", 0.33))
73
- w_topo = float(w.get("topo", 0.0))
74
- s = w_phase + w_epi + w_vf + w_topo
75
- if s <= 0:
76
- w_phase = w_epi = w_vf = 1/3
77
- w_topo = 0.0
78
- s = 1.0
79
- else:
80
- w_phase, w_epi, w_vf, w_topo = (w_phase/s, w_epi/s, w_vf/s, w_topo/s)
81
-
82
- # Documentar mezcla y hook activo
83
- _write_dnfr_metadata(
84
- G,
85
- weights={"phase": w_phase, "epi": w_epi, "vf": w_vf, "topo": w_topo},
86
- hook_name="default_compute_delta_nfr",
87
- )
88
-
89
- degs = dict(G.degree()) if w_topo != 0 else None
90
-
91
- for n in G.nodes():
92
- nd = G.nodes[n]
93
- th_i = _get_attr(nd, ALIAS_THETA, 0.0)
94
- th_bar = fase_media(G, n)
95
- # Gradiente de fase: empuja hacia la fase media (signo envuelto)
96
- g_phase = -((th_i - th_bar + math.pi) % (2 * math.pi) - math.pi) / math.pi # ~[-1,1]
97
-
98
- epi_i = _get_attr(nd, ALIAS_EPI, 0.0)
99
- epi_bar = media_vecinal(G, n, ALIAS_EPI, default=epi_i)
100
- g_epi = (epi_bar - epi_i) # gradiente escalar
101
-
102
- vf_i = _get_attr(nd, ALIAS_VF, 0.0)
103
- vf_bar = media_vecinal(G, n, ALIAS_VF, default=vf_i)
104
- g_vf = (vf_bar - vf_i)
105
-
106
- if w_topo != 0 and degs is not None:
107
- deg_i = float(degs.get(n, 0))
108
- deg_bar = list_mean(degs.get(v, deg_i) for v in G.neighbors(n)) if G.degree(n) else deg_i
109
- g_topo = deg_bar - deg_i
110
- else:
111
- g_topo = 0.0
112
-
113
- dnfr = w_phase * g_phase + w_epi * g_epi + w_vf * g_vf + w_topo * g_topo
114
- _set_attr(nd, ALIAS_DNFR, dnfr)
115
-
116
- def set_delta_nfr_hook(G, func, *, name: str | None = None, note: str | None = None) -> None:
117
- """Fija un hook estable para calcular ΔNFR. Firma requerida: func(G)->None y debe
118
- escribir ALIAS_DNFR en cada nodo. Actualiza metadatos básicos en G.graph."""
119
- G.graph["compute_delta_nfr"] = func
120
- G.graph["_dnfr_hook_name"] = str(name or getattr(func, "__name__", "custom_dnfr"))
121
- if note:
122
- meta = G.graph.get("_DNFR_META", {})
123
- meta["note"] = str(note)
124
- G.graph["_DNFR_META"] = meta
125
-
126
- # --- Hooks de ejemplo (opcionales) ---
127
- def dnfr_phase_only(G) -> None:
128
- """Ejemplo: ΔNFR solo desde fase (tipo Kuramoto-like)."""
129
- for n in G.nodes():
130
- nd = G.nodes[n]
131
- th_i = _get_attr(nd, ALIAS_THETA, 0.0)
132
- th_bar = fase_media(G, n)
133
- g_phase = -((th_i - th_bar + math.pi) % (2 * math.pi) - math.pi) / math.pi
134
- _set_attr(nd, ALIAS_DNFR, g_phase)
135
- _write_dnfr_metadata(G, weights={"phase": 1.0}, hook_name="dnfr_phase_only", note="Hook de ejemplo.")
136
-
137
- def dnfr_epi_vf_mixed(G) -> None:
138
- """Ejemplo: ΔNFR sin fase, mezclando EPI y νf."""
139
- for n in G.nodes():
140
- nd = G.nodes[n]
141
- epi_i = _get_attr(nd, ALIAS_EPI, 0.0)
142
- epi_bar = media_vecinal(G, n, ALIAS_EPI, default=epi_i)
143
- g_epi = (epi_bar - epi_i)
144
- vf_i = _get_attr(nd, ALIAS_VF, 0.0)
145
- vf_bar = media_vecinal(G, n, ALIAS_VF, default=vf_i)
146
- g_vf = (vf_bar - vf_i)
147
- _set_attr(nd, ALIAS_DNFR, 0.5*g_epi + 0.5*g_vf)
148
- _write_dnfr_metadata(G, weights={"phase":0.0, "epi":0.5, "vf":0.5}, hook_name="dnfr_epi_vf_mixed", note="Hook de ejemplo.")
149
-
150
-
151
- def dnfr_laplacian(G) -> None:
152
- """Gradiente topológico explícito usando Laplaciano sobre EPI y νf."""
153
- wE = float(G.graph.get("DNFR_WEIGHTS", {}).get("epi", 0.33))
154
- wV = float(G.graph.get("DNFR_WEIGHTS", {}).get("vf", 0.33))
155
- for n in G.nodes():
156
- nd = G.nodes[n]
157
- epi = _get_attr(nd, ALIAS_EPI, 0.0)
158
- vf = _get_attr(nd, ALIAS_VF, 0.0)
159
- neigh = list(G.neighbors(n))
160
- deg = len(neigh) or 1
161
- epi_bar = sum(_get_attr(G.nodes[v], ALIAS_EPI, epi) for v in neigh) / deg
162
- vf_bar = sum(_get_attr(G.nodes[v], ALIAS_VF, vf) for v in neigh) / deg
163
- g_epi = epi_bar - epi
164
- g_vf = vf_bar - vf
165
- _set_attr(nd, ALIAS_DNFR, wE * g_epi + wV * g_vf)
166
- _write_dnfr_metadata(
167
- G,
168
- weights={"epi": wE, "vf": wV},
169
- hook_name="dnfr_laplacian",
170
- note="Gradiente topológico",
171
- )
172
-
173
- # -------------------------
174
- # Ecuación nodal
175
- # -------------------------
176
-
177
- def update_epi_via_nodal_equation(
178
- G,
179
- *,
180
- dt: float = None,
181
- t: float | None = None,
182
- method: Literal["euler", "rk4"] | None = None,
183
- ) -> None:
184
- """Ecuación nodal TNFR.
185
-
186
- Implementa la forma extendida de la ecuación nodal:
187
- ∂EPI/∂t = νf · ΔNFR(t) + Γi(R)
188
-
189
- Donde:
190
- - EPI es la Estructura Primaria de Información del nodo.
191
- - νf es la frecuencia estructural del nodo (Hz_str).
192
- - ΔNFR(t) es el gradiente nodal (necesidad de reorganización),
193
- típicamente una mezcla de componentes (p. ej. fase θ, EPI, νf).
194
- - Γi(R) es el acoplamiento de red opcional en función del orden de Kuramoto R
195
- (ver gamma.py), usado para modular la integración en red.
196
-
197
- Referencias TNFR: ecuación nodal (manual), glosario νf/ΔNFR/EPI, operador Γ.
198
- Efectos secundarios: cachea dEPI y actualiza EPI por integración explícita.
199
- """
200
- if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)):
201
- raise TypeError("G must be a networkx graph instance")
202
- if dt is None:
203
- dt = float(G.graph.get("DT", DEFAULTS["DT"]))
204
- else:
205
- if not isinstance(dt, (int, float)):
206
- raise TypeError("dt must be a number")
207
- if dt < 0:
208
- raise ValueError("dt must be non-negative")
209
- dt = float(dt)
210
- if t is None:
211
- t = float(G.graph.get("_t", 0.0))
212
- else:
213
- t = float(t)
214
-
215
- method = (method or G.graph.get("INTEGRATOR_METHOD", DEFAULTS.get("INTEGRATOR_METHOD", "euler"))).lower()
216
- dt_min = float(G.graph.get("DT_MIN", DEFAULTS.get("DT_MIN", 0.0)))
217
- if dt_min > 0 and dt > dt_min:
218
- steps = int(math.ceil(dt / dt_min))
219
- else:
220
- steps = 1
221
- dt_step = dt / steps if steps else 0.0
222
-
223
- t_local = t
224
- for _ in range(steps):
225
- for n in G.nodes():
226
- nd = G.nodes[n]
227
- vf = _get_attr(nd, ALIAS_VF, 0.0)
228
- dnfr = _get_attr(nd, ALIAS_DNFR, 0.0)
229
- dEPI_dt_prev = _get_attr(nd, ALIAS_dEPI, 0.0)
230
- epi_i = _get_attr(nd, ALIAS_EPI, 0.0)
231
-
232
- def _f(time: float) -> float:
233
- return vf * dnfr + eval_gamma(G, n, time)
234
-
235
- if method == "rk4":
236
- k1 = _f(t_local)
237
- k2 = _f(t_local + dt_step / 2.0)
238
- k3 = _f(t_local + dt_step / 2.0)
239
- k4 = _f(t_local + dt_step)
240
- epi = epi_i + (dt_step / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)
241
- dEPI_dt = k4
242
- else:
243
- if method != "euler":
244
- raise ValueError("method must be 'euler' or 'rk4'")
245
- dEPI_dt = _f(t_local)
246
- epi = epi_i + dt_step * dEPI_dt
247
-
248
- epi_kind = _get_attr_str(nd, ALIAS_EPI_KIND, "")
249
- _set_attr(nd, ALIAS_EPI, epi)
250
- if epi_kind:
251
- _set_attr_str(nd, ALIAS_EPI_KIND, epi_kind)
252
- _set_attr(nd, ALIAS_dEPI, dEPI_dt)
253
- _set_attr(nd, ALIAS_D2EPI, (dEPI_dt - dEPI_dt_prev) / dt_step if dt_step != 0 else 0.0)
254
-
255
- t_local += dt_step
256
-
257
- G.graph["_t"] = t_local
258
-
259
-
260
- # -------------------------
261
- # Wrappers nombrados (compatibilidad)
262
- # -------------------------
263
-
264
- def aplicar_dnfr_campo(G, w_theta=None, w_epi=None, w_vf=None) -> None:
265
- if any(v is not None for v in (w_theta, w_epi, w_vf)):
266
- mix = G.graph.get("DNFR_WEIGHTS", DEFAULTS["DNFR_WEIGHTS"]).copy()
267
- if w_theta is not None: mix["phase"] = float(w_theta)
268
- if w_epi is not None: mix["epi"] = float(w_epi)
269
- if w_vf is not None: mix["vf"] = float(w_vf)
270
- G.graph["DNFR_WEIGHTS"] = mix
271
- default_compute_delta_nfr(G)
272
-
273
-
274
- def integrar_epi_euler(G, dt: float | None = None) -> None:
275
- update_epi_via_nodal_equation(G, dt=dt, method="euler")
276
-
277
-
278
- def aplicar_clamps_canonicos(nd: Dict[str, Any], G=None, node=None) -> None:
279
- eps_min = float((G.graph.get("EPI_MIN") if G is not None else DEFAULTS["EPI_MIN"]))
280
- eps_max = float((G.graph.get("EPI_MAX") if G is not None else DEFAULTS["EPI_MAX"]))
281
- vf_min = float((G.graph.get("VF_MIN") if G is not None else DEFAULTS["VF_MIN"]))
282
- vf_max = float((G.graph.get("VF_MAX") if G is not None else DEFAULTS["VF_MAX"]))
283
-
284
- epi = _get_attr(nd, ALIAS_EPI, 0.0)
285
- vf = _get_attr(nd, ALIAS_VF, 0.0)
286
- th = _get_attr(nd, ALIAS_THETA, 0.0)
287
-
288
- strict = bool((G.graph.get("VALIDATORS_STRICT") if G is not None else DEFAULTS.get("VALIDATORS_STRICT", False)))
289
- if strict and G is not None:
290
- hist = G.graph.setdefault("history", {}).setdefault("clamp_alerts", [])
291
- if epi < eps_min or epi > eps_max:
292
- hist.append({"node": node, "attr": "EPI", "value": float(epi)})
293
- if vf < vf_min or vf > vf_max:
294
- hist.append({"node": node, "attr": "VF", "value": float(vf)})
295
-
296
- _set_attr(nd, ALIAS_EPI, clamp(epi, eps_min, eps_max))
297
- _set_attr(nd, ALIAS_VF, clamp(vf, vf_min, vf_max))
298
- if (G.graph.get("THETA_WRAP") if G is not None else DEFAULTS["THETA_WRAP"]):
299
- # envolver fase
300
- _set_attr(nd, ALIAS_THETA, ((th + math.pi) % (2*math.pi) - math.pi))
301
-
302
-
303
- def validate_canon(G) -> None:
304
- """Aplica clamps canónicos a todos los nodos de ``G``.
305
-
306
- Envuelve fase y restringe ``EPI`` y ``νf`` a los rangos en ``G.graph``.
307
- Si ``VALIDATORS_STRICT`` está activo, registra alertas en ``history``.
308
- """
309
- for n in G.nodes():
310
- aplicar_clamps_canonicos(G.nodes[n], G, n)
311
- return G
312
-
313
-
314
- def coordinar_fase_global_vecinal(G, fuerza_global: float | None = None, fuerza_vecinal: float | None = None) -> None:
315
- """
316
- Ajusta fase con mezcla GLOBAL+VECINAL.
317
- Si no se pasan fuerzas explícitas, adapta kG/kL según estado (disonante / transición / estable).
318
- Estado se decide por R (Kuramoto) y carga glífica disruptiva reciente.
319
- """
320
- g = G.graph
321
- defaults = DEFAULTS
322
- hist = g.setdefault("history", {})
323
- hist_state = hist.setdefault("phase_state", [])
324
- hist_R = hist.setdefault("phase_R", [])
325
- hist_disr = hist.setdefault("phase_disr", [])
326
- # 0) Si hay fuerzas explícitas, usar y salir del modo adaptativo
327
- if (fuerza_global is not None) or (fuerza_vecinal is not None):
328
- kG = float(
329
- fuerza_global
330
- if fuerza_global is not None
331
- else g.get("PHASE_K_GLOBAL", defaults["PHASE_K_GLOBAL"])
332
- )
333
- kL = float(
334
- fuerza_vecinal
335
- if fuerza_vecinal is not None
336
- else g.get("PHASE_K_LOCAL", defaults["PHASE_K_LOCAL"])
337
- )
338
- else:
339
- # 1) Lectura de configuración
340
- cfg = g.get("PHASE_ADAPT", defaults.get("PHASE_ADAPT", {}))
341
- kG = float(g.get("PHASE_K_GLOBAL", defaults["PHASE_K_GLOBAL"]))
342
- kL = float(g.get("PHASE_K_LOCAL", defaults["PHASE_K_LOCAL"]))
343
-
344
- if bool(cfg.get("enabled", False)):
345
- # 2) Métricas actuales (no dependemos de history)
346
- R = orden_kuramoto(G)
347
- win = int(g.get("GLYPH_LOAD_WINDOW", defaults["GLYPH_LOAD_WINDOW"]))
348
- dist = carga_glifica(G, window=win)
349
- disr = float(dist.get("_disruptivos", 0.0)) if dist else 0.0
350
-
351
- # 3) Decidir estado
352
- R_hi = float(cfg.get("R_hi", 0.90)); R_lo = float(cfg.get("R_lo", 0.60))
353
- disr_hi = float(cfg.get("disr_hi", 0.50)); disr_lo = float(cfg.get("disr_lo", 0.25))
354
- if (R >= R_hi) and (disr <= disr_lo):
355
- state = "estable"
356
- elif (R <= R_lo) or (disr >= disr_hi):
357
- state = "disonante"
358
- else:
359
- state = "transicion"
360
-
361
- # 4) Objetivos y actualización suave (con saturación)
362
- kG_min = float(cfg.get("kG_min", 0.01)); kG_max = float(cfg.get("kG_max", 0.20))
363
- kL_min = float(cfg.get("kL_min", 0.05)); kL_max = float(cfg.get("kL_max", 0.25))
364
-
365
- if state == "disonante":
366
- kG_t = kG_max
367
- kL_t = 0.5 * (kL_min + kL_max) # local medio para no perder plasticidad
368
- elif state == "estable":
369
- kG_t = kG_min
370
- kL_t = kL_min
371
- else:
372
- kG_t = 0.5 * (kG_min + kG_max)
373
- kL_t = 0.5 * (kL_min + kL_max)
374
-
375
- up = float(cfg.get("up", 0.10))
376
- down = float(cfg.get("down", 0.07))
377
-
378
- def _step(curr, target, mn, mx):
379
- gain = up if target > curr else down
380
- nxt = curr + gain * (target - curr)
381
- return max(mn, min(mx, nxt))
382
-
383
- kG = _step(kG, kG_t, kG_min, kG_max)
384
- kL = _step(kL, kL_t, kL_min, kL_max)
385
-
386
- # 5) Persistir en G.graph y log de serie
387
- hist_state.append(state)
388
- hist_R.append(float(R))
389
- hist_disr.append(float(disr))
390
-
391
- g["PHASE_K_GLOBAL"] = kG
392
- g["PHASE_K_LOCAL"] = kL
393
- hist.setdefault("phase_kG", []).append(float(kG))
394
- hist.setdefault("phase_kL", []).append(float(kL))
395
-
396
- # 6) Fase GLOBAL (centroide) para empuje
397
- X = list(math.cos(_get_attr(G.nodes[n], ALIAS_THETA, 0.0)) for n in G.nodes())
398
- Y = list(math.sin(_get_attr(G.nodes[n], ALIAS_THETA, 0.0)) for n in G.nodes())
399
- if X:
400
- thG = math.atan2(sum(Y)/len(Y), sum(X)/len(X))
401
- else:
402
- thG = 0.0
403
-
404
- # 7) Aplicar corrección global+vecinal
405
- for n in G.nodes():
406
- nd = G.nodes[n]
407
- th = _get_attr(nd, ALIAS_THETA, 0.0)
408
- thL = fase_media(G, n)
409
- dG = ((thG - th + math.pi) % (2*math.pi) - math.pi)
410
- dL = ((thL - th + math.pi) % (2*math.pi) - math.pi)
411
- _set_attr(nd, ALIAS_THETA, th + kG*dG + kL*dL)
412
-
413
- # -------------------------
414
- # Adaptación de νf por coherencia
415
- # -------------------------
416
-
417
- def adaptar_vf_por_coherencia(G) -> None:
418
- """Ajusta νf hacia la media vecinal en nodos con estabilidad sostenida."""
419
- tau = int(G.graph.get("VF_ADAPT_TAU", DEFAULTS.get("VF_ADAPT_TAU", 5)))
420
- mu = float(G.graph.get("VF_ADAPT_MU", DEFAULTS.get("VF_ADAPT_MU", 0.1)))
421
- eps_dnfr = float(G.graph.get("EPS_DNFR_STABLE", DEFAULTS["EPS_DNFR_STABLE"]))
422
- thr_sel = G.graph.get("SELECTOR_THRESHOLDS", DEFAULTS.get("SELECTOR_THRESHOLDS", {}))
423
- thr_def = G.graph.get("GLYPH_THRESHOLDS", DEFAULTS.get("GLYPH_THRESHOLDS", {"hi": 0.66}))
424
- si_hi = float(thr_sel.get("si_hi", thr_def.get("hi", 0.66)))
425
- vf_min = float(G.graph.get("VF_MIN", DEFAULTS["VF_MIN"]))
426
- vf_max = float(G.graph.get("VF_MAX", DEFAULTS["VF_MAX"]))
427
-
428
- updates = {}
429
- for n in G.nodes():
430
- nd = G.nodes[n]
431
- Si = _get_attr(nd, ALIAS_SI, 0.0)
432
- dnfr = abs(_get_attr(nd, ALIAS_DNFR, 0.0))
433
- if Si >= si_hi and dnfr <= eps_dnfr:
434
- nd["stable_count"] = nd.get("stable_count", 0) + 1
435
- else:
436
- nd["stable_count"] = 0
437
- continue
438
-
439
- if nd["stable_count"] >= tau:
440
- vf = _get_attr(nd, ALIAS_VF, 0.0)
441
- vf_bar = media_vecinal(G, n, ALIAS_VF, default=vf)
442
- updates[n] = vf + mu * (vf_bar - vf)
443
-
444
- for n, vf_new in updates.items():
445
- _set_attr(G.nodes[n], ALIAS_VF, clamp(vf_new, vf_min, vf_max))
446
-
447
- # -------------------------
448
- # Selector glífico por defecto
449
- # -------------------------
450
-
451
- def default_glyph_selector(G, n) -> str:
452
- nd = G.nodes[n]
453
- # Umbrales desde configuración (fallback a DEFAULTS)
454
- thr = G.graph.get("GLYPH_THRESHOLDS", DEFAULTS.get("GLYPH_THRESHOLDS", {"hi": 0.66, "lo": 0.33, "dnfr": 1e-3}))
455
- hi = float(thr.get("hi", 0.66))
456
- lo = float(thr.get("lo", 0.33))
457
- tdnfr = float(thr.get("dnfr", 1e-3))
458
-
459
-
460
- Si = _get_attr(nd, ALIAS_SI, 0.5)
461
- dnfr = _get_attr(nd, ALIAS_DNFR, 0.0)
462
-
463
-
464
- if Si >= hi:
465
- return "I’L" # estabiliza
466
- if Si <= lo:
467
- return "O’Z" if abs(dnfr) > tdnfr else "Z’HIR"
468
- return "NA’V" if abs(dnfr) > tdnfr else "R’A"
469
-
470
-
471
- # -------------------------
472
- # Selector glífico multiobjetivo (paramétrico)
473
- # -------------------------
474
- def _norms_para_selector(G) -> dict:
475
- """Calcula y guarda en G.graph los máximos para normalizar |ΔNFR| y |d2EPI/dt2|."""
476
- dnfr_max = 0.0
477
- accel_max = 0.0
478
- for n in G.nodes():
479
- nd = G.nodes[n]
480
- dnfr_max = max(dnfr_max, abs(_get_attr(nd, ALIAS_DNFR, 0.0)))
481
- accel_max = max(accel_max, abs(_get_attr(nd, ALIAS_D2EPI, 0.0)))
482
- if dnfr_max <= 0: dnfr_max = 1.0
483
- if accel_max <= 0: accel_max = 1.0
484
- norms = {"dnfr_max": float(dnfr_max), "accel_max": float(accel_max)}
485
- G.graph["_sel_norms"] = norms
486
- return norms
487
-
488
-
489
- def _soft_grammar_prefilter(G, n, cand, dnfr, accel):
490
- """Gramática suave: evita repeticiones antes de la canónica."""
491
- gram = G.graph.get("GRAMMAR", DEFAULTS.get("GRAMMAR", {}))
492
- gwin = int(gram.get("window", 3))
493
- avoid = set(gram.get("avoid_repeats", []))
494
- force_dn = float(gram.get("force_dnfr", 0.60))
495
- force_ac = float(gram.get("force_accel", 0.60))
496
- fallbacks = gram.get("fallbacks", {})
497
- nd = G.nodes[n]
498
- if cand in avoid and reciente_glifo(nd, cand, gwin):
499
- if not (dnfr >= force_dn or accel >= force_ac):
500
- cand = fallbacks.get(cand, cand)
501
- return cand
502
-
503
- def parametric_glyph_selector(G, n) -> str:
504
- """Multiobjetivo: combina Si, |ΔNFR|_norm y |accel|_norm + histéresis.
505
- Reglas base:
506
- - Si alto ⇒ I’L
507
- - Si bajo ⇒ O’Z si |ΔNFR| alto; Z’HIR si |ΔNFR| bajo; T’HOL si hay mucha aceleración
508
- - Si medio ⇒ NA’V si |ΔNFR| alto (o accel alta), si no R’A
509
- """
510
- nd = G.nodes[n]
511
- thr = G.graph.get("SELECTOR_THRESHOLDS", DEFAULTS["SELECTOR_THRESHOLDS"])
512
- si_hi, si_lo = float(thr.get("si_hi", 0.66)), float(thr.get("si_lo", 0.33))
513
- dnfr_hi, dnfr_lo = float(thr.get("dnfr_hi", 0.5)), float(thr.get("dnfr_lo", 0.1))
514
- acc_hi, acc_lo = float(thr.get("accel_hi", 0.5)), float(thr.get("accel_lo", 0.1))
515
- margin = float(G.graph.get("GLYPH_SELECTOR_MARGIN", DEFAULTS["GLYPH_SELECTOR_MARGIN"]))
516
-
517
- # Normalizadores por paso
518
- norms = G.graph.get("_sel_norms") or _norms_para_selector(G)
519
- dnfr_max = float(norms.get("dnfr_max", 1.0))
520
- acc_max = float(norms.get("accel_max", 1.0))
521
-
522
- # Lecturas nodales
523
- Si = clamp01(_get_attr(nd, ALIAS_SI, 0.5))
524
- dnfr = abs(_get_attr(nd, ALIAS_DNFR, 0.0)) / dnfr_max
525
- accel = abs(_get_attr(nd, ALIAS_D2EPI, 0.0)) / acc_max
526
-
527
- W = G.graph.get("SELECTOR_WEIGHTS", DEFAULTS["SELECTOR_WEIGHTS"])
528
- w_si = float(W.get("w_si", 0.5)); w_dn = float(W.get("w_dnfr", 0.3)); w_ac = float(W.get("w_accel", 0.2))
529
- s = max(1e-9, w_si + w_dn + w_ac)
530
- w_si, w_dn, w_ac = w_si/s, w_dn/s, w_ac/s
531
- score = w_si*Si + w_dn*(1.0 - dnfr) + w_ac*(1.0 - accel)
532
- # usar score como desempate/override suave: si score>0.66 ⇒ inclinar a I’L; <0.33 ⇒ inclinar a O’Z/Z’HIR
533
-
534
- # Decisión base
535
- if Si >= si_hi:
536
- cand = "I’L"
537
- elif Si <= si_lo:
538
- if accel >= acc_hi:
539
- cand = "T’HOL"
540
- else:
541
- cand = "O’Z" if dnfr >= dnfr_hi else "Z’HIR"
542
- else:
543
- # Zona intermedia: transición si el campo "pide" reorganizar (dnfr/accel altos)
544
- if dnfr >= dnfr_hi or accel >= acc_hi:
545
- cand = "NA’V"
546
- else:
547
- cand = "R’A"
548
-
549
- # --- Histéresis del selector: si está cerca de umbrales, conserva el glifo reciente ---
550
- # Medimos "certeza" como distancia mínima a los umbrales relevantes
551
- d_si = min(abs(Si - si_hi), abs(Si - si_lo))
552
- d_dn = min(abs(dnfr - dnfr_hi), abs(dnfr - dnfr_lo))
553
- d_ac = min(abs(accel - acc_hi), abs(accel - acc_lo))
554
- certeza = min(d_si, d_dn, d_ac)
555
- if certeza < margin:
556
- hist = nd.get("hist_glifos")
557
- if hist:
558
- prev = list(hist)[-1]
559
- if isinstance(prev, str) and prev in ("I’L","O’Z","Z’HIR","T’HOL","NA’V","R’A"):
560
- return prev
561
-
562
- # Penalización por falta de avance en σ/Si si se repite glifo
563
- prev = None
564
- hist_prev = nd.get("hist_glifos")
565
- if hist_prev:
566
- prev = list(hist_prev)[-1]
567
- if prev == cand:
568
- delta_si = _get_attr(nd, ALIAS_dSI, 0.0)
569
- h = G.graph.get("history", {})
570
- sig = h.get("sense_sigma_mag", [])
571
- delta_sigma = sig[-1] - sig[-2] if len(sig) >= 2 else 0.0
572
- if delta_si <= 0.0 and delta_sigma <= 0.0:
573
- score -= 0.05
574
-
575
- # Override suave guiado por score (solo si NO cayó la histéresis arriba)
576
- # Regla: score>=0.66 inclina a I’L; score<=0.33 inclina a O’Z/Z’HIR
577
- try:
578
- if score >= 0.66 and cand in ("NA’V","R’A","Z’HIR","O’Z"):
579
- cand = "I’L"
580
- elif score <= 0.33 and cand in ("NA’V","R’A","I’L"):
581
- cand = "O’Z" if dnfr >= dnfr_lo else "Z’HIR"
582
- except NameError:
583
- pass
584
-
585
- cand = _soft_grammar_prefilter(G, n, cand, dnfr, accel)
586
- return cand
587
-
588
- # -------------------------
589
- # Step / run
590
- # -------------------------
591
-
592
- def step(G, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool = True) -> None:
593
- # Contexto inicial
594
- _hist0 = G.graph.setdefault("history", {"C_steps": []})
595
- step_idx = len(_hist0.get("C_steps", []))
596
- invoke_callbacks(G, "before_step", {"step": step_idx, "dt": dt, "use_Si": use_Si, "apply_glyphs": apply_glyphs})
597
-
598
- # 1) ΔNFR (campo)
599
- compute_dnfr_cb = G.graph.get("compute_delta_nfr", default_compute_delta_nfr)
600
- compute_dnfr_cb(G)
601
-
602
- # 2) (opcional) Si
603
- if use_Si:
604
- from .helpers import compute_Si
605
- compute_Si(G, inplace=True)
606
-
607
- # 2b) Normalizadores para selector paramétrico (por paso)
608
- _norms_para_selector(G) # no molesta si luego se usa el selector por defecto
609
-
610
- # 3) Selección glífica + aplicación (con lags obligatorios A’L/E’N)
611
- if apply_glyphs:
612
- selector = G.graph.get("glyph_selector", default_glyph_selector)
613
- from .operators import aplicar_glifo
614
- window = int(G.graph.get("GLYPH_HYSTERESIS_WINDOW", DEFAULTS["GLYPH_HYSTERESIS_WINDOW"]))
615
- use_canon = bool(G.graph.get("GRAMMAR_CANON", DEFAULTS.get("GRAMMAR_CANON", {})).get("enabled", False))
616
-
617
- al_max = int(G.graph.get("AL_MAX_LAG", DEFAULTS["AL_MAX_LAG"]))
618
- en_max = int(G.graph.get("EN_MAX_LAG", DEFAULTS["EN_MAX_LAG"]))
619
- h_al = _hist0.setdefault("since_AL", {})
620
- h_en = _hist0.setdefault("since_EN", {})
621
-
622
- for n in G.nodes():
623
- h_al[n] = int(h_al.get(n, 0)) + 1
624
- h_en[n] = int(h_en.get(n, 0)) + 1
625
-
626
- if h_al[n] > al_max:
627
- g = AL
628
- elif h_en[n] > en_max:
629
- g = EN
630
- else:
631
- g = selector(G, n)
632
- if use_canon:
633
- g = enforce_canonical_grammar(G, n, g)
634
-
635
- aplicar_glifo(G, n, g, window=window)
636
- if use_canon:
637
- on_applied_glifo(G, n, g)
638
-
639
- if g == AL:
640
- h_al[n] = 0
641
- h_en[n] = min(h_en[n], en_max)
642
- elif g == EN:
643
- h_en[n] = 0
644
-
645
- # 4) Ecuación nodal
646
- _dt = float(G.graph.get("DT", DEFAULTS["DT"])) if dt is None else float(dt)
647
- method = G.graph.get("INTEGRATOR_METHOD", DEFAULTS.get("INTEGRATOR_METHOD", "euler"))
648
- update_epi_via_nodal_equation(G, dt=_dt, method=method)
649
-
650
- # 5) Clamps
651
- for n in G.nodes():
652
- aplicar_clamps_canonicos(G.nodes[n], G, n)
653
-
654
- # 6) Coordinación de fase
655
- coordinar_fase_global_vecinal(G, None, None)
656
-
657
- # 6b) Adaptación de νf por coherencia
658
- adaptar_vf_por_coherencia(G)
659
-
660
- # 7) Observadores ligeros
661
- _update_history(G)
662
- # dynamics.py — dentro de step(), justo antes del punto 8)
663
- tau_g = int(G.graph.get("REMESH_TAU_GLOBAL", G.graph.get("REMESH_TAU", DEFAULTS["REMESH_TAU_GLOBAL"])))
664
- tau_l = int(G.graph.get("REMESH_TAU_LOCAL", G.graph.get("REMESH_TAU", DEFAULTS["REMESH_TAU_LOCAL"])))
665
- tau = max(tau_g, tau_l)
666
- maxlen = max(2 * tau + 5, 64)
667
- epi_hist = G.graph.get("_epi_hist")
668
- if not isinstance(epi_hist, deque) or epi_hist.maxlen != maxlen:
669
- epi_hist = deque(list(epi_hist or [])[-maxlen:], maxlen=maxlen)
670
- G.graph["_epi_hist"] = epi_hist
671
- epi_hist.append({n: _get_attr(G.nodes[n], ALIAS_EPI, 0.0) for n in G.nodes()})
672
-
673
- # 8) RE’MESH condicionado
674
- aplicar_remesh_si_estabilizacion_global(G)
675
-
676
- # 8b) Validadores de invariantes
677
- from .validators import run_validators
678
- run_validators(G)
679
-
680
- # Contexto final (últimas métricas del paso)
681
- h = G.graph.get("history", {})
682
- ctx = {"step": step_idx}
683
- if h.get("C_steps"): ctx["C"] = h["C_steps"][-1]
684
- if h.get("stable_frac"): ctx["stable_frac"] = h["stable_frac"][-1]
685
- if h.get("phase_sync"): ctx["phase_sync"] = h["phase_sync"][-1]
686
- if h.get("glyph_load_disr"): ctx["glyph_disr"] = h["glyph_load_disr"][-1]
687
- if h.get("Si_mean"): ctx["Si_mean"] = h["Si_mean"][-1]
688
- invoke_callbacks(G, "after_step", ctx)
689
-
690
-
691
- def run(G, steps: int, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool = True) -> None:
692
- for _ in range(int(steps)):
693
- step(G, dt=dt, use_Si=use_Si, apply_glyphs=apply_glyphs)
694
- # Early-stop opcional
695
- stop_cfg = G.graph.get("STOP_EARLY", DEFAULTS.get("STOP_EARLY", {"enabled": False}))
696
- if stop_cfg and stop_cfg.get("enabled", False):
697
- w = int(stop_cfg.get("window", 25))
698
- frac = float(stop_cfg.get("fraction", 0.90))
699
- hist = G.graph.setdefault("history", {"stable_frac": []})
700
- series = hist.get("stable_frac", [])
701
- if len(series) >= w and all(v >= frac for v in series[-w:]):
702
- break
703
-
704
-
705
- # -------------------------
706
- # Historial simple
707
- # -------------------------
708
-
709
- def _update_history(G) -> None:
710
- hist = G.graph.setdefault("history", {})
711
- for k in (
712
- "C_steps", "stable_frac", "phase_sync", "glyph_load_estab", "glyph_load_disr",
713
- "Si_mean", "Si_hi_frac", "Si_lo_frac", "delta_Si", "B"
714
- ):
715
- hist.setdefault(k, [])
716
-
717
- # Proxy de coherencia C(t)
718
- dnfr_mean = list_mean(abs(_get_attr(G.nodes[n], ALIAS_DNFR, 0.0)) for n in G.nodes())
719
- dEPI_mean = list_mean(abs(_get_attr(G.nodes[n], ALIAS_dEPI, 0.0)) for n in G.nodes())
720
- C = 1.0 / (1.0 + dnfr_mean + dEPI_mean)
721
- hist["C_steps"].append(C)
722
-
723
- # --- W̄: coherencia promedio en ventana ---
724
- wbar_w = int(G.graph.get("WBAR_WINDOW", DEFAULTS.get("WBAR_WINDOW", 25)))
725
- cs = hist["C_steps"]
726
- if cs:
727
- w = min(len(cs), max(1, wbar_w))
728
- wbar = sum(cs[-w:]) / w
729
- hist.setdefault("W_bar", []).append(wbar)
730
-
731
- eps_dnfr = float(G.graph.get("EPS_DNFR_STABLE", DEFAULTS["EPS_DNFR_STABLE"]))
732
- eps_depi = float(G.graph.get("EPS_DEPI_STABLE", DEFAULTS["EPS_DEPI_STABLE"]))
733
- stables = 0
734
- total = max(1, G.number_of_nodes())
735
- dt = float(G.graph.get("DT", DEFAULTS.get("DT", 1.0))) or 1.0
736
- delta_si_acc = []
737
- B_acc = []
738
- for n in G.nodes():
739
- nd = G.nodes[n]
740
- if abs(_get_attr(nd, ALIAS_DNFR, 0.0)) <= eps_dnfr and abs(_get_attr(nd, ALIAS_dEPI, 0.0)) <= eps_depi:
741
- stables += 1
742
-
743
- # δSi por nodo
744
- Si_curr = _get_attr(nd, ALIAS_SI, 0.0)
745
- Si_prev = nd.get("_prev_Si", Si_curr)
746
- dSi = Si_curr - Si_prev
747
- nd["_prev_Si"] = Si_curr
748
- _set_attr(nd, ALIAS_dSI, dSi)
749
- delta_si_acc.append(dSi)
750
-
751
- # Bifurcación B = ∂²νf/∂t²
752
- vf_curr = _get_attr(nd, ALIAS_VF, 0.0)
753
- vf_prev = nd.get("_prev_vf", vf_curr)
754
- dvf_dt = (vf_curr - vf_prev) / dt
755
- dvf_prev = nd.get("_prev_dvf", dvf_dt)
756
- B = (dvf_dt - dvf_prev) / dt
757
- nd["_prev_vf"] = vf_curr
758
- nd["_prev_dvf"] = dvf_dt
759
- _set_attr(nd, ALIAS_dVF, dvf_dt)
760
- _set_attr(nd, ALIAS_D2VF, B)
761
- B_acc.append(B)
762
-
763
- hist["stable_frac"].append(stables/total)
764
- hist["delta_Si"].append(list_mean(delta_si_acc, 0.0))
765
- hist["B"].append(list_mean(B_acc, 0.0))
766
- # --- nuevas series: sincronía de fase y carga glífica ---
767
- try:
768
- ps = sincronía_fase(G) # [0,1], más alto = más en fase
769
- hist["phase_sync"].append(ps)
770
- R = orden_kuramoto(G)
771
- hist.setdefault("kuramoto_R", []).append(R)
772
- win = int(G.graph.get("GLYPH_LOAD_WINDOW", DEFAULTS["GLYPH_LOAD_WINDOW"]))
773
- gl = carga_glifica(G, window=win) # proporciones
774
- hist["glyph_load_estab"].append(gl.get("_estabilizadores", 0.0))
775
- hist["glyph_load_disr"].append(gl.get("_disruptivos", 0.0))
776
- # --- Σ⃗(t): vector de sentido a partir de la distribución glífica ---
777
- sig = sigma_vector(G, window=win)
778
- hist.setdefault("sense_sigma_x", []).append(sig.get("x", 0.0))
779
- hist.setdefault("sense_sigma_y", []).append(sig.get("y", 0.0))
780
- hist.setdefault("sense_sigma_mag", []).append(sig.get("mag", 0.0))
781
- hist.setdefault("sense_sigma_angle", []).append(sig.get("angle", 0.0))
782
- # --- ι(t): intensidad de activación coherente (proxy) ---
783
- # Definición operativa: iota = C(t) * stable_frac(t)
784
- if hist.get("C_steps") and hist.get("stable_frac"):
785
- hist.setdefault("iota", []).append(hist["C_steps"][-1] * hist["stable_frac"][-1])
786
- except Exception:
787
- # observadores son opcionales; si no están, no rompemos el bucle
788
- pass
789
-
790
- # --- nuevas series: Si agregado (media y colas) ---
791
- try:
792
- import math
793
- sis = []
794
- for n in G.nodes():
795
- sis.append(_get_attr(G.nodes[n], ALIAS_SI, float("nan")))
796
- sis = [s for s in sis if not math.isnan(s)]
797
- if sis:
798
- si_mean = list_mean(sis, 0.0)
799
- hist["Si_mean"].append(si_mean)
800
- # umbrales preferentes del selector paramétrico; fallback a los del selector simple
801
- thr_sel = G.graph.get("SELECTOR_THRESHOLDS", DEFAULTS.get("SELECTOR_THRESHOLDS", {}))
802
- thr_def = G.graph.get("GLYPH_THRESHOLDS", DEFAULTS.get("GLYPH_THRESHOLDS", {"hi":0.66,"lo":0.33}))
803
- si_hi = float(thr_sel.get("si_hi", thr_def.get("hi", 0.66)))
804
- si_lo = float(thr_sel.get("si_lo", thr_def.get("lo", 0.33)))
805
- n = len(sis)
806
- hist["Si_hi_frac"].append(sum(1 for s in sis if s >= si_hi) / n)
807
- hist["Si_lo_frac"].append(sum(1 for s in sis if s <= si_lo) / n)
808
- else:
809
- hist["Si_mean"].append(0.0)
810
- hist["Si_hi_frac"].append(0.0)
811
- hist["Si_lo_frac"].append(0.0)
812
- except Exception:
813
- # si aún no se calculó Si este paso, no interrumpimos
814
- pass