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/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 G.graph['GAMMA']
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
- Si = α·νf_norm + β·(1 - disp_fase_local) + γ·(1 - |ΔNFR|/max|ΔNFR|)
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 _get_attr, clamp01, register_callback
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
- st = nd.setdefault("_Tg", {"curr": None, "run": 0.0})
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 = _ensure_history(G)
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 = _last_glifo(nd)
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 = _ensure_history(G)
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 = _ensure_history(G)
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 = _ensure_history(G)
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 = _ensure_history(G)
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 = _ensure_history(G)
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 + hash(("OZ", n)) % 1009)
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 + hash(n) % 1009)
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
- 1) GLYPH_FACTORS["REMESH_alpha"] 2) G.graph["REMESH_ALPHA"] 3) DEFAULTS["REMESH_ALPHA"]"""
186
- gf = G.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"])
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"]), "G.graph"
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
- RE’MESH a escala de red usando _epi_hist capturado en dynamics.step.
197
- Loguea meta con alpha/tau/step + topo_hash y checksums/medias de EPI antes/después.
198
- Precedencia de alpha: GLYPH_FACTORS → G.graph → DEFAULTS.
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) < tau + 1:
213
+ if len(hist) < tau_req + 1:
204
214
  return
205
215
 
206
- past = hist[-(tau + 1)]
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
- epi_old = float(past.get(n, epi_now))
232
- _set_attr(nd, ALIAS_EPI, (1 - alpha) * epi_now + alpha * epi_old)
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
- "tau": tau,
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
- if not (ps_ok and disr_ok):
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(0.8, 1.2))
24
- nd.setdefault("θ", rng.uniform(-3.1416, 3.1416))
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
- def _last_glifo(nd: Dict[str, Any]) -> str | None:
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 = _last_glifo(nd)
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 = _ensure_history(G)
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 = _last_glifo(G.nodes[n])
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 = _last_glifo(nd)
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", "dnfr_mix", "callbacks", "thol_state", "sigma", "kuramoto", "glifo_counts"],
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 = _ensure_history(G)
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 "dnfr_mix" in capture:
74
- # tratar de capturar varias convenciones posibles
75
- mix = G.graph.get("DNFR_MIX") or G.graph.get("DELTA_NFR_MIX") or G.graph.get("NFR_MIX")
76
- meta["dnfr_mix"] = mix if isinstance(mix, dict) else {"value": mix}
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 = _ensure_history(G)
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 = _last_glifo(G.nodes[n])
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
- - dnfr_mix: mezcla (si el motor la expone en G.graph)
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