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/__init__.py +45 -31
- tnfr/constants.py +183 -7
- tnfr/dynamics.py +543 -0
- tnfr/helpers.py +198 -0
- tnfr/main.py +37 -0
- tnfr/observers.py +149 -0
- tnfr/ontosim.py +137 -0
- tnfr/operators.py +296 -0
- tnfr-3.0.0.dist-info/METADATA +35 -0
- tnfr-3.0.0.dist-info/RECORD +13 -0
- tnfr-3.0.0.dist-info/top_level.txt +1 -0
- core/ontosim.py +0 -757
- matrix/operators.py +0 -496
- tnfr/core/ontosim.py +0 -1074
- tnfr/matrix/operators.py +0 -500
- tnfr/resonance/dynamics.py +0 -1305
- tnfr/utils/helpers.py +0 -357
- tnfr-1.0.dist-info/METADATA +0 -95
- tnfr-1.0.dist-info/RECORD +0 -14
- tnfr-1.0.dist-info/entry_points.txt +0 -2
- tnfr-1.0.dist-info/top_level.txt +0 -3
- {tnfr-1.0.dist-info → tnfr-3.0.0.dist-info}/WHEEL +0 -0
- {tnfr-1.0.dist-info → tnfr-3.0.0.dist-info}/licenses/LICENSE.txt +0 -0
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:])
|