tnfr 4.1.0__py3-none-any.whl → 4.5.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 +34 -4
- tnfr/cli.py +138 -9
- tnfr/config.py +41 -0
- tnfr/constants.py +102 -41
- tnfr/dynamics.py +255 -49
- tnfr/gamma.py +35 -8
- tnfr/helpers.py +50 -17
- tnfr/metrics.py +416 -30
- tnfr/node.py +202 -0
- tnfr/operators.py +341 -146
- tnfr/presets.py +3 -0
- tnfr/scenarios.py +9 -3
- tnfr/sense.py +6 -21
- tnfr/structural.py +201 -0
- tnfr/trace.py +4 -20
- tnfr/types.py +2 -1
- tnfr/validators.py +38 -0
- {tnfr-4.1.0.dist-info → tnfr-4.5.0.dist-info}/METADATA +10 -4
- tnfr-4.5.0.dist-info/RECORD +28 -0
- tnfr-4.1.0.dist-info/RECORD +0 -24
- {tnfr-4.1.0.dist-info → tnfr-4.5.0.dist-info}/WHEEL +0 -0
- {tnfr-4.1.0.dist-info → tnfr-4.5.0.dist-info}/entry_points.txt +0 -0
- {tnfr-4.1.0.dist-info → tnfr-4.5.0.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.1.0.dist-info → tnfr-4.5.0.dist-info}/top_level.txt +0 -0
tnfr/dynamics.py
CHANGED
|
@@ -11,19 +11,29 @@ Incluye:
|
|
|
11
11
|
- default_glyph_selector, step, run
|
|
12
12
|
"""
|
|
13
13
|
from __future__ import annotations
|
|
14
|
-
from typing import Dict, Any, Iterable
|
|
14
|
+
from typing import Dict, Any, Iterable, Literal
|
|
15
15
|
import math
|
|
16
16
|
from collections import deque
|
|
17
17
|
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
|
|
22
|
-
|
|
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
|
+
)
|
|
23
33
|
from .gamma import eval_gamma
|
|
24
34
|
from .helpers import (
|
|
25
35
|
clamp, clamp01, list_mean, phase_distance,
|
|
26
|
-
_get_attr, _set_attr, media_vecinal, fase_media,
|
|
36
|
+
_get_attr, _set_attr, _get_attr_str, _set_attr_str, media_vecinal, fase_media,
|
|
27
37
|
invoke_callbacks, reciente_glifo
|
|
28
38
|
)
|
|
29
39
|
|
|
@@ -32,20 +42,21 @@ from .helpers import (
|
|
|
32
42
|
# -------------------------
|
|
33
43
|
|
|
34
44
|
def _write_dnfr_metadata(G, *, weights: dict, hook_name: str, note: str | None = None) -> None:
|
|
35
|
-
"""Escribe en G.graph un bloque _DNFR_META con la mezcla y el nombre del hook.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
43
54
|
meta = {
|
|
44
55
|
"hook": hook_name,
|
|
45
56
|
"weights_raw": dict(weights),
|
|
46
|
-
"weights_norm": {
|
|
47
|
-
"components": [k for k, v in
|
|
48
|
-
"doc": "ΔNFR =
|
|
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",
|
|
49
60
|
}
|
|
50
61
|
if note:
|
|
51
62
|
meta["note"] = str(note)
|
|
@@ -54,26 +65,35 @@ def _write_dnfr_metadata(G, *, weights: dict, hook_name: str, note: str | None =
|
|
|
54
65
|
|
|
55
66
|
|
|
56
67
|
def default_compute_delta_nfr(G) -> None:
|
|
57
|
-
"""Calcula ΔNFR mezclando gradientes de fase, EPI
|
|
68
|
+
"""Calcula ΔNFR mezclando gradientes de fase, EPI, νf y un término topológico."""
|
|
58
69
|
w = G.graph.get("DNFR_WEIGHTS", DEFAULTS["DNFR_WEIGHTS"]) # dict
|
|
59
70
|
w_phase = float(w.get("phase", 0.34))
|
|
60
71
|
w_epi = float(w.get("epi", 0.33))
|
|
61
72
|
w_vf = float(w.get("vf", 0.33))
|
|
62
|
-
|
|
73
|
+
w_topo = float(w.get("topo", 0.0))
|
|
74
|
+
s = w_phase + w_epi + w_vf + w_topo
|
|
63
75
|
if s <= 0:
|
|
64
76
|
w_phase = w_epi = w_vf = 1/3
|
|
77
|
+
w_topo = 0.0
|
|
78
|
+
s = 1.0
|
|
65
79
|
else:
|
|
66
|
-
w_phase, w_epi, w_vf = w_phase/s, w_epi/s, w_vf/s
|
|
80
|
+
w_phase, w_epi, w_vf, w_topo = (w_phase/s, w_epi/s, w_vf/s, w_topo/s)
|
|
67
81
|
|
|
68
82
|
# Documentar mezcla y hook activo
|
|
69
|
-
_write_dnfr_metadata(
|
|
70
|
-
|
|
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
|
+
|
|
71
91
|
for n in G.nodes():
|
|
72
92
|
nd = G.nodes[n]
|
|
73
93
|
th_i = _get_attr(nd, ALIAS_THETA, 0.0)
|
|
74
94
|
th_bar = fase_media(G, n)
|
|
75
95
|
# Gradiente de fase: empuja hacia la fase media (signo envuelto)
|
|
76
|
-
g_phase = -
|
|
96
|
+
g_phase = -((th_i - th_bar + math.pi) % (2 * math.pi) - math.pi) / math.pi # ~[-1,1]
|
|
77
97
|
|
|
78
98
|
epi_i = _get_attr(nd, ALIAS_EPI, 0.0)
|
|
79
99
|
epi_bar = media_vecinal(G, n, ALIAS_EPI, default=epi_i)
|
|
@@ -83,7 +103,14 @@ def default_compute_delta_nfr(G) -> None:
|
|
|
83
103
|
vf_bar = media_vecinal(G, n, ALIAS_VF, default=vf_i)
|
|
84
104
|
g_vf = (vf_bar - vf_i)
|
|
85
105
|
|
|
86
|
-
|
|
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
|
|
87
114
|
_set_attr(nd, ALIAS_DNFR, dnfr)
|
|
88
115
|
|
|
89
116
|
def set_delta_nfr_hook(G, func, *, name: str | None = None, note: str | None = None) -> None:
|
|
@@ -103,9 +130,9 @@ def dnfr_phase_only(G) -> None:
|
|
|
103
130
|
nd = G.nodes[n]
|
|
104
131
|
th_i = _get_attr(nd, ALIAS_THETA, 0.0)
|
|
105
132
|
th_bar = fase_media(G, n)
|
|
106
|
-
g_phase = -
|
|
133
|
+
g_phase = -((th_i - th_bar + math.pi) % (2 * math.pi) - math.pi) / math.pi
|
|
107
134
|
_set_attr(nd, ALIAS_DNFR, g_phase)
|
|
108
|
-
_write_dnfr_metadata(G, weights={"phase":1.0
|
|
135
|
+
_write_dnfr_metadata(G, weights={"phase": 1.0}, hook_name="dnfr_phase_only", note="Hook de ejemplo.")
|
|
109
136
|
|
|
110
137
|
def dnfr_epi_vf_mixed(G) -> None:
|
|
111
138
|
"""Ejemplo: ΔNFR sin fase, mezclando EPI y νf."""
|
|
@@ -120,11 +147,40 @@ def dnfr_epi_vf_mixed(G) -> None:
|
|
|
120
147
|
_set_attr(nd, ALIAS_DNFR, 0.5*g_epi + 0.5*g_vf)
|
|
121
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.")
|
|
122
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
|
+
|
|
123
173
|
# -------------------------
|
|
124
174
|
# Ecuación nodal
|
|
125
175
|
# -------------------------
|
|
126
176
|
|
|
127
|
-
def update_epi_via_nodal_equation(
|
|
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:
|
|
128
184
|
"""Ecuación nodal TNFR.
|
|
129
185
|
|
|
130
186
|
Implementa la forma extendida de la ecuación nodal:
|
|
@@ -155,18 +211,50 @@ def update_epi_via_nodal_equation(G, *, dt: float = None, t: float | None = None
|
|
|
155
211
|
t = float(G.graph.get("_t", 0.0))
|
|
156
212
|
else:
|
|
157
213
|
t = float(t)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
170
258
|
|
|
171
259
|
|
|
172
260
|
# -------------------------
|
|
@@ -184,7 +272,7 @@ def aplicar_dnfr_campo(G, w_theta=None, w_epi=None, w_vf=None) -> None:
|
|
|
184
272
|
|
|
185
273
|
|
|
186
274
|
def integrar_epi_euler(G, dt: float | None = None) -> None:
|
|
187
|
-
update_epi_via_nodal_equation(G, dt=dt)
|
|
275
|
+
update_epi_via_nodal_equation(G, dt=dt, method="euler")
|
|
188
276
|
|
|
189
277
|
|
|
190
278
|
def aplicar_clamps_canonicos(nd: Dict[str, Any], G=None, node=None) -> None:
|
|
@@ -212,6 +300,17 @@ def aplicar_clamps_canonicos(nd: Dict[str, Any], G=None, node=None) -> None:
|
|
|
212
300
|
_set_attr(nd, ALIAS_THETA, ((th + math.pi) % (2*math.pi) - math.pi))
|
|
213
301
|
|
|
214
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
|
+
|
|
215
314
|
def coordinar_fase_global_vecinal(G, fuerza_global: float | None = None, fuerza_vecinal: float | None = None) -> None:
|
|
216
315
|
"""
|
|
217
316
|
Ajusta fase con mezcla GLOBAL+VECINAL.
|
|
@@ -311,6 +410,40 @@ def coordinar_fase_global_vecinal(G, fuerza_global: float | None = None, fuerza_
|
|
|
311
410
|
dL = ((thL - th + math.pi) % (2*math.pi) - math.pi)
|
|
312
411
|
_set_attr(nd, ALIAS_THETA, th + kG*dG + kL*dL)
|
|
313
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
|
+
|
|
314
447
|
# -------------------------
|
|
315
448
|
# Selector glífico por defecto
|
|
316
449
|
# -------------------------
|
|
@@ -425,6 +558,19 @@ def parametric_glyph_selector(G, n) -> str:
|
|
|
425
558
|
prev = list(hist)[-1]
|
|
426
559
|
if isinstance(prev, str) and prev in ("I’L","O’Z","Z’HIR","T’HOL","NA’V","R’A"):
|
|
427
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
|
|
428
574
|
|
|
429
575
|
# Override suave guiado por score (solo si NO cayó la histéresis arriba)
|
|
430
576
|
# Regla: score>=0.66 inclina a I’L; score<=0.33 inclina a O’Z/Z’HIR
|
|
@@ -461,21 +607,45 @@ def step(G, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool
|
|
|
461
607
|
# 2b) Normalizadores para selector paramétrico (por paso)
|
|
462
608
|
_norms_para_selector(G) # no molesta si luego se usa el selector por defecto
|
|
463
609
|
|
|
464
|
-
# 3) Selección glífica + aplicación
|
|
610
|
+
# 3) Selección glífica + aplicación (con lags obligatorios A’L/E’N)
|
|
465
611
|
if apply_glyphs:
|
|
466
612
|
selector = G.graph.get("glyph_selector", default_glyph_selector)
|
|
467
613
|
from .operators import aplicar_glifo
|
|
468
614
|
window = int(G.graph.get("GLYPH_HYSTERESIS_WINDOW", DEFAULTS["GLYPH_HYSTERESIS_WINDOW"]))
|
|
469
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
|
+
|
|
470
622
|
for n in G.nodes():
|
|
471
|
-
|
|
472
|
-
|
|
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
|
|
473
630
|
else:
|
|
474
631
|
g = selector(G, n)
|
|
475
|
-
|
|
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
|
|
476
644
|
|
|
477
645
|
# 4) Ecuación nodal
|
|
478
|
-
|
|
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)
|
|
479
649
|
|
|
480
650
|
# 5) Clamps
|
|
481
651
|
for n in G.nodes():
|
|
@@ -484,10 +654,15 @@ def step(G, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool
|
|
|
484
654
|
# 6) Coordinación de fase
|
|
485
655
|
coordinar_fase_global_vecinal(G, None, None)
|
|
486
656
|
|
|
657
|
+
# 6b) Adaptación de νf por coherencia
|
|
658
|
+
adaptar_vf_por_coherencia(G)
|
|
659
|
+
|
|
487
660
|
# 7) Observadores ligeros
|
|
488
661
|
_update_history(G)
|
|
489
662
|
# dynamics.py — dentro de step(), justo antes del punto 8)
|
|
490
|
-
|
|
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)
|
|
491
666
|
maxlen = max(2 * tau + 5, 64)
|
|
492
667
|
epi_hist = G.graph.get("_epi_hist")
|
|
493
668
|
if not isinstance(epi_hist, deque) or epi_hist.maxlen != maxlen:
|
|
@@ -498,6 +673,10 @@ def step(G, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool
|
|
|
498
673
|
# 8) RE’MESH condicionado
|
|
499
674
|
aplicar_remesh_si_estabilizacion_global(G)
|
|
500
675
|
|
|
676
|
+
# 8b) Validadores de invariantes
|
|
677
|
+
from .validators import run_validators
|
|
678
|
+
run_validators(G)
|
|
679
|
+
|
|
501
680
|
# Contexto final (últimas métricas del paso)
|
|
502
681
|
h = G.graph.get("history", {})
|
|
503
682
|
ctx = {"step": step_idx}
|
|
@@ -528,11 +707,12 @@ def run(G, steps: int, *, dt: float | None = None, use_Si: bool = True, apply_gl
|
|
|
528
707
|
# -------------------------
|
|
529
708
|
|
|
530
709
|
def _update_history(G) -> None:
|
|
531
|
-
hist = G.graph.setdefault("history", {
|
|
532
|
-
|
|
533
|
-
"
|
|
534
|
-
"Si_mean"
|
|
535
|
-
|
|
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, [])
|
|
536
716
|
|
|
537
717
|
# Proxy de coherencia C(t)
|
|
538
718
|
dnfr_mean = list_mean(abs(_get_attr(G.nodes[n], ALIAS_DNFR, 0.0)) for n in G.nodes())
|
|
@@ -552,11 +732,37 @@ def _update_history(G) -> None:
|
|
|
552
732
|
eps_depi = float(G.graph.get("EPS_DEPI_STABLE", DEFAULTS["EPS_DEPI_STABLE"]))
|
|
553
733
|
stables = 0
|
|
554
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 = []
|
|
555
738
|
for n in G.nodes():
|
|
556
739
|
nd = G.nodes[n]
|
|
557
740
|
if abs(_get_attr(nd, ALIAS_DNFR, 0.0)) <= eps_dnfr and abs(_get_attr(nd, ALIAS_dEPI, 0.0)) <= eps_depi:
|
|
558
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
|
+
|
|
559
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))
|
|
560
766
|
# --- nuevas series: sincronía de fase y carga glífica ---
|
|
561
767
|
try:
|
|
562
768
|
ps = sincronía_fase(G) # [0,1], más alto = más en fase
|
tnfr/gamma.py
CHANGED
|
@@ -23,14 +23,7 @@ import math
|
|
|
23
23
|
import cmath
|
|
24
24
|
|
|
25
25
|
from .constants import ALIAS_THETA
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _get_attr(nd: Dict[str, Any], aliases, default: float = 0.0) -> float:
|
|
29
|
-
"""Obtiene el primer atributo presente en nd según aliases."""
|
|
30
|
-
for k in aliases:
|
|
31
|
-
if k in nd:
|
|
32
|
-
return nd[k]
|
|
33
|
-
return default
|
|
26
|
+
from .helpers import _get_attr
|
|
34
27
|
|
|
35
28
|
|
|
36
29
|
def kuramoto_R_psi(G) -> Tuple[float, float]:
|
|
@@ -83,10 +76,44 @@ def gamma_kuramoto_bandpass(G, node, t, cfg: Dict[str, Any]) -> float:
|
|
|
83
76
|
return beta * R * (1.0 - R) * sgn
|
|
84
77
|
|
|
85
78
|
|
|
79
|
+
def gamma_kuramoto_tanh(G, node, t, cfg: Dict[str, Any]) -> float:
|
|
80
|
+
"""Acoplamiento saturante tipo tanh para Γi(R).
|
|
81
|
+
|
|
82
|
+
Fórmula: Γ = β · tanh(k·(R - R0)) · cos(θ_i - ψ)
|
|
83
|
+
- β: ganancia del acoplamiento
|
|
84
|
+
- k: pendiente de la tanh (cuán rápido satura)
|
|
85
|
+
- R0: umbral de activación
|
|
86
|
+
"""
|
|
87
|
+
beta = float(cfg.get("beta", 0.0))
|
|
88
|
+
k = float(cfg.get("k", 1.0))
|
|
89
|
+
R0 = float(cfg.get("R0", 0.0))
|
|
90
|
+
R, psi = kuramoto_R_psi(G)
|
|
91
|
+
th_i = _get_attr(G.nodes[node], ALIAS_THETA, 0.0)
|
|
92
|
+
return beta * math.tanh(k * (R - R0)) * math.cos(th_i - psi)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def gamma_harmonic(G, node, t, cfg: Dict[str, Any]) -> float:
|
|
96
|
+
"""Forzamiento armónico coherente con el campo global de fase.
|
|
97
|
+
|
|
98
|
+
Fórmula: Γ = β · sin(ω·t + φ) · cos(θ_i - ψ)
|
|
99
|
+
- β: ganancia del acoplamiento
|
|
100
|
+
- ω: frecuencia angular del forzante
|
|
101
|
+
- φ: fase inicial del forzante
|
|
102
|
+
"""
|
|
103
|
+
beta = float(cfg.get("beta", 0.0))
|
|
104
|
+
omega = float(cfg.get("omega", 1.0))
|
|
105
|
+
phi = float(cfg.get("phi", 0.0))
|
|
106
|
+
R, psi = kuramoto_R_psi(G)
|
|
107
|
+
th = _get_attr(G.nodes[node], ALIAS_THETA, 0.0)
|
|
108
|
+
return beta * math.sin(omega * t + phi) * math.cos(th - psi)
|
|
109
|
+
|
|
110
|
+
|
|
86
111
|
GAMMA_REGISTRY = {
|
|
87
112
|
"none": gamma_none,
|
|
88
113
|
"kuramoto_linear": gamma_kuramoto_linear,
|
|
89
114
|
"kuramoto_bandpass": gamma_kuramoto_bandpass,
|
|
115
|
+
"kuramoto_tanh": gamma_kuramoto_tanh,
|
|
116
|
+
"harmonic": gamma_harmonic,
|
|
90
117
|
}
|
|
91
118
|
|
|
92
119
|
|
tnfr/helpers.py
CHANGED
|
@@ -14,7 +14,7 @@ try:
|
|
|
14
14
|
except Exception: # pragma: no cover
|
|
15
15
|
nx = None # type: ignore
|
|
16
16
|
|
|
17
|
-
from .constants import DEFAULTS, ALIAS_VF, ALIAS_THETA, ALIAS_DNFR, ALIAS_EPI, ALIAS_SI
|
|
17
|
+
from .constants import DEFAULTS, ALIAS_VF, ALIAS_THETA, ALIAS_DNFR, ALIAS_EPI, ALIAS_SI, ALIAS_EPI_KIND
|
|
18
18
|
|
|
19
19
|
# -------------------------
|
|
20
20
|
# Utilidades numéricas
|
|
@@ -76,6 +76,24 @@ def _set_attr(d, aliases, value: float) -> None:
|
|
|
76
76
|
return
|
|
77
77
|
d[next(iter(aliases))] = float(value)
|
|
78
78
|
|
|
79
|
+
|
|
80
|
+
def _get_attr_str(d: Dict[str, Any], aliases: Iterable[str], default: str = "") -> str:
|
|
81
|
+
for k in aliases:
|
|
82
|
+
if k in d:
|
|
83
|
+
try:
|
|
84
|
+
return str(d[k])
|
|
85
|
+
except Exception:
|
|
86
|
+
continue
|
|
87
|
+
return str(default)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _set_attr_str(d, aliases, value: str) -> None:
|
|
91
|
+
for k in aliases:
|
|
92
|
+
if k in d:
|
|
93
|
+
d[k] = str(value)
|
|
94
|
+
return
|
|
95
|
+
d[next(iter(aliases))] = str(value)
|
|
96
|
+
|
|
79
97
|
# -------------------------
|
|
80
98
|
# Estadísticos vecinales
|
|
81
99
|
# -------------------------
|
|
@@ -111,18 +129,36 @@ def push_glifo(nd: Dict[str, Any], glifo: str, window: int) -> None:
|
|
|
111
129
|
|
|
112
130
|
|
|
113
131
|
def reciente_glifo(nd: Dict[str, Any], glifo: str, ventana: int) -> bool:
|
|
114
|
-
"""Indica si ``glifo`` apareció en las últimas ``ventana`` emisiones
|
|
132
|
+
"""Indica si ``glifo`` apareció en las últimas ``ventana`` emisiones"""
|
|
115
133
|
hist = nd.get("hist_glifos")
|
|
116
|
-
if not hist:
|
|
117
|
-
return False
|
|
118
134
|
gl = str(glifo)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
135
|
+
from itertools import islice
|
|
136
|
+
if hist and any(g == gl for g in islice(reversed(hist), ventana)):
|
|
137
|
+
return True
|
|
138
|
+
# fallback al glifo dominante actual
|
|
139
|
+
return _get_attr_str(nd, ALIAS_EPI_KIND, "") == gl
|
|
140
|
+
|
|
141
|
+
# -------------------------
|
|
142
|
+
# Utilidades de historial global
|
|
143
|
+
# -------------------------
|
|
144
|
+
|
|
145
|
+
def ensure_history(G) -> Dict[str, Any]:
|
|
146
|
+
"""Garantiza G.graph['history'] y la devuelve."""
|
|
147
|
+
return G.graph.setdefault("history", {})
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def last_glifo(nd: Dict[str, Any]) -> str | None:
|
|
151
|
+
"""Retorna el glifo más reciente del nodo o ``None``."""
|
|
152
|
+
kind = _get_attr_str(nd, ALIAS_EPI_KIND, "")
|
|
153
|
+
if kind:
|
|
154
|
+
return kind
|
|
155
|
+
hist = nd.get("hist_glifos")
|
|
156
|
+
if not hist:
|
|
157
|
+
return None
|
|
158
|
+
try:
|
|
159
|
+
return hist[-1]
|
|
160
|
+
except Exception:
|
|
161
|
+
return None
|
|
126
162
|
|
|
127
163
|
# -------------------------
|
|
128
164
|
# Callbacks Γ(R)
|
|
@@ -202,12 +238,9 @@ def compute_Si(G, *, inplace: bool = True) -> Dict[Any, float]:
|
|
|
202
238
|
G.graph["_Si_weights"] = {"alpha": alpha, "beta": beta, "gamma": gamma}
|
|
203
239
|
G.graph["_Si_sensitivity"] = {"dSi_dvf_norm": alpha, "dSi_ddisp_fase": -beta, "dSi_ddnfr_norm": -gamma}
|
|
204
240
|
|
|
205
|
-
# Normalización de νf en red
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
# Normalización de ΔNFR
|
|
209
|
-
dnfrs = [abs(_get_attr(G.nodes[n], ALIAS_DNFR, 0.0)) for n in G.nodes()]
|
|
210
|
-
dnfrmax = max(dnfrs) if dnfrs else 1.0
|
|
241
|
+
# Normalización de νf y ΔNFR en red
|
|
242
|
+
vfmax = max((abs(_get_attr(G.nodes[n], ALIAS_VF, 0.0)) for n in G.nodes()), default=1.0)
|
|
243
|
+
dnfrmax = max((abs(_get_attr(G.nodes[n], ALIAS_DNFR, 0.0)) for n in G.nodes()), default=1.0)
|
|
211
244
|
|
|
212
245
|
out: Dict[Any, float] = {}
|
|
213
246
|
for n in G.nodes():
|