tnfr 1.0__py3-none-any.whl → 3.0.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/helpers.py ADDED
@@ -0,0 +1,198 @@
1
+ """
2
+ helpers.py — TNFR canónica
3
+
4
+ Utilidades transversales + cálculo de Índice de sentido (Si).
5
+ """
6
+ from __future__ import annotations
7
+ from typing import Iterable, Dict, Any, Tuple, List
8
+ import math
9
+ from collections import deque
10
+
11
+ try:
12
+ import networkx as nx # solo para tipos
13
+ except Exception: # pragma: no cover
14
+ nx = None # type: ignore
15
+
16
+ from constants import DEFAULTS, ALIAS_VF, ALIAS_THETA, ALIAS_DNFR, ALIAS_EPI, ALIAS_SI
17
+
18
+ # -------------------------
19
+ # Utilidades numéricas
20
+ # -------------------------
21
+
22
+ def clamp(x: float, a: float, b: float) -> float:
23
+ return a if x < a else b if x > b else x
24
+
25
+
26
+ def clamp_abs(x: float, m: float) -> float:
27
+ m = abs(m)
28
+ return clamp(x, -m, m)
29
+
30
+
31
+ def clamp01(x: float) -> float:
32
+ return clamp(x, 0.0, 1.0)
33
+
34
+
35
+ def list_mean(xs: Iterable[float], default: float = 0.0) -> float:
36
+ xs = list(xs)
37
+ return sum(xs) / len(xs) if xs else default
38
+
39
+
40
+ def _wrap_angle(a: float) -> float:
41
+ """Envuelve ángulo a (-π, π]."""
42
+ pi = math.pi
43
+ a = (a + pi) % (2 * pi) - pi
44
+ return a
45
+
46
+
47
+ def phase_distance(a: float, b: float) -> float:
48
+ """Distancia de fase normalizada en [0,1]. 0 = misma fase, 1 = opuesta."""
49
+ return abs(_wrap_angle(a - b)) / math.pi
50
+
51
+
52
+ # -------------------------
53
+ # Acceso a atributos con alias
54
+ # -------------------------
55
+
56
+ def _get_attr(d: Dict[str, Any], aliases: Iterable[str], default: float = 0.0) -> float:
57
+ for k in aliases:
58
+ if k in d:
59
+ try:
60
+ return float(d[k])
61
+ except Exception:
62
+ continue
63
+ return float(default)
64
+
65
+ def _set_attr(d, aliases, value: float) -> None:
66
+ for k in aliases:
67
+ if k in d:
68
+ d[k] = float(value)
69
+ return
70
+ d[next(iter(aliases))] = float(value)
71
+
72
+ # -------------------------
73
+ # Estadísticos vecinales
74
+ # -------------------------
75
+
76
+ def media_vecinal(G, n, aliases: Iterable[str], default: float = 0.0) -> float:
77
+ vals: List[float] = []
78
+ for v in G.neighbors(n):
79
+ vals.append(_get_attr(G.nodes[v], aliases, default))
80
+ return list_mean(vals, default)
81
+
82
+
83
+ def fase_media(G, n) -> float:
84
+ """Promedio circular de las fases de los vecinos."""
85
+ import math
86
+ x = 0.0
87
+ y = 0.0
88
+ count = 0
89
+ for v in G.neighbors(n):
90
+ th = _get_attr(G.nodes[v], ALIAS_THETA, 0.0)
91
+ x += math.cos(th)
92
+ y += math.sin(th)
93
+ count += 1
94
+ if count == 0:
95
+ return _get_attr(G.nodes[n], ALIAS_THETA, 0.0)
96
+ return math.atan2(y / max(1, count), x / max(1, count))
97
+
98
+
99
+ # -------------------------
100
+ # Historial de glifos por nodo
101
+ # -------------------------
102
+
103
+ def push_glifo(nd: Dict[str, Any], glifo: str, window: int) -> None:
104
+ hist = nd.setdefault("hist_glifos", deque(maxlen=window))
105
+ hist.append(str(glifo))
106
+
107
+
108
+ def reciente_glifo(nd: Dict[str, Any], glifo: str, ventana: int) -> bool:
109
+ hist = nd.get("hist_glifos")
110
+ if not hist:
111
+ return False
112
+ last = list(hist)[-ventana:]
113
+ return str(glifo) in last
114
+
115
+ # -------------------------
116
+ # Callbacks Γ(R)
117
+ # -------------------------
118
+
119
+ def _ensure_callbacks(G):
120
+ """Garantiza la estructura de callbacks en G.graph."""
121
+ cbs = G.graph.setdefault("callbacks", {
122
+ "before_step": [],
123
+ "after_step": [],
124
+ "on_remesh": [],
125
+ })
126
+ # normaliza claves por si vienen incompletas
127
+ for k in ("before_step", "after_step", "on_remesh"):
128
+ cbs.setdefault(k, [])
129
+ return cbs
130
+
131
+ def register_callback(G, event: str, func):
132
+ """Registra un callback en G.graph['callbacks'][event]. Firma: func(G, ctx) -> None"""
133
+ if event not in ("before_step", "after_step", "on_remesh"):
134
+ raise ValueError(f"Evento desconocido: {event}")
135
+ cbs = _ensure_callbacks(G)
136
+ cbs[event].append(func)
137
+ return func
138
+
139
+ def invoke_callbacks(G, event: str, ctx: dict | None = None):
140
+ """Invoca todos los callbacks registrados para `event` con el contexto `ctx`."""
141
+ cbs = _ensure_callbacks(G).get(event, [])
142
+ strict = bool(G.graph.get("CALLBACKS_STRICT", DEFAULTS["CALLBACKS_STRICT"]))
143
+ ctx = ctx or {}
144
+ for fn in list(cbs):
145
+ try:
146
+ fn(G, ctx)
147
+ except Exception as e:
148
+ if strict:
149
+ raise
150
+ G.graph.setdefault("_callback_errors", []).append({
151
+ "event": event, "step": ctx.get("step"), "error": repr(e), "fn": repr(fn)
152
+ })
153
+
154
+ # -------------------------
155
+ # Índice de sentido (Si)
156
+ # -------------------------
157
+
158
+ def compute_Si(G, *, inplace: bool = True) -> Dict[Any, float]:
159
+ """Calcula Si por nodo y lo escribe en G.nodes[n]["Si"].
160
+
161
+ Si = α·νf_norm + β·(1 - disp_fase_local) + γ·(1 - |ΔNFR|/max|ΔNFR|)
162
+ """
163
+ alpha = float(G.graph.get("SI_WEIGHTS", DEFAULTS["SI_WEIGHTS"]).get("alpha", 0.34))
164
+ beta = float(G.graph.get("SI_WEIGHTS", DEFAULTS["SI_WEIGHTS"]).get("beta", 0.33))
165
+ gamma = float(G.graph.get("SI_WEIGHTS", DEFAULTS["SI_WEIGHTS"]).get("gamma", 0.33))
166
+ s = alpha + beta + gamma
167
+ if s <= 0:
168
+ alpha = beta = gamma = 1/3
169
+ else:
170
+ alpha, beta, gamma = alpha/s, beta/s, gamma/s
171
+
172
+ # Normalización de νf en red
173
+ vfs = [abs(_get_attr(G.nodes[n], ALIAS_VF, 0.0)) for n in G.nodes()]
174
+ vfmax = max(vfs) if vfs else 1.0
175
+ # Normalización de ΔNFR
176
+ dnfrs = [abs(_get_attr(G.nodes[n], ALIAS_DNFR, 0.0)) for n in G.nodes()]
177
+ dnfrmax = max(dnfrs) if dnfrs else 1.0
178
+
179
+ out: Dict[Any, float] = {}
180
+ for n in G.nodes():
181
+ nd = G.nodes[n]
182
+ vf = _get_attr(nd, ALIAS_VF, 0.0)
183
+ vf_norm = 0.0 if vfmax == 0 else clamp01(abs(vf)/vfmax)
184
+
185
+ # dispersión de fase local
186
+ th_i = _get_attr(nd, ALIAS_THETA, 0.0)
187
+ th_bar = fase_media(G, n)
188
+ disp_fase = phase_distance(th_i, th_bar) # [0,1]
189
+
190
+ dnfr = _get_attr(nd, ALIAS_DNFR, 0.0)
191
+ dnfr_norm = 0.0 if dnfrmax == 0 else clamp01(abs(dnfr)/dnfrmax)
192
+
193
+ Si = alpha*vf_norm + beta*(1.0 - disp_fase) + gamma*(1.0 - dnfr_norm)
194
+ Si = clamp01(Si)
195
+ out[n] = Si
196
+ if inplace:
197
+ _set_attr(nd, ALIAS_SI, Si)
198
+ return out
tnfr/main.py ADDED
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+ import argparse, sys
3
+ import networkx as nx
4
+ from . import preparar_red, run, __version__
5
+
6
+ def main(argv: list[str] | None = None) -> None:
7
+ p = argparse.ArgumentParser(
8
+ prog="tnfr",
9
+ description="TNFR canónica — demo CLI (red Erdős–Rényi + dinámica glífica)",
10
+ )
11
+ p.add_argument("--version", action="store_true", help="muestra versión y sale")
12
+ p.add_argument("--n", type=int, default=30, help="nodos (Erdős–Rényi)")
13
+ p.add_argument("--p", type=float, default=0.15, help="probabilidad de arista (Erdős–Rényi)")
14
+ p.add_argument("--steps", type=int, default=100, help="pasos a simular")
15
+ p.add_argument("--observer", action="store_true", help="adjunta observador estándar")
16
+ args = p.parse_args(argv)
17
+
18
+ if args.version:
19
+ print(__version__)
20
+ return
21
+
22
+ G = nx.erdos_renyi_graph(args.n, args.p)
23
+ preparar_red(G, ATTACH_STD_OBSERVER=bool(args.observer))
24
+ run(G, args.steps)
25
+
26
+ h = G.graph.get("history", {})
27
+ C = h.get("C_steps", [])[-1] if h.get("C_steps") else None
28
+ stab = h.get("stable_frac", [])[-1] if h.get("stable_frac") else None
29
+ R = h.get("kuramoto_R", [])[-1] if h.get("kuramoto_R") else None
30
+
31
+ print("TNFR terminado:")
32
+ if C is not None: print(f" C(t) ~ {C:.3f}")
33
+ if stab is not None: print(f" estable ~ {stab:.3f}")
34
+ if R is not None: print(f" R (Kuramoto) ~ {R:.3f}")
35
+
36
+ if __name__ == "__main__":
37
+ main(sys.argv[1:])
tnfr/observers.py ADDED
@@ -0,0 +1,149 @@
1
+ """
2
+ observers.py — TNFR canónica
3
+
4
+ Observadores y métricas auxiliares.
5
+ """
6
+ from __future__ import annotations
7
+ from collections import Counter
8
+ from typing import Dict, Any
9
+ import math
10
+
11
+ from constants import ALIAS_DNFR, ALIAS_EPI, ALIAS_THETA, ALIAS_dEPI
12
+ from helpers import _get_attr, list_mean, register_callback
13
+
14
+ # -------------------------
15
+ # Observador estándar Γ(R)
16
+ # -------------------------
17
+ def _std_log(G, kind: str, ctx: dict):
18
+ """Guarda eventos compactos en history['events']."""
19
+ h = G.graph.setdefault("history", {})
20
+ h.setdefault("events", []).append((kind, dict(ctx)))
21
+
22
+ def std_before(G, ctx):
23
+ _std_log(G, "before", ctx)
24
+
25
+ def std_after(G, ctx):
26
+ _std_log(G, "after", ctx)
27
+
28
+ def std_on_remesh(G, ctx):
29
+ _std_log(G, "remesh", ctx)
30
+
31
+ def attach_standard_observer(G):
32
+ """Registra callbacks estándar: before_step, after_step, on_remesh."""
33
+ register_callback(G, "before_step", std_before)
34
+ register_callback(G, "after_step", std_after)
35
+ register_callback(G, "on_remesh", std_on_remesh)
36
+ G.graph.setdefault("_STD_OBSERVER", "attached")
37
+ return G
38
+
39
+ def coherencia_global(G) -> float:
40
+ """Proxy de C(t): alta cuando |ΔNFR| y |dEPI_dt| son pequeños."""
41
+ dnfr = list_mean(abs(_get_attr(G.nodes[n], ALIAS_DNFR, 0.0)) for n in G.nodes())
42
+ dEPI = list_mean(abs(_get_attr(G.nodes[n], ALIAS_dEPI, 0.0)) for n in G.nodes())
43
+ return 1.0 / (1.0 + dnfr + dEPI)
44
+
45
+
46
+ def sincronía_fase(G) -> float:
47
+ X = list(math.cos(_get_attr(G.nodes[n], ALIAS_THETA, 0.0)) for n in G.nodes())
48
+ Y = list(math.sin(_get_attr(G.nodes[n], ALIAS_THETA, 0.0)) for n in G.nodes())
49
+ if not X:
50
+ return 1.0
51
+ import math
52
+ th = math.atan2(sum(Y)/len(Y), sum(X)/len(X))
53
+ # varianza angular aproximada (0 = muy sincronizado)
54
+ import statistics as st
55
+ var = st.pvariance([((_get_attr(G.nodes[n], ALIAS_THETA, 0.0) - th + math.pi) % (2*math.pi) - math.pi) for n in G.nodes()]) if len(X) > 1 else 0.0
56
+ return 1.0 / (1.0 + var)
57
+
58
+ def orden_kuramoto(G) -> float:
59
+ """R en [0,1], 1 = fases perfectamente alineadas."""
60
+ X = [math.cos(_get_attr(G.nodes[n], ALIAS_THETA, 0.0)) for n in G.nodes()]
61
+ Y = [math.sin(_get_attr(G.nodes[n], ALIAS_THETA, 0.0)) for n in G.nodes()]
62
+ if not X:
63
+ return 1.0
64
+ R = ((sum(X)**2 + sum(Y)**2) ** 0.5) / max(1, len(X))
65
+ return float(R)
66
+
67
+ def carga_glifica(G, window: int | None = None) -> dict:
68
+ """Devuelve distribución de glifos aplicados en la red.
69
+ - window: si se indica, cuenta solo los últimos `window` eventos por nodo; si no, usa el maxlen del deque.
70
+ Retorna un dict con proporciones por glifo y agregados útiles.
71
+ """
72
+ total = Counter()
73
+ for n in G.nodes():
74
+ nd = G.nodes[n]
75
+ hist = nd.get("hist_glifos")
76
+ if not hist:
77
+ continue
78
+ seq = list(hist)
79
+ if window is not None and window > 0:
80
+ seq = seq[-window:]
81
+ total.update(seq)
82
+
83
+
84
+ count = sum(total.values())
85
+ if count == 0:
86
+ return {"_count": 0}
87
+
88
+
89
+ # Proporciones por glifo
90
+ dist = {k: v / count for k, v in total.items()}
91
+
92
+
93
+ # Agregados conceptuales (puedes ajustar categorías)
94
+ estabilizadores = ["I’L", "R’A", "U’M", "SH’A"]
95
+ disruptivos = ["O’Z", "Z’HIR", "NA’V", "T’HOL"]
96
+
97
+
98
+ dist["_estabilizadores"] = sum(dist.get(k, 0.0) for k in estabilizadores)
99
+ dist["_disruptivos"] = sum(dist.get(k, 0.0) for k in disruptivos)
100
+ dist["_count"] = count
101
+ return dist
102
+
103
+ def sigma_vector(G, window: int | None = None) -> dict:
104
+ """Vector de sentido Σ⃗ a partir de la distribución glífica reciente.
105
+ Devuelve dict con x, y, mag (0..1) y angle (rad)."""
106
+ # Distribución glífica (proporciones)
107
+ dist = carga_glifica(G, window=window)
108
+ if not dist or dist.get("_count", 0) == 0:
109
+ return {"x": 0.0, "y": 0.0, "mag": 0.0, "angle": 0.0}
110
+
111
+ # Mapeo polar de glifos principales en el plano de sentido
112
+ # (ordenado estabilización→expansión→acoplamiento→silencio→disonancia→mutación→transición→autoorg.)
113
+ angles = {
114
+ "I’L": 0.0,
115
+ "R’A": math.pi/4,
116
+ "U’M": math.pi/2,
117
+ "SH’A": 3*math.pi/4,
118
+ "O’Z": math.pi,
119
+ "Z’HIR": 5*math.pi/4,
120
+ "NA’V": 3*math.pi/2,
121
+ "T’HOL": 7*math.pi/4,
122
+ }
123
+ # Normaliza solo sobre glifos mapeados
124
+ total = sum(dist.get(k, 0.0) for k in angles.keys())
125
+ if total <= 0:
126
+ return {"x": 0.0, "y": 0.0, "mag": 0.0, "angle": 0.0}
127
+
128
+ x = 0.0
129
+ y = 0.0
130
+ for k, a in angles.items():
131
+ p = dist.get(k, 0.0) / total
132
+ x += p * math.cos(a)
133
+ y += p * math.sin(a)
134
+
135
+ mag = (x*x + y*y) ** 0.5
136
+ ang = math.atan2(y, x)
137
+ return {"x": float(x), "y": float(y), "mag": float(mag), "angle": float(ang)}
138
+
139
+ def wbar(G, window: int | None = None) -> float:
140
+ """Devuelve W̄ = media de C(t) en una ventana reciente."""
141
+ hist = G.graph.get("history", {})
142
+ cs = hist.get("C_steps", [])
143
+ if not cs:
144
+ # fallback: coherencia instantánea
145
+ return coherencia_global(G)
146
+ if window is None:
147
+ window = int(G.graph.get("WBAR_WINDOW", 25))
148
+ w = min(len(cs), max(1, int(window)))
149
+ return float(sum(cs[-w:]) / w)
tnfr/ontosim.py ADDED
@@ -0,0 +1,137 @@
1
+ """
2
+ ontosim.py — TNFR canónica
3
+
4
+ Módulo de orquestación mínima que encadena:
5
+ ΔNFR (campo) → Si → glifos → ecuación nodal → clamps → U’M → observadores → RE’MESH
6
+ """
7
+ from __future__ import annotations
8
+ import networkx as nx
9
+ import math
10
+ import random
11
+
12
+ from constants import DEFAULTS, attach_defaults
13
+ from dynamics import step as _step, run as _run
14
+ from dynamics import default_compute_delta_nfr
15
+
16
+ # API de alto nivel
17
+
18
+ def preparar_red(G: nx.Graph, *, override_defaults: bool = False, **overrides) -> nx.Graph:
19
+ attach_defaults(G, override=override_defaults)
20
+ if overrides:
21
+ from constants import merge_overrides
22
+ merge_overrides(G, **overrides)
23
+ # Inicializaciones blandas
24
+ G.graph.setdefault("history", {
25
+ "C_steps": [],
26
+ "stable_frac": [],
27
+ "phase_sync": [],
28
+ "kuramoto_R": [],
29
+ "sense_sigma_x": [],
30
+ "sense_sigma_y": [],
31
+ "sense_sigma_mag": [],
32
+ "sense_sigma_angle": [],
33
+ "iota": [],
34
+ "glyph_load_estab": [],
35
+ "glyph_load_disr": [],
36
+ "Si_mean": [],
37
+ "Si_hi_frac": [],
38
+ "Si_lo_frac": [],
39
+ "W_bar": [],
40
+ "phase_kG": [],
41
+ "phase_kL": [],
42
+ "phase_state": [],
43
+ "phase_R": [],
44
+ "phase_disr": [],
45
+ })
46
+ G.graph.setdefault("_epi_hist", [])
47
+ # Auto-attach del observador estándar si se pide
48
+ if G.graph.get("ATTACH_STD_OBSERVER", False):
49
+ try:
50
+ from observers import attach_standard_observer
51
+ attach_standard_observer(G)
52
+ except Exception as e:
53
+ G.graph.setdefault("_callback_errors", []).append(
54
+ {"event":"attach_std_observer","error":repr(e)}
55
+ )
56
+ # Hook explícito para ΔNFR (se puede sustituir luego con dynamics.set_delta_nfr_hook)
57
+ G.graph.setdefault("compute_delta_nfr", default_compute_delta_nfr)
58
+ G.graph.setdefault("_dnfr_hook_name", "default_compute_delta_nfr")
59
+ # Callbacks Γ(R): before_step / after_step / on_remesh
60
+ G.graph.setdefault("callbacks", {
61
+ "before_step": [],
62
+ "after_step": [],
63
+ "on_remesh": [],
64
+ })
65
+ G.graph.setdefault("_CALLBACKS_DOC",
66
+ "Interfaz Γ(R): registrar funciones (G, ctx) en callbacks['before_step'|'after_step'|'on_remesh']")
67
+
68
+ # --- Inicialización configurable de θ y νf ---
69
+ seed = int(G.graph.get("RANDOM_SEED", 0))
70
+ init_rand_phase = bool(G.graph.get("INIT_RANDOM_PHASE", DEFAULTS.get("INIT_RANDOM_PHASE", True)))
71
+
72
+ th_min = float(G.graph.get("INIT_THETA_MIN", DEFAULTS.get("INIT_THETA_MIN", -math.pi)))
73
+ th_max = float(G.graph.get("INIT_THETA_MAX", DEFAULTS.get("INIT_THETA_MAX", math.pi)))
74
+
75
+ vf_mode = str(G.graph.get("INIT_VF_MODE", DEFAULTS.get("INIT_VF_MODE", "uniform"))).lower()
76
+ vf_min_lim = float(G.graph.get("VF_MIN", DEFAULTS["VF_MIN"]))
77
+ vf_max_lim = float(G.graph.get("VF_MAX", DEFAULTS["VF_MAX"]))
78
+
79
+ vf_uniform_min = G.graph.get("INIT_VF_MIN", DEFAULTS.get("INIT_VF_MIN", None))
80
+ vf_uniform_max = G.graph.get("INIT_VF_MAX", DEFAULTS.get("INIT_VF_MAX", None))
81
+ if vf_uniform_min is None: vf_uniform_min = vf_min_lim
82
+ if vf_uniform_max is None: vf_uniform_max = vf_max_lim
83
+
84
+ vf_mean = float(G.graph.get("INIT_VF_MEAN", DEFAULTS.get("INIT_VF_MEAN", 0.5)))
85
+ vf_std = float(G.graph.get("INIT_VF_STD", DEFAULTS.get("INIT_VF_STD", 0.15)))
86
+ clamp_to_limits = bool(G.graph.get("INIT_VF_CLAMP_TO_LIMITS", DEFAULTS.get("INIT_VF_CLAMP_TO_LIMITS", True)))
87
+
88
+ for idx, n in enumerate(G.nodes()):
89
+ nd = G.nodes[n]
90
+ # EPI canónico
91
+ nd.setdefault("EPI", 0.0)
92
+
93
+ # θ aleatoria (opt-in por flag)
94
+ if init_rand_phase:
95
+ th_rng = random.Random(seed + 1009 * idx)
96
+ nd["θ"] = th_rng.uniform(th_min, th_max)
97
+ else:
98
+ nd.setdefault("θ", 0.0)
99
+
100
+ # νf distribuida
101
+ if vf_mode == "uniform":
102
+ vf_rng = random.Random(seed * 1000003 + idx)
103
+ vf = vf_rng.uniform(float(vf_uniform_min), float(vf_uniform_max))
104
+ elif vf_mode == "normal":
105
+ vf_rng = random.Random(seed * 1000003 + idx)
106
+ # normal truncada simple (rechazo)
107
+ for _ in range(16):
108
+ cand = vf_rng.normalvariate(vf_mean, vf_std)
109
+ if vf_min_lim <= cand <= vf_max_lim:
110
+ vf = cand
111
+ break
112
+ else:
113
+ # fallback: clamp del último candidato
114
+ vf = min(max(vf_rng.normalvariate(vf_mean, vf_std), vf_min_lim), vf_max_lim)
115
+ else:
116
+ # fallback: conserva si existe, si no 0.5
117
+ vf = float(nd.get("νf", 0.5))
118
+
119
+ if clamp_to_limits:
120
+ vf = min(max(vf, vf_min_lim), vf_max_lim)
121
+
122
+ nd["νf"] = float(vf)
123
+
124
+ return G
125
+
126
+ def step(G: nx.Graph, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool = True) -> None:
127
+ _step(G, dt=dt, use_Si=use_Si, apply_glyphs=apply_glyphs)
128
+
129
+ def run(G: nx.Graph, steps: int, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool = True) -> None:
130
+ _run(G, steps=steps, dt=dt, use_Si=use_Si, apply_glyphs=apply_glyphs)
131
+
132
+ # Helper rápido para pruebas manuales
133
+ if __name__ == "__main__":
134
+ G = nx.erdos_renyi_graph(30, 0.15)
135
+ preparar_red(G)
136
+ run(G, 100)
137
+ print("C(t) muestras:", G.graph["history"]["C_steps"][-5:])