tnfr 4.0.0__py3-none-any.whl → 4.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tnfr might be problematic. Click here for more details.
- tnfr/__init__.py +54 -50
- tnfr/cli.py +94 -1
- tnfr/constants.py +30 -23
- tnfr/dynamics.py +99 -33
- tnfr/gamma.py +28 -9
- tnfr/grammar.py +6 -0
- tnfr/helpers.py +27 -1
- tnfr/metrics.py +50 -27
- tnfr/operators.py +59 -20
- tnfr/presets.py +5 -1
- tnfr/program.py +8 -0
- tnfr/scenarios.py +9 -3
- tnfr/sense.py +6 -21
- tnfr/trace.py +15 -26
- {tnfr-4.0.0.dist-info → tnfr-4.3.0.dist-info}/METADATA +12 -4
- tnfr-4.3.0.dist-info/RECORD +24 -0
- tnfr-4.0.0.dist-info/RECORD +0 -24
- {tnfr-4.0.0.dist-info → tnfr-4.3.0.dist-info}/WHEEL +0 -0
- {tnfr-4.0.0.dist-info → tnfr-4.3.0.dist-info}/entry_points.txt +0 -0
- {tnfr-4.0.0.dist-info → tnfr-4.3.0.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.0.0.dist-info → tnfr-4.3.0.dist-info}/top_level.txt +0 -0
tnfr/gamma.py
CHANGED
|
@@ -3,10 +3,19 @@
|
|
|
3
3
|
Γi(R): acoplamientos de red para la ecuación nodal extendida
|
|
4
4
|
∂EPI/∂t = νf · ΔNFR(t) + Γi(R)
|
|
5
5
|
|
|
6
|
+
`Γ` suma un término de acoplamiento dependiente del orden global de fase
|
|
7
|
+
`R`. La especificación se toma de ``G.graph['GAMMA']`` (ver
|
|
8
|
+
``DEFAULTS['GAMMA']``) con parámetros como:
|
|
9
|
+
|
|
10
|
+
* ``type`` – modo de acoplamiento (``none``, ``kuramoto_linear``,
|
|
11
|
+
``kuramoto_bandpass``)
|
|
12
|
+
* ``beta`` – ganancia del acoplamiento
|
|
13
|
+
* ``R0`` – umbral de activación (solo lineal)
|
|
14
|
+
|
|
6
15
|
Provee:
|
|
7
16
|
- kuramoto_R_psi(G): (R, ψ) orden de Kuramoto en la red
|
|
8
17
|
- GAMMA_REGISTRY: registro de acoplamientos canónicos
|
|
9
|
-
- eval_gamma(G, node, t): evalúa Γ para cada nodo según
|
|
18
|
+
- eval_gamma(G, node, t): evalúa Γ para cada nodo según la config
|
|
10
19
|
"""
|
|
11
20
|
from __future__ import annotations
|
|
12
21
|
from typing import Dict, Any, Tuple
|
|
@@ -14,14 +23,7 @@ import math
|
|
|
14
23
|
import cmath
|
|
15
24
|
|
|
16
25
|
from .constants import ALIAS_THETA
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _get_attr(nd: Dict[str, Any], aliases, default: float = 0.0) -> float:
|
|
20
|
-
"""Obtiene el primer atributo presente en nd según aliases."""
|
|
21
|
-
for k in aliases:
|
|
22
|
-
if k in nd:
|
|
23
|
-
return nd[k]
|
|
24
|
-
return default
|
|
26
|
+
from .helpers import _get_attr
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def kuramoto_R_psi(G) -> Tuple[float, float]:
|
|
@@ -74,10 +76,27 @@ def gamma_kuramoto_bandpass(G, node, t, cfg: Dict[str, Any]) -> float:
|
|
|
74
76
|
return beta * R * (1.0 - R) * sgn
|
|
75
77
|
|
|
76
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
|
+
|
|
77
95
|
GAMMA_REGISTRY = {
|
|
78
96
|
"none": gamma_none,
|
|
79
97
|
"kuramoto_linear": gamma_kuramoto_linear,
|
|
80
98
|
"kuramoto_bandpass": gamma_kuramoto_bandpass,
|
|
99
|
+
"kuramoto_tanh": gamma_kuramoto_tanh,
|
|
81
100
|
}
|
|
82
101
|
|
|
83
102
|
|
tnfr/grammar.py
CHANGED
|
@@ -142,6 +142,12 @@ def on_applied_glifo(G, n, applied: str) -> None:
|
|
|
142
142
|
# -------------------------
|
|
143
143
|
|
|
144
144
|
def select_and_apply_with_grammar(G, n, selector, window: int) -> None:
|
|
145
|
+
"""Aplica gramática canónica sobre la propuesta del selector.
|
|
146
|
+
|
|
147
|
+
El selector puede incluir una gramática **suave** (pre–filtro) como
|
|
148
|
+
`parametric_glyph_selector`; la presente función garantiza que la
|
|
149
|
+
gramática canónica tenga precedencia final.
|
|
150
|
+
"""
|
|
145
151
|
from .operators import aplicar_glifo
|
|
146
152
|
cand = selector(G, n)
|
|
147
153
|
cand = enforce_canonical_grammar(G, n, cand)
|
tnfr/helpers.py
CHANGED
|
@@ -124,6 +124,27 @@ def reciente_glifo(nd: Dict[str, Any], glifo: str, ventana: int) -> bool:
|
|
|
124
124
|
break
|
|
125
125
|
return False
|
|
126
126
|
|
|
127
|
+
# -------------------------
|
|
128
|
+
# Utilidades de historial global
|
|
129
|
+
# -------------------------
|
|
130
|
+
|
|
131
|
+
def ensure_history(G) -> Dict[str, Any]:
|
|
132
|
+
"""Garantiza G.graph['history'] y la devuelve."""
|
|
133
|
+
if "history" not in G.graph:
|
|
134
|
+
G.graph["history"] = {}
|
|
135
|
+
return G.graph["history"]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def last_glifo(nd: Dict[str, Any]) -> str | None:
|
|
139
|
+
"""Retorna el glifo más reciente del nodo o ``None``."""
|
|
140
|
+
hist = nd.get("hist_glifos")
|
|
141
|
+
if not hist:
|
|
142
|
+
return None
|
|
143
|
+
try:
|
|
144
|
+
return list(hist)[-1]
|
|
145
|
+
except Exception:
|
|
146
|
+
return None
|
|
147
|
+
|
|
127
148
|
# -------------------------
|
|
128
149
|
# Callbacks Γ(R)
|
|
129
150
|
# -------------------------
|
|
@@ -186,7 +207,10 @@ def invoke_callbacks(G, event: str, ctx: dict | None = None):
|
|
|
186
207
|
def compute_Si(G, *, inplace: bool = True) -> Dict[Any, float]:
|
|
187
208
|
"""Calcula Si por nodo y lo escribe en G.nodes[n]["Si"].
|
|
188
209
|
|
|
189
|
-
|
|
210
|
+
Fórmula:
|
|
211
|
+
Si = α·νf_norm + β·(1 - disp_fase_local) + γ·(1 - |ΔNFR|/max|ΔNFR|)
|
|
212
|
+
También guarda en ``G.graph`` los pesos normalizados y la
|
|
213
|
+
sensibilidad parcial (∂Si/∂componente).
|
|
190
214
|
"""
|
|
191
215
|
alpha = float(G.graph.get("SI_WEIGHTS", DEFAULTS["SI_WEIGHTS"]).get("alpha", 0.34))
|
|
192
216
|
beta = float(G.graph.get("SI_WEIGHTS", DEFAULTS["SI_WEIGHTS"]).get("beta", 0.33))
|
|
@@ -196,6 +220,8 @@ def compute_Si(G, *, inplace: bool = True) -> Dict[Any, float]:
|
|
|
196
220
|
alpha = beta = gamma = 1/3
|
|
197
221
|
else:
|
|
198
222
|
alpha, beta, gamma = alpha/s, beta/s, gamma/s
|
|
223
|
+
G.graph["_Si_weights"] = {"alpha": alpha, "beta": beta, "gamma": gamma}
|
|
224
|
+
G.graph["_Si_sensitivity"] = {"dSi_dvf_norm": alpha, "dSi_ddisp_fase": -beta, "dSi_ddnfr_norm": -gamma}
|
|
199
225
|
|
|
200
226
|
# Normalización de νf en red
|
|
201
227
|
vfs = [abs(_get_attr(G.nodes[n], ALIAS_VF, 0.0)) for n in G.nodes()]
|
tnfr/metrics.py
CHANGED
|
@@ -2,9 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
from typing import Dict, Any, List, Tuple
|
|
3
3
|
from collections import defaultdict, Counter
|
|
4
4
|
import statistics
|
|
5
|
+
import csv
|
|
6
|
+
import json
|
|
5
7
|
|
|
6
8
|
from .constants import DEFAULTS
|
|
7
|
-
from .helpers import
|
|
9
|
+
from .helpers import register_callback, ensure_history, last_glifo
|
|
8
10
|
from .sense import GLYPHS_CANONICAL
|
|
9
11
|
|
|
10
12
|
# -------------
|
|
@@ -16,25 +18,12 @@ DEFAULTS.setdefault("METRICS", {
|
|
|
16
18
|
"normalize_series": False # glifograma normalizado a fracción por paso
|
|
17
19
|
})
|
|
18
20
|
|
|
21
|
+
|
|
22
|
+
|
|
19
23
|
# -------------
|
|
20
24
|
# Utilidades internas
|
|
21
25
|
# -------------
|
|
22
26
|
|
|
23
|
-
def _ensure_history(G):
|
|
24
|
-
if "history" not in G.graph:
|
|
25
|
-
G.graph["history"] = {}
|
|
26
|
-
return G.graph["history"]
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _last_glifo(nd: Dict[str, Any]) -> str | None:
|
|
30
|
-
hist = nd.get("hist_glifos")
|
|
31
|
-
if not hist:
|
|
32
|
-
return None
|
|
33
|
-
try:
|
|
34
|
-
return list(hist)[-1]
|
|
35
|
-
except Exception:
|
|
36
|
-
return None
|
|
37
|
-
|
|
38
27
|
|
|
39
28
|
# -------------
|
|
40
29
|
# Estado nodal para Tg
|
|
@@ -44,10 +33,7 @@ def _tg_state(nd: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
44
33
|
"""Estructura interna por nodo para acumular tiempos de corrida por glifo.
|
|
45
34
|
Campos: curr (glifo actual), run (tiempo acumulado en el glifo actual)
|
|
46
35
|
"""
|
|
47
|
-
|
|
48
|
-
st.setdefault("curr", None)
|
|
49
|
-
st.setdefault("run", 0.0)
|
|
50
|
-
return st
|
|
36
|
+
return nd.setdefault("_Tg", {"curr": None, "run": 0.0})
|
|
51
37
|
|
|
52
38
|
|
|
53
39
|
# -------------
|
|
@@ -66,7 +52,7 @@ def _metrics_step(G, *args, **kwargs):
|
|
|
66
52
|
if not G.graph.get("METRICS", DEFAULTS.get("METRICS", {})).get("enabled", True):
|
|
67
53
|
return
|
|
68
54
|
|
|
69
|
-
hist =
|
|
55
|
+
hist = ensure_history(G)
|
|
70
56
|
dt = float(G.graph.get("DT", 1.0))
|
|
71
57
|
t = float(G.graph.get("_t", 0.0))
|
|
72
58
|
|
|
@@ -84,7 +70,7 @@ def _metrics_step(G, *args, **kwargs):
|
|
|
84
70
|
|
|
85
71
|
for n in G.nodes():
|
|
86
72
|
nd = G.nodes[n]
|
|
87
|
-
g =
|
|
73
|
+
g = last_glifo(nd)
|
|
88
74
|
if not g:
|
|
89
75
|
continue
|
|
90
76
|
|
|
@@ -143,7 +129,7 @@ def register_metrics_callbacks(G) -> None:
|
|
|
143
129
|
|
|
144
130
|
def Tg_global(G, normalize: bool = True) -> Dict[str, float]:
|
|
145
131
|
"""Tiempo glífico total por clase. Si normalize=True, devuelve fracciones del total."""
|
|
146
|
-
hist =
|
|
132
|
+
hist = ensure_history(G)
|
|
147
133
|
tg_total: Dict[str, float] = hist.get("Tg_total", {})
|
|
148
134
|
total = sum(tg_total.values()) or 1.0
|
|
149
135
|
if normalize:
|
|
@@ -153,7 +139,7 @@ def Tg_global(G, normalize: bool = True) -> Dict[str, float]:
|
|
|
153
139
|
|
|
154
140
|
def Tg_by_node(G, n, normalize: bool = False) -> Dict[str, float | List[float]]:
|
|
155
141
|
"""Resumen por nodo: si normalize, devuelve medias por glifo; si no, lista de corridas."""
|
|
156
|
-
hist =
|
|
142
|
+
hist = ensure_history(G)
|
|
157
143
|
rec = hist.get("Tg_by_node", {}).get(n, {})
|
|
158
144
|
if not normalize:
|
|
159
145
|
# convertir default dict → list para serializar
|
|
@@ -167,7 +153,7 @@ def Tg_by_node(G, n, normalize: bool = False) -> Dict[str, float | List[float]]:
|
|
|
167
153
|
|
|
168
154
|
|
|
169
155
|
def latency_series(G) -> Dict[str, List[float]]:
|
|
170
|
-
hist =
|
|
156
|
+
hist = ensure_history(G)
|
|
171
157
|
xs = hist.get("latency_index", [])
|
|
172
158
|
return {
|
|
173
159
|
"t": [float(x.get("t", i)) for i, x in enumerate(xs)],
|
|
@@ -176,7 +162,7 @@ def latency_series(G) -> Dict[str, List[float]]:
|
|
|
176
162
|
|
|
177
163
|
|
|
178
164
|
def glifogram_series(G) -> Dict[str, List[float]]:
|
|
179
|
-
hist =
|
|
165
|
+
hist = ensure_history(G)
|
|
180
166
|
xs = hist.get("glifogram", [])
|
|
181
167
|
if not xs:
|
|
182
168
|
return {"t": []}
|
|
@@ -194,7 +180,7 @@ def glyph_top(G, k: int = 3) -> List[Tuple[str, float]]:
|
|
|
194
180
|
|
|
195
181
|
def glyph_dwell_stats(G, n) -> Dict[str, Dict[str, float]]:
|
|
196
182
|
"""Estadísticos por nodo: mean/median/max de corridas por glifo."""
|
|
197
|
-
hist =
|
|
183
|
+
hist = ensure_history(G)
|
|
198
184
|
rec = hist.get("Tg_by_node", {}).get(n, {})
|
|
199
185
|
out = {}
|
|
200
186
|
for g in GLYPHS_CANONICAL:
|
|
@@ -209,3 +195,40 @@ def glyph_dwell_stats(G, n) -> Dict[str, Dict[str, float]]:
|
|
|
209
195
|
"count": int(len(runs)),
|
|
210
196
|
}
|
|
211
197
|
return out
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# -----------------------------
|
|
201
|
+
# Export history to CSV/JSON
|
|
202
|
+
# -----------------------------
|
|
203
|
+
|
|
204
|
+
def export_history(G, base_path: str, fmt: str = "csv") -> None:
|
|
205
|
+
"""Vuelca glifograma y traza σ(t) a archivos CSV o JSON compactos."""
|
|
206
|
+
hist = ensure_history(G)
|
|
207
|
+
glifo = glifogram_series(G)
|
|
208
|
+
sigma_mag = hist.get("sense_sigma_mag", [])
|
|
209
|
+
sigma = {
|
|
210
|
+
"t": list(range(len(sigma_mag))),
|
|
211
|
+
"sigma_x": hist.get("sense_sigma_x", []),
|
|
212
|
+
"sigma_y": hist.get("sense_sigma_y", []),
|
|
213
|
+
"mag": sigma_mag,
|
|
214
|
+
"angle": hist.get("sense_sigma_angle", []),
|
|
215
|
+
}
|
|
216
|
+
fmt = fmt.lower()
|
|
217
|
+
if fmt == "csv":
|
|
218
|
+
with open(base_path + "_glifogram.csv", "w", newline="") as f:
|
|
219
|
+
writer = csv.writer(f)
|
|
220
|
+
writer.writerow(["t", *GLYPHS_CANONICAL])
|
|
221
|
+
ts = glifo.get("t", [])
|
|
222
|
+
default_col = [0] * len(ts)
|
|
223
|
+
for i, t in enumerate(ts):
|
|
224
|
+
row = [t] + [glifo.get(g, default_col)[i] for g in GLYPHS_CANONICAL]
|
|
225
|
+
writer.writerow(row)
|
|
226
|
+
with open(base_path + "_sigma.csv", "w", newline="") as f:
|
|
227
|
+
writer = csv.writer(f)
|
|
228
|
+
writer.writerow(["t", "x", "y", "mag", "angle"])
|
|
229
|
+
for i, t in enumerate(sigma["t"]):
|
|
230
|
+
writer.writerow([t, sigma["sigma_x"][i], sigma["sigma_y"][i], sigma["mag"][i], sigma["angle"][i]])
|
|
231
|
+
else:
|
|
232
|
+
data = {"glifogram": glifo, "sigma": sigma}
|
|
233
|
+
with open(base_path + ".json", "w") as f:
|
|
234
|
+
json.dump(data, f)
|
tnfr/operators.py
CHANGED
|
@@ -21,6 +21,15 @@ Nota sobre α (alpha) de RE’MESH: se toma por prioridad de
|
|
|
21
21
|
3) DEFAULTS["REMESH_ALPHA"]
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
|
|
25
|
+
def _node_offset(G, n) -> int:
|
|
26
|
+
"""Deterministic node index used for jitter seeds."""
|
|
27
|
+
mapping = G.graph.get("_node_offset_map")
|
|
28
|
+
if mapping is None or len(mapping) != G.number_of_nodes():
|
|
29
|
+
mapping = {node: idx for idx, node in enumerate(sorted(G.nodes(), key=lambda x: str(x)))}
|
|
30
|
+
G.graph["_node_offset_map"] = mapping
|
|
31
|
+
return int(mapping.get(n, 0))
|
|
32
|
+
|
|
24
33
|
# -------------------------
|
|
25
34
|
# Glifos (operadores locales)
|
|
26
35
|
# -------------------------
|
|
@@ -55,7 +64,7 @@ def op_OZ(G, n): # O’Z — Disonancia (aumenta ΔNFR o añade ruido)
|
|
|
55
64
|
if bool(G.graph.get("OZ_NOISE_MODE", False)):
|
|
56
65
|
base_seed = int(G.graph.get("RANDOM_SEED", 0))
|
|
57
66
|
step_idx = len(G.graph.get("history", {}).get("C_steps", []))
|
|
58
|
-
rnd = random.Random(base_seed + step_idx*1000003 +
|
|
67
|
+
rnd = random.Random(base_seed + step_idx*1000003 + _node_offset(G, n) % 1009)
|
|
59
68
|
sigma = float(G.graph.get("OZ_SIGMA", 0.1))
|
|
60
69
|
noise = sigma * (2.0 * rnd.random() - 1.0)
|
|
61
70
|
_set_attr(nd, ALIAS_DNFR, dnfr + noise)
|
|
@@ -124,7 +133,7 @@ def op_NAV(G, n): # NA’V — Transición (jitter suave de ΔNFR)
|
|
|
124
133
|
base_seed = int(G.graph.get("RANDOM_SEED", 0))
|
|
125
134
|
# opcional: pequeño offset para evitar misma secuencia en todos los nodos/pasos
|
|
126
135
|
step_idx = len(G.graph.get("history", {}).get("C_steps", []))
|
|
127
|
-
rnd = random.Random(base_seed + step_idx*1000003 +
|
|
136
|
+
rnd = random.Random(base_seed + step_idx*1000003 + _node_offset(G, n) % 1009)
|
|
128
137
|
jitter = j * (2.0 * rnd.random() - 1.0)
|
|
129
138
|
else:
|
|
130
139
|
# comportamiento determinista (compatibilidad previa)
|
|
@@ -181,29 +190,31 @@ def aplicar_glifo(G, n, glifo: str, *, window: Optional[int] = None) -> None:
|
|
|
181
190
|
# -------------------------
|
|
182
191
|
|
|
183
192
|
def _remesh_alpha_info(G):
|
|
184
|
-
"""Devuelve (alpha, source) con precedencia explícita
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
"""Devuelve `(alpha, source)` con precedencia explícita."""
|
|
194
|
+
if bool(G.graph.get("REMESH_ALPHA_HARD", DEFAULTS.get("REMESH_ALPHA_HARD", False))):
|
|
195
|
+
val = float(G.graph.get("REMESH_ALPHA", DEFAULTS["REMESH_ALPHA"]))
|
|
196
|
+
return val, "REMESH_ALPHA"
|
|
197
|
+
gf = G.graph.get("GLYPH_FACTORS", DEFAULTS.get("GLYPH_FACTORS", {}))
|
|
187
198
|
if "REMESH_alpha" in gf:
|
|
188
|
-
return float(gf["REMESH_alpha"]), "GLYPH_FACTORS"
|
|
199
|
+
return float(gf["REMESH_alpha"]), "GLYPH_FACTORS.REMESH_alpha"
|
|
189
200
|
if "REMESH_ALPHA" in G.graph:
|
|
190
|
-
return float(G.graph["REMESH_ALPHA"]), "
|
|
191
|
-
return float(DEFAULTS["REMESH_ALPHA"]), "DEFAULTS"
|
|
201
|
+
return float(G.graph["REMESH_ALPHA"]), "REMESH_ALPHA"
|
|
202
|
+
return float(DEFAULTS["REMESH_ALPHA"]), "DEFAULTS.REMESH_ALPHA"
|
|
192
203
|
|
|
193
204
|
|
|
194
205
|
def aplicar_remesh_red(G) -> None:
|
|
195
|
-
"""
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"""
|
|
200
|
-
tau = int(G.graph.get("REMESH_TAU", DEFAULTS["REMESH_TAU"]))
|
|
206
|
+
"""RE’MESH a escala de red usando _epi_hist con memoria multi-escala."""
|
|
207
|
+
tau_g = int(G.graph.get("REMESH_TAU_GLOBAL", G.graph.get("REMESH_TAU", DEFAULTS["REMESH_TAU_GLOBAL"])))
|
|
208
|
+
tau_l = int(G.graph.get("REMESH_TAU_LOCAL", G.graph.get("REMESH_TAU", DEFAULTS["REMESH_TAU_LOCAL"])))
|
|
209
|
+
tau_req = max(tau_g, tau_l)
|
|
201
210
|
alpha, alpha_src = _remesh_alpha_info(G)
|
|
211
|
+
G.graph["_REMESH_ALPHA_SRC"] = alpha_src
|
|
202
212
|
hist = G.graph.get("_epi_hist", deque())
|
|
203
|
-
if len(hist) <
|
|
213
|
+
if len(hist) < tau_req + 1:
|
|
204
214
|
return
|
|
205
215
|
|
|
206
|
-
|
|
216
|
+
past_g = hist[-(tau_g + 1)]
|
|
217
|
+
past_l = hist[-(tau_l + 1)]
|
|
207
218
|
|
|
208
219
|
# --- Topología + snapshot EPI (ANTES) ---
|
|
209
220
|
try:
|
|
@@ -228,8 +239,11 @@ def aplicar_remesh_red(G) -> None:
|
|
|
228
239
|
for n in G.nodes():
|
|
229
240
|
nd = G.nodes[n]
|
|
230
241
|
epi_now = _get_attr(nd, ALIAS_EPI, 0.0)
|
|
231
|
-
|
|
232
|
-
|
|
242
|
+
epi_old_l = float(past_l.get(n, epi_now))
|
|
243
|
+
epi_old_g = float(past_g.get(n, epi_now))
|
|
244
|
+
mixed = (1 - alpha) * epi_now + alpha * epi_old_l
|
|
245
|
+
mixed = (1 - alpha) * mixed + alpha * epi_old_g
|
|
246
|
+
_set_attr(nd, ALIAS_EPI, mixed)
|
|
233
247
|
|
|
234
248
|
# --- Snapshot EPI (DESPUÉS) ---
|
|
235
249
|
epi_mean_after = list_mean(_get_attr(G.nodes[n], ALIAS_EPI, 0.0) for n in G.nodes())
|
|
@@ -242,7 +256,8 @@ def aplicar_remesh_red(G) -> None:
|
|
|
242
256
|
meta = {
|
|
243
257
|
"alpha": alpha,
|
|
244
258
|
"alpha_source": alpha_src,
|
|
245
|
-
"
|
|
259
|
+
"tau_global": tau_g,
|
|
260
|
+
"tau_local": tau_l,
|
|
246
261
|
"step": step_idx,
|
|
247
262
|
# firmas
|
|
248
263
|
"topo_hash": topo_hash,
|
|
@@ -278,6 +293,9 @@ def aplicar_remesh_si_estabilizacion_global(G, pasos_estables_consecutivos: Opti
|
|
|
278
293
|
req_extra = bool(G.graph.get("REMESH_REQUIRE_STABILITY", DEFAULTS["REMESH_REQUIRE_STABILITY"]))
|
|
279
294
|
min_sync = float(G.graph.get("REMESH_MIN_PHASE_SYNC", DEFAULTS["REMESH_MIN_PHASE_SYNC"]))
|
|
280
295
|
max_disr = float(G.graph.get("REMESH_MAX_GLYPH_DISR", DEFAULTS["REMESH_MAX_GLYPH_DISR"]))
|
|
296
|
+
min_sigma = float(G.graph.get("REMESH_MIN_SIGMA_MAG", DEFAULTS["REMESH_MIN_SIGMA_MAG"]))
|
|
297
|
+
min_R = float(G.graph.get("REMESH_MIN_KURAMOTO_R", DEFAULTS["REMESH_MIN_KURAMOTO_R"]))
|
|
298
|
+
min_sihi = float(G.graph.get("REMESH_MIN_SI_HI_FRAC", DEFAULTS["REMESH_MIN_SI_HI_FRAC"]))
|
|
281
299
|
|
|
282
300
|
hist = G.graph.setdefault("history", {"stable_frac": []})
|
|
283
301
|
sf = hist.get("stable_frac", [])
|
|
@@ -300,7 +318,22 @@ def aplicar_remesh_si_estabilizacion_global(G, pasos_estables_consecutivos: Opti
|
|
|
300
318
|
if "glyph_load_disr" in hist and len(hist["glyph_load_disr"]) >= w_estab:
|
|
301
319
|
win_disr = hist["glyph_load_disr"][-w_estab:]
|
|
302
320
|
disr_ok = (sum(win_disr)/len(win_disr)) <= max_disr
|
|
303
|
-
|
|
321
|
+
# magnitud de sigma (mayor mejor)
|
|
322
|
+
sig_ok = True
|
|
323
|
+
if "sense_sigma_mag" in hist and len(hist["sense_sigma_mag"]) >= w_estab:
|
|
324
|
+
win_sig = hist["sense_sigma_mag"][-w_estab:]
|
|
325
|
+
sig_ok = (sum(win_sig)/len(win_sig)) >= min_sigma
|
|
326
|
+
# orden de Kuramoto R (mayor mejor)
|
|
327
|
+
R_ok = True
|
|
328
|
+
if "kuramoto_R" in hist and len(hist["kuramoto_R"]) >= w_estab:
|
|
329
|
+
win_R = hist["kuramoto_R"][-w_estab:]
|
|
330
|
+
R_ok = (sum(win_R)/len(win_R)) >= min_R
|
|
331
|
+
# fracción de nodos con Si alto (mayor mejor)
|
|
332
|
+
sihi_ok = True
|
|
333
|
+
if "Si_hi_frac" in hist and len(hist["Si_hi_frac"]) >= w_estab:
|
|
334
|
+
win_sihi = hist["Si_hi_frac"][-w_estab:]
|
|
335
|
+
sihi_ok = (sum(win_sihi)/len(win_sihi)) >= min_sihi
|
|
336
|
+
if not (ps_ok and disr_ok and sig_ok and R_ok and sihi_ok):
|
|
304
337
|
return
|
|
305
338
|
# 3) Cooldown
|
|
306
339
|
last = G.graph.get("_last_remesh_step", -10**9)
|
|
@@ -308,6 +341,12 @@ def aplicar_remesh_si_estabilizacion_global(G, pasos_estables_consecutivos: Opti
|
|
|
308
341
|
cooldown = int(G.graph.get("REMESH_COOLDOWN_VENTANA", DEFAULTS["REMESH_COOLDOWN_VENTANA"]))
|
|
309
342
|
if step_idx - last < cooldown:
|
|
310
343
|
return
|
|
344
|
+
t_now = float(G.graph.get("_t", 0.0))
|
|
345
|
+
last_ts = float(G.graph.get("_last_remesh_ts", -1e12))
|
|
346
|
+
cooldown_ts = float(G.graph.get("REMESH_COOLDOWN_TS", DEFAULTS.get("REMESH_COOLDOWN_TS", 0.0)))
|
|
347
|
+
if cooldown_ts > 0 and (t_now - last_ts) < cooldown_ts:
|
|
348
|
+
return
|
|
311
349
|
# 4) Aplicar y registrar
|
|
312
350
|
aplicar_remesh_red(G)
|
|
313
351
|
G.graph["_last_remesh_step"] = step_idx
|
|
352
|
+
G.graph["_last_remesh_ts"] = t_now
|
tnfr/presets.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from .program import seq, block, wait
|
|
2
|
+
from .program import seq, block, wait, ejemplo_canonico_basico
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
_PRESETS = {
|
|
@@ -15,6 +15,10 @@ _PRESETS = {
|
|
|
15
15
|
"R’A",
|
|
16
16
|
"SH’A",
|
|
17
17
|
),
|
|
18
|
+
"ejemplo_canonico": ejemplo_canonico_basico(),
|
|
19
|
+
# Topologías fractales: expansión/contracción modular
|
|
20
|
+
"fractal_expand": seq(block("T’HOL", "VA’L", "U’M", repeat=2, close="NU’L"), "R’A"),
|
|
21
|
+
"fractal_contract": seq(block("T’HOL", "NU’L", "U’M", repeat=2, close="SH’A"), "R’A"),
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
|
tnfr/program.py
CHANGED
|
@@ -166,3 +166,11 @@ def target(nodes: Optional[Iterable[Node]] = None) -> TARGET:
|
|
|
166
166
|
|
|
167
167
|
def wait(steps: int = 1) -> WAIT:
|
|
168
168
|
return WAIT(steps=max(1, int(steps)))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def ejemplo_canonico_basico() -> List[Token]:
|
|
172
|
+
"""Secuencia canónica de referencia.
|
|
173
|
+
|
|
174
|
+
SH’A → A’L → R’A → Z’HIR → NU’L → T’HOL
|
|
175
|
+
"""
|
|
176
|
+
return seq("SH’A", "A’L", "R’A", "Z’HIR", "NU’L", "T’HOL")
|
tnfr/scenarios.py
CHANGED
|
@@ -17,12 +17,18 @@ def build_graph(n: int = 24, topology: str = "ring", seed: int | None = 1):
|
|
|
17
17
|
else:
|
|
18
18
|
G = nx.path_graph(n)
|
|
19
19
|
|
|
20
|
+
# Valores canónicos para inicialización
|
|
21
|
+
inject_defaults(G, DEFAULTS)
|
|
22
|
+
vf_min = float(G.graph.get("VF_MIN", DEFAULTS["VF_MIN"]))
|
|
23
|
+
vf_max = float(G.graph.get("VF_MAX", DEFAULTS["VF_MAX"]))
|
|
24
|
+
th_min = float(G.graph.get("INIT_THETA_MIN", DEFAULTS.get("INIT_THETA_MIN", -3.1416)))
|
|
25
|
+
th_max = float(G.graph.get("INIT_THETA_MAX", DEFAULTS.get("INIT_THETA_MAX", 3.1416)))
|
|
26
|
+
|
|
20
27
|
for i in G.nodes():
|
|
21
28
|
nd = G.nodes[i]
|
|
22
29
|
nd.setdefault("EPI", rng.uniform(0.1, 0.3))
|
|
23
|
-
nd.setdefault("νf", rng.uniform(
|
|
24
|
-
nd.setdefault("θ", rng.uniform(
|
|
30
|
+
nd.setdefault("νf", rng.uniform(vf_min, vf_max))
|
|
31
|
+
nd.setdefault("θ", rng.uniform(th_min, th_max))
|
|
25
32
|
nd.setdefault("Si", rng.uniform(0.4, 0.7))
|
|
26
33
|
|
|
27
|
-
inject_defaults(G, DEFAULTS)
|
|
28
34
|
return G
|
tnfr/sense.py
CHANGED
|
@@ -4,7 +4,7 @@ import math
|
|
|
4
4
|
from collections import Counter
|
|
5
5
|
|
|
6
6
|
from .constants import DEFAULTS, ALIAS_SI, ALIAS_EPI
|
|
7
|
-
from .helpers import _get_attr, clamp01, register_callback
|
|
7
|
+
from .helpers import _get_attr, clamp01, register_callback, ensure_history, last_glifo
|
|
8
8
|
|
|
9
9
|
# -------------------------
|
|
10
10
|
# Canon: orden circular de glifos y ángulos
|
|
@@ -60,23 +60,14 @@ def _weight(G, n, mode: str) -> float:
|
|
|
60
60
|
return 1.0
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
hist = nd.get("hist_glifos")
|
|
65
|
-
if not hist:
|
|
66
|
-
return None
|
|
67
|
-
try:
|
|
68
|
-
return list(hist)[-1]
|
|
69
|
-
except Exception:
|
|
70
|
-
return None
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
|
|
73
64
|
# -------------------------
|
|
74
65
|
# σ por nodo y σ global
|
|
75
66
|
# -------------------------
|
|
76
67
|
|
|
77
68
|
def sigma_vector_node(G, n, weight_mode: str | None = None) -> Dict[str, float] | None:
|
|
78
69
|
nd = G.nodes[n]
|
|
79
|
-
g =
|
|
70
|
+
g = last_glifo(nd)
|
|
80
71
|
if g is None:
|
|
81
72
|
return None
|
|
82
73
|
w = _weight(G, n, weight_mode or G.graph.get("SIGMA", DEFAULTS["SIGMA"]).get("weight", "Si"))
|
|
@@ -120,17 +111,11 @@ def sigma_vector_global(G, weight_mode: str | None = None) -> Dict[str, float]:
|
|
|
120
111
|
# Historia / series
|
|
121
112
|
# -------------------------
|
|
122
113
|
|
|
123
|
-
def _ensure_history(G):
|
|
124
|
-
if "history" not in G.graph:
|
|
125
|
-
G.graph["history"] = {}
|
|
126
|
-
return G.graph["history"]
|
|
127
|
-
|
|
128
|
-
|
|
129
114
|
def push_sigma_snapshot(G, t: float | None = None) -> None:
|
|
130
115
|
cfg = G.graph.get("SIGMA", DEFAULTS["SIGMA"])
|
|
131
116
|
if not cfg.get("enabled", True):
|
|
132
117
|
return
|
|
133
|
-
hist =
|
|
118
|
+
hist = ensure_history(G)
|
|
134
119
|
key = cfg.get("history_key", "sigma_global")
|
|
135
120
|
|
|
136
121
|
# Global
|
|
@@ -153,7 +138,7 @@ def push_sigma_snapshot(G, t: float | None = None) -> None:
|
|
|
153
138
|
# Conteo de glifos por paso (útil para rosa glífica)
|
|
154
139
|
counts = Counter()
|
|
155
140
|
for n in G.nodes():
|
|
156
|
-
g =
|
|
141
|
+
g = last_glifo(G.nodes[n])
|
|
157
142
|
if g:
|
|
158
143
|
counts[g] += 1
|
|
159
144
|
hist.setdefault("sigma_counts", []).append({"t": sv["t"], **counts})
|
|
@@ -163,7 +148,7 @@ def push_sigma_snapshot(G, t: float | None = None) -> None:
|
|
|
163
148
|
per = hist.setdefault("sigma_per_node", {})
|
|
164
149
|
for n in G.nodes():
|
|
165
150
|
nd = G.nodes[n]
|
|
166
|
-
g =
|
|
151
|
+
g = last_glifo(nd)
|
|
167
152
|
if not g:
|
|
168
153
|
continue
|
|
169
154
|
a = glyph_angle(g)
|
tnfr/trace.py
CHANGED
|
@@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional
|
|
|
3
3
|
from collections import Counter
|
|
4
4
|
|
|
5
5
|
from .constants import DEFAULTS
|
|
6
|
-
from .helpers import register_callback
|
|
6
|
+
from .helpers import register_callback, ensure_history, last_glifo
|
|
7
7
|
|
|
8
8
|
try:
|
|
9
9
|
from .gamma import kuramoto_R_psi
|
|
@@ -22,7 +22,7 @@ except Exception: # pragma: no cover
|
|
|
22
22
|
# -------------------------
|
|
23
23
|
DEFAULTS.setdefault("TRACE", {
|
|
24
24
|
"enabled": True,
|
|
25
|
-
"capture": ["gamma", "grammar", "selector", "
|
|
25
|
+
"capture": ["gamma", "grammar", "selector", "dnfr_weights", "si_weights", "callbacks", "thol_state", "sigma", "kuramoto", "glifo_counts"],
|
|
26
26
|
"history_key": "trace_meta",
|
|
27
27
|
})
|
|
28
28
|
|
|
@@ -30,22 +30,6 @@ DEFAULTS.setdefault("TRACE", {
|
|
|
30
30
|
# Helpers
|
|
31
31
|
# -------------------------
|
|
32
32
|
|
|
33
|
-
def _ensure_history(G):
|
|
34
|
-
if "history" not in G.graph:
|
|
35
|
-
G.graph["history"] = {}
|
|
36
|
-
return G.graph["history"]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _last_glifo(nd: Dict[str, Any]) -> str | None:
|
|
40
|
-
h = nd.get("hist_glifos")
|
|
41
|
-
if not h:
|
|
42
|
-
return None
|
|
43
|
-
try:
|
|
44
|
-
return list(h)[-1]
|
|
45
|
-
except Exception:
|
|
46
|
-
return None
|
|
47
|
-
|
|
48
|
-
|
|
49
33
|
# -------------------------
|
|
50
34
|
# Snapshots
|
|
51
35
|
# -------------------------
|
|
@@ -55,7 +39,7 @@ def _trace_before(G, *args, **kwargs):
|
|
|
55
39
|
return
|
|
56
40
|
cfg = G.graph.get("TRACE", DEFAULTS["TRACE"])
|
|
57
41
|
capture: List[str] = list(cfg.get("capture", []))
|
|
58
|
-
hist =
|
|
42
|
+
hist = ensure_history(G)
|
|
59
43
|
key = cfg.get("history_key", "trace_meta")
|
|
60
44
|
|
|
61
45
|
meta: Dict[str, Any] = {"t": float(G.graph.get("_t", 0.0)), "phase": "before"}
|
|
@@ -70,10 +54,14 @@ def _trace_before(G, *args, **kwargs):
|
|
|
70
54
|
sel = G.graph.get("glyph_selector")
|
|
71
55
|
meta["selector"] = getattr(sel, "__name__", str(sel)) if sel else None
|
|
72
56
|
|
|
73
|
-
if "
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
57
|
+
if "dnfr_weights" in capture:
|
|
58
|
+
mix = G.graph.get("DNFR_WEIGHTS")
|
|
59
|
+
if isinstance(mix, dict):
|
|
60
|
+
meta["dnfr_weights"] = dict(mix)
|
|
61
|
+
|
|
62
|
+
if "si_weights" in capture:
|
|
63
|
+
meta["si_weights"] = dict(G.graph.get("_Si_weights", {}))
|
|
64
|
+
meta["si_sensitivity"] = dict(G.graph.get("_Si_sensitivity", {}))
|
|
77
65
|
|
|
78
66
|
if "callbacks" in capture:
|
|
79
67
|
# si el motor guarda los callbacks, exponer nombres por fase
|
|
@@ -99,7 +87,7 @@ def _trace_after(G, *args, **kwargs):
|
|
|
99
87
|
return
|
|
100
88
|
cfg = G.graph.get("TRACE", DEFAULTS["TRACE"])
|
|
101
89
|
capture: List[str] = list(cfg.get("capture", []))
|
|
102
|
-
hist =
|
|
90
|
+
hist = ensure_history(G)
|
|
103
91
|
key = cfg.get("history_key", "trace_meta")
|
|
104
92
|
|
|
105
93
|
meta: Dict[str, Any] = {"t": float(G.graph.get("_t", 0.0)), "phase": "after"}
|
|
@@ -115,7 +103,7 @@ def _trace_after(G, *args, **kwargs):
|
|
|
115
103
|
if "glifo_counts" in capture:
|
|
116
104
|
cnt = Counter()
|
|
117
105
|
for n in G.nodes():
|
|
118
|
-
g =
|
|
106
|
+
g = last_glifo(G.nodes[n])
|
|
119
107
|
if g:
|
|
120
108
|
cnt[g] += 1
|
|
121
109
|
meta["glifos"] = dict(cnt)
|
|
@@ -134,7 +122,8 @@ def register_trace(G) -> None:
|
|
|
134
122
|
- gamma: especificación activa de Γi(R)
|
|
135
123
|
- grammar: configuración de gramática canónica
|
|
136
124
|
- selector: nombre del selector glífico
|
|
137
|
-
-
|
|
125
|
+
- dnfr_weights: mezcla ΔNFR declarada en el motor
|
|
126
|
+
- si_weights: pesos α/β/γ y sensibilidad de Si
|
|
138
127
|
- callbacks: callbacks registrados por fase (si están en G.graph['_callbacks'])
|
|
139
128
|
- thol_open_nodes: cuántos nodos tienen bloque T’HOL abierto
|
|
140
129
|
- kuramoto: (R, ψ) de la red
|