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.
- tnfr/__init__.py +91 -89
- tnfr/alias.py +546 -0
- tnfr/cache.py +578 -0
- tnfr/callback_utils.py +388 -0
- tnfr/cli/__init__.py +75 -0
- tnfr/cli/arguments.py +177 -0
- tnfr/cli/execution.py +288 -0
- tnfr/cli/utils.py +36 -0
- tnfr/collections_utils.py +300 -0
- tnfr/config.py +19 -28
- tnfr/constants/__init__.py +174 -0
- tnfr/constants/core.py +159 -0
- tnfr/constants/init.py +31 -0
- tnfr/constants/metric.py +110 -0
- tnfr/constants_glyphs.py +98 -0
- tnfr/dynamics/__init__.py +658 -0
- tnfr/dynamics/dnfr.py +733 -0
- tnfr/dynamics/integrators.py +267 -0
- tnfr/dynamics/sampling.py +31 -0
- tnfr/execution.py +201 -0
- tnfr/flatten.py +283 -0
- tnfr/gamma.py +302 -88
- tnfr/glyph_history.py +290 -0
- tnfr/grammar.py +285 -96
- tnfr/graph_utils.py +84 -0
- tnfr/helpers/__init__.py +71 -0
- tnfr/helpers/numeric.py +87 -0
- tnfr/immutable.py +178 -0
- tnfr/import_utils.py +228 -0
- tnfr/initialization.py +197 -0
- tnfr/io.py +246 -0
- tnfr/json_utils.py +162 -0
- tnfr/locking.py +37 -0
- tnfr/logging_utils.py +116 -0
- tnfr/metrics/__init__.py +41 -0
- tnfr/metrics/coherence.py +829 -0
- tnfr/metrics/common.py +151 -0
- tnfr/metrics/core.py +101 -0
- tnfr/metrics/diagnosis.py +234 -0
- tnfr/metrics/export.py +137 -0
- tnfr/metrics/glyph_timing.py +189 -0
- tnfr/metrics/reporting.py +148 -0
- tnfr/metrics/sense_index.py +120 -0
- tnfr/metrics/trig.py +181 -0
- tnfr/metrics/trig_cache.py +109 -0
- tnfr/node.py +214 -159
- tnfr/observers.py +126 -128
- tnfr/ontosim.py +134 -134
- tnfr/operators/__init__.py +420 -0
- tnfr/operators/jitter.py +203 -0
- tnfr/operators/remesh.py +485 -0
- tnfr/presets.py +46 -14
- tnfr/rng.py +254 -0
- tnfr/selector.py +210 -0
- tnfr/sense.py +284 -131
- tnfr/structural.py +207 -79
- tnfr/tokens.py +60 -0
- tnfr/trace.py +329 -94
- tnfr/types.py +43 -17
- tnfr/validators.py +70 -24
- tnfr/value_utils.py +59 -0
- tnfr-4.5.2.dist-info/METADATA +379 -0
- tnfr-4.5.2.dist-info/RECORD +67 -0
- tnfr/cli.py +0 -322
- tnfr/constants.py +0 -277
- tnfr/dynamics.py +0 -814
- tnfr/helpers.py +0 -264
- tnfr/main.py +0 -47
- tnfr/metrics.py +0 -597
- tnfr/operators.py +0 -525
- tnfr/program.py +0 -176
- tnfr/scenarios.py +0 -34
- tnfr-4.5.0.dist-info/METADATA +0 -109
- tnfr-4.5.0.dist-info/RECORD +0 -28
- {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/WHEEL +0 -0
- {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/licenses/LICENSE.md +0 -0
- {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
|