tnfr 4.3.0__py3-none-any.whl → 4.5.1__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 +41 -12
- tnfr/cli.py +53 -1
- tnfr/config.py +41 -0
- tnfr/constants.py +82 -25
- tnfr/dynamics.py +191 -42
- tnfr/gamma.py +17 -0
- tnfr/helpers.py +33 -21
- tnfr/metrics.py +368 -5
- tnfr/node.py +202 -0
- tnfr/observers.py +9 -1
- tnfr/operators.py +298 -125
- tnfr/structural.py +201 -0
- tnfr/types.py +2 -1
- tnfr/validators.py +38 -0
- tnfr-4.5.1.dist-info/METADATA +221 -0
- tnfr-4.5.1.dist-info/RECORD +28 -0
- tnfr-4.3.0.dist-info/METADATA +0 -109
- tnfr-4.3.0.dist-info/RECORD +0 -24
- {tnfr-4.3.0.dist-info → tnfr-4.5.1.dist-info}/WHEEL +0 -0
- {tnfr-4.3.0.dist-info → tnfr-4.5.1.dist-info}/entry_points.txt +0 -0
- {tnfr-4.3.0.dist-info → tnfr-4.5.1.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.3.0.dist-info → tnfr-4.5.1.dist-info}/top_level.txt +0 -0
tnfr/operators.py
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
# operators.py — TNFR canónica (ASCII-safe)
|
|
2
2
|
from __future__ import annotations
|
|
3
|
-
from typing import Dict, Any, Optional
|
|
3
|
+
from typing import Dict, Any, Optional, Iterable
|
|
4
4
|
import math
|
|
5
5
|
import random
|
|
6
6
|
import hashlib
|
|
7
|
-
|
|
8
|
-
from .
|
|
9
|
-
|
|
7
|
+
import networkx as nx
|
|
8
|
+
from networkx.algorithms import community as nx_comm
|
|
9
|
+
|
|
10
|
+
from .constants import DEFAULTS, ALIAS_EPI
|
|
11
|
+
from .helpers import (
|
|
12
|
+
clamp,
|
|
13
|
+
clamp01,
|
|
14
|
+
list_mean,
|
|
15
|
+
invoke_callbacks,
|
|
16
|
+
_get_attr,
|
|
17
|
+
_set_attr,
|
|
18
|
+
_get_attr_str,
|
|
19
|
+
_set_attr_str,
|
|
20
|
+
)
|
|
21
|
+
from .node import NodoProtocol, NodoNX
|
|
10
22
|
from collections import deque
|
|
11
23
|
|
|
12
24
|
"""
|
|
@@ -34,121 +46,156 @@ def _node_offset(G, n) -> int:
|
|
|
34
46
|
# Glifos (operadores locales)
|
|
35
47
|
# -------------------------
|
|
36
48
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
49
|
+
def _fase_media_node(node: NodoProtocol) -> float:
|
|
50
|
+
x = y = 0.0
|
|
51
|
+
count = 0
|
|
52
|
+
for v in node.neighbors():
|
|
53
|
+
th = getattr(v, "theta", 0.0)
|
|
54
|
+
x += math.cos(th)
|
|
55
|
+
y += math.sin(th)
|
|
56
|
+
count += 1
|
|
57
|
+
if count == 0:
|
|
58
|
+
return getattr(node, "theta", 0.0)
|
|
59
|
+
return math.atan2(y / count, x / count)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _op_AL(node: NodoProtocol) -> None: # A’L — Emisión
|
|
63
|
+
f = float(node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"]).get("AL_boost", 0.05))
|
|
64
|
+
node.EPI = node.EPI + f
|
|
65
|
+
node.epi_kind = "A’L"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _op_EN(node: NodoProtocol) -> None: # E’N — Recepción
|
|
69
|
+
mix = float(node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"]).get("EN_mix", 0.25))
|
|
70
|
+
epi = node.EPI
|
|
71
|
+
neigh = list(node.neighbors())
|
|
72
|
+
if not neigh:
|
|
73
|
+
return
|
|
74
|
+
epi_bar = list_mean(v.EPI for v in neigh) if neigh else epi
|
|
75
|
+
node.EPI = (1 - mix) * epi + mix * epi_bar
|
|
76
|
+
|
|
77
|
+
candidatos = [(abs(node.EPI), node.epi_kind)]
|
|
78
|
+
for v in neigh:
|
|
79
|
+
candidatos.append((abs(v.EPI), v.epi_kind))
|
|
80
|
+
node.epi_kind = max(candidatos, key=lambda x: x[0])[1] or "E’N"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _op_IL(node: NodoProtocol) -> None: # I’L — Coherencia
|
|
84
|
+
factor = float(node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"]).get("IL_dnfr_factor", 0.7))
|
|
85
|
+
node.dnfr = factor * getattr(node, "dnfr", 0.0)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _op_OZ(node: NodoProtocol) -> None: # O’Z — Disonancia
|
|
89
|
+
factor = float(node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"]).get("OZ_dnfr_factor", 1.3))
|
|
90
|
+
dnfr = getattr(node, "dnfr", 0.0)
|
|
91
|
+
if bool(node.graph.get("OZ_NOISE_MODE", False)):
|
|
92
|
+
base_seed = int(node.graph.get("RANDOM_SEED", 0))
|
|
93
|
+
step_idx = len(node.graph.get("history", {}).get("C_steps", []))
|
|
94
|
+
rnd = random.Random(base_seed + step_idx * 1000003 + node.offset() % 1009)
|
|
95
|
+
sigma = float(node.graph.get("OZ_SIGMA", 0.1))
|
|
69
96
|
noise = sigma * (2.0 * rnd.random() - 1.0)
|
|
70
|
-
|
|
97
|
+
node.dnfr = dnfr + noise
|
|
71
98
|
else:
|
|
72
|
-
|
|
99
|
+
node.dnfr = factor * dnfr if abs(dnfr) > 1e-9 else 0.1
|
|
100
|
+
|
|
73
101
|
|
|
74
|
-
def
|
|
75
|
-
k = float(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
thL = fase_media(G, n)
|
|
102
|
+
def _op_UM(node: NodoProtocol) -> None: # U’M — Acoplamiento
|
|
103
|
+
k = float(node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"]).get("UM_theta_push", 0.25))
|
|
104
|
+
th = node.theta
|
|
105
|
+
thL = _fase_media_node(node)
|
|
79
106
|
d = ((thL - th + math.pi) % (2 * math.pi) - math.pi)
|
|
80
|
-
|
|
107
|
+
node.theta = th + k * d
|
|
108
|
+
|
|
109
|
+
if bool(node.graph.get("UM_FUNCTIONAL_LINKS", False)):
|
|
110
|
+
thr = float(node.graph.get("UM_COMPAT_THRESHOLD", DEFAULTS.get("UM_COMPAT_THRESHOLD", 0.75)))
|
|
111
|
+
epi_i = node.EPI
|
|
112
|
+
si_i = node.Si
|
|
113
|
+
for j in node.all_nodes():
|
|
114
|
+
if j is node or node.has_edge(j):
|
|
115
|
+
continue
|
|
116
|
+
th_j = j.theta
|
|
117
|
+
dphi = abs(((th_j - th + math.pi) % (2 * math.pi)) - math.pi) / math.pi
|
|
118
|
+
epi_j = j.EPI
|
|
119
|
+
si_j = j.Si
|
|
120
|
+
epi_sim = 1.0 - abs(epi_i - epi_j) / (abs(epi_i) + abs(epi_j) + 1e-9)
|
|
121
|
+
si_sim = 1.0 - abs(si_i - si_j)
|
|
122
|
+
compat = (1 - dphi) * 0.5 + 0.25 * epi_sim + 0.25 * si_sim
|
|
123
|
+
if compat >= thr:
|
|
124
|
+
node.add_edge(j, compat)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _op_RA(node: NodoProtocol) -> None: # R’A — Resonancia
|
|
128
|
+
diff = float(node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"]).get("RA_epi_diff", 0.15))
|
|
129
|
+
epi = node.EPI
|
|
130
|
+
neigh = list(node.neighbors())
|
|
131
|
+
if not neigh:
|
|
132
|
+
return
|
|
133
|
+
epi_bar = list_mean(v.EPI for v in neigh)
|
|
134
|
+
node.EPI = epi + diff * (epi_bar - epi)
|
|
81
135
|
|
|
136
|
+
candidatos = [(abs(node.EPI), node.epi_kind)]
|
|
137
|
+
for v in neigh:
|
|
138
|
+
candidatos.append((abs(v.EPI), v.epi_kind))
|
|
139
|
+
node.epi_kind = max(candidatos, key=lambda x: x[0])[1] or "R’A"
|
|
82
140
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
nd = G.nodes[n]
|
|
129
|
-
dnfr = _get_attr(nd, ALIAS_DNFR, 0.0)
|
|
130
|
-
j = float(G.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"]).get("NAV_jitter", 0.05))
|
|
131
|
-
if bool(G.graph.get("NAV_RANDOM", True)):
|
|
132
|
-
# jitter uniforme en [-j, j] con semilla reproducible
|
|
133
|
-
base_seed = int(G.graph.get("RANDOM_SEED", 0))
|
|
134
|
-
# opcional: pequeño offset para evitar misma secuencia en todos los nodos/pasos
|
|
135
|
-
step_idx = len(G.graph.get("history", {}).get("C_steps", []))
|
|
136
|
-
rnd = random.Random(base_seed + step_idx*1000003 + _node_offset(G, n) % 1009)
|
|
141
|
+
|
|
142
|
+
def _op_SHA(node: NodoProtocol) -> None: # SH’A — Silencio
|
|
143
|
+
factor = float(node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"]).get("SHA_vf_factor", 0.85))
|
|
144
|
+
node.vf = factor * node.vf
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _op_VAL(node: NodoProtocol) -> None: # VA’L — Expansión
|
|
148
|
+
s = float(node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"]).get("VAL_scale", 1.15))
|
|
149
|
+
node.EPI = s * node.EPI
|
|
150
|
+
node.epi_kind = "VA’L"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _op_NUL(node: NodoProtocol) -> None: # NU’L — Contracción
|
|
154
|
+
s = float(node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"]).get("NUL_scale", 0.85))
|
|
155
|
+
node.EPI = s * node.EPI
|
|
156
|
+
node.epi_kind = "NU’L"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _op_THOL(node: NodoProtocol) -> None: # T’HOL — Autoorganización
|
|
160
|
+
a = float(node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"]).get("THOL_accel", 0.10))
|
|
161
|
+
node.dnfr = node.dnfr + a * getattr(node, "d2EPI", 0.0)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _op_ZHIR(node: NodoProtocol) -> None: # Z’HIR — Mutación
|
|
165
|
+
shift = float(node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"]).get("ZHIR_theta_shift", 1.57079632679))
|
|
166
|
+
node.theta = node.theta + shift
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _op_NAV(node: NodoProtocol) -> None: # NA’V — Transición
|
|
170
|
+
dnfr = node.dnfr
|
|
171
|
+
vf = node.vf
|
|
172
|
+
gf = node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"])
|
|
173
|
+
eta = float(gf.get("NAV_eta", 0.5))
|
|
174
|
+
strict = bool(node.graph.get("NAV_STRICT", False))
|
|
175
|
+
if strict:
|
|
176
|
+
base = vf
|
|
177
|
+
else:
|
|
178
|
+
sign = 1.0 if dnfr >= 0 else -1.0
|
|
179
|
+
target = sign * vf
|
|
180
|
+
base = (1.0 - eta) * dnfr + eta * target
|
|
181
|
+
j = float(gf.get("NAV_jitter", 0.05))
|
|
182
|
+
if bool(node.graph.get("NAV_RANDOM", True)):
|
|
183
|
+
base_seed = int(node.graph.get("RANDOM_SEED", 0))
|
|
184
|
+
step_idx = len(node.graph.get("history", {}).get("C_steps", []))
|
|
185
|
+
rnd = random.Random(base_seed + step_idx * 1000003 + node.offset() % 1009)
|
|
137
186
|
jitter = j * (2.0 * rnd.random() - 1.0)
|
|
138
187
|
else:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
_set_attr(nd, ALIAS_DNFR, dnfr + jitter)
|
|
188
|
+
jitter = j * (1 if base >= 0 else -1)
|
|
189
|
+
node.dnfr = base + jitter
|
|
142
190
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
step_idx = len(
|
|
146
|
-
last_warn =
|
|
191
|
+
|
|
192
|
+
def _op_REMESH(node: NodoProtocol) -> None: # RE’MESH — aviso
|
|
193
|
+
step_idx = len(node.graph.get("history", {}).get("C_steps", []))
|
|
194
|
+
last_warn = node.graph.get("_remesh_warn_step", None)
|
|
147
195
|
if last_warn != step_idx:
|
|
148
196
|
msg = "RE’MESH es a escala de red. Usa aplicar_remesh_si_estabilizacion_global(G) o aplicar_remesh_red(G)."
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
# no cambia estado local
|
|
197
|
+
node.graph.setdefault("history", {}).setdefault("events", []).append(("warn", {"step": step_idx, "node": None, "msg": msg}))
|
|
198
|
+
node.graph["_remesh_warn_step"] = step_idx
|
|
152
199
|
return
|
|
153
200
|
|
|
154
201
|
# -------------------------
|
|
@@ -156,33 +203,50 @@ def op_REMESH(G, n): # RE’MESH — se realiza a escala de red (no-op local co
|
|
|
156
203
|
# -------------------------
|
|
157
204
|
|
|
158
205
|
_NAME_TO_OP = {
|
|
159
|
-
"A’L":
|
|
160
|
-
"R’A":
|
|
161
|
-
"T’HOL":
|
|
206
|
+
"A’L": _op_AL, "E’N": _op_EN, "I’L": _op_IL, "O’Z": _op_OZ, "U’M": _op_UM,
|
|
207
|
+
"R’A": _op_RA, "SH’A": _op_SHA, "VA’L": _op_VAL, "NU’L": _op_NUL,
|
|
208
|
+
"T’HOL": _op_THOL, "Z’HIR": _op_ZHIR, "NA’V": _op_NAV, "RE’MESH": _op_REMESH,
|
|
162
209
|
}
|
|
163
210
|
|
|
164
211
|
|
|
165
|
-
def
|
|
166
|
-
|
|
212
|
+
def _wrap(fn):
|
|
213
|
+
def inner(obj, n=None):
|
|
214
|
+
node = obj if n is None else NodoNX(obj, n)
|
|
215
|
+
return fn(node)
|
|
216
|
+
return inner
|
|
167
217
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
218
|
+
op_AL = _wrap(_op_AL)
|
|
219
|
+
op_EN = _wrap(_op_EN)
|
|
220
|
+
op_IL = _wrap(_op_IL)
|
|
221
|
+
op_OZ = _wrap(_op_OZ)
|
|
222
|
+
op_UM = _wrap(_op_UM)
|
|
223
|
+
op_RA = _wrap(_op_RA)
|
|
224
|
+
op_SHA = _wrap(_op_SHA)
|
|
225
|
+
op_VAL = _wrap(_op_VAL)
|
|
226
|
+
op_NUL = _wrap(_op_NUL)
|
|
227
|
+
op_THOL = _wrap(_op_THOL)
|
|
228
|
+
op_ZHIR = _wrap(_op_ZHIR)
|
|
229
|
+
op_NAV = _wrap(_op_NAV)
|
|
230
|
+
op_REMESH = _wrap(_op_REMESH)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def aplicar_glifo_obj(node: NodoProtocol, glifo: str, *, window: Optional[int] = None) -> None:
|
|
234
|
+
"""Aplica ``glifo`` a un objeto que cumple :class:`NodoProtocol`."""
|
|
173
235
|
|
|
174
|
-
Relación con la gramática: la selección previa debe pasar por
|
|
175
|
-
`enforce_canonical_grammar` (grammar.py) para respetar compatibilidades,
|
|
176
|
-
precondición O’Z→Z’HIR y cierres T’HOL[...].
|
|
177
|
-
"""
|
|
178
236
|
glifo = str(glifo)
|
|
179
237
|
op = _NAME_TO_OP.get(glifo)
|
|
180
238
|
if not op:
|
|
181
239
|
return
|
|
182
240
|
if window is None:
|
|
183
|
-
window = int(
|
|
184
|
-
push_glifo(
|
|
185
|
-
op(
|
|
241
|
+
window = int(node.graph.get("GLYPH_HYSTERESIS_WINDOW", DEFAULTS["GLYPH_HYSTERESIS_WINDOW"]))
|
|
242
|
+
node.push_glifo(glifo, window)
|
|
243
|
+
op(node)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def aplicar_glifo(G, n, glifo: str, *, window: Optional[int] = None) -> None:
|
|
247
|
+
"""Adaptador para operar sobre grafos ``networkx``."""
|
|
248
|
+
node = NodoNX(G, n)
|
|
249
|
+
aplicar_glifo_obj(node, glifo, window=window)
|
|
186
250
|
|
|
187
251
|
|
|
188
252
|
# -------------------------
|
|
@@ -282,6 +346,115 @@ def aplicar_remesh_red(G) -> None:
|
|
|
282
346
|
# Callbacks Γ(R)
|
|
283
347
|
invoke_callbacks(G, "on_remesh", dict(meta))
|
|
284
348
|
|
|
349
|
+
|
|
350
|
+
def aplicar_remesh_red_topologico(
|
|
351
|
+
G,
|
|
352
|
+
mode: Optional[str] = None,
|
|
353
|
+
*,
|
|
354
|
+
k: Optional[int] = None,
|
|
355
|
+
p_rewire: float = 0.2,
|
|
356
|
+
seed: Optional[int] = None,
|
|
357
|
+
) -> None:
|
|
358
|
+
"""Remallado topológico aproximado.
|
|
359
|
+
|
|
360
|
+
- mode="knn": conecta cada nodo con sus ``k`` vecinos más similares en EPI
|
|
361
|
+
con probabilidad ``p_rewire``.
|
|
362
|
+
- mode="mst": sólo preserva un árbol de expansión mínima según distancia EPI.
|
|
363
|
+
- mode="community": agrupa por comunidades modulares y las conecta por
|
|
364
|
+
similitud intercomunidad.
|
|
365
|
+
|
|
366
|
+
Siempre preserva conectividad añadiendo un MST base.
|
|
367
|
+
"""
|
|
368
|
+
nodes = list(G.nodes())
|
|
369
|
+
n_before = len(nodes)
|
|
370
|
+
if n_before <= 1:
|
|
371
|
+
return
|
|
372
|
+
rnd = random.Random(seed)
|
|
373
|
+
|
|
374
|
+
if mode is None:
|
|
375
|
+
mode = str(G.graph.get("REMESH_MODE", DEFAULTS.get("REMESH_MODE", "knn")))
|
|
376
|
+
mode = str(mode)
|
|
377
|
+
|
|
378
|
+
# Similaridad basada en EPI (distancia absoluta)
|
|
379
|
+
epi = {n: _get_attr(G.nodes[n], ALIAS_EPI, 0.0) for n in nodes}
|
|
380
|
+
H = nx.Graph()
|
|
381
|
+
H.add_nodes_from(nodes)
|
|
382
|
+
for i, u in enumerate(nodes):
|
|
383
|
+
for v in nodes[i + 1 :]:
|
|
384
|
+
w = abs(epi[u] - epi[v])
|
|
385
|
+
H.add_edge(u, v, weight=w)
|
|
386
|
+
mst = nx.minimum_spanning_tree(H, weight="weight")
|
|
387
|
+
|
|
388
|
+
if mode == "community":
|
|
389
|
+
# Detectar comunidades y reconstruir la red con metanodos
|
|
390
|
+
comms = list(nx_comm.greedy_modularity_communities(G))
|
|
391
|
+
if len(comms) <= 1:
|
|
392
|
+
new_edges = set(mst.edges())
|
|
393
|
+
else:
|
|
394
|
+
k_val = (
|
|
395
|
+
int(k)
|
|
396
|
+
if k is not None
|
|
397
|
+
else int(G.graph.get("REMESH_COMMUNITY_K", DEFAULTS.get("REMESH_COMMUNITY_K", 2)))
|
|
398
|
+
)
|
|
399
|
+
# Grafo de comunidades basado en medias de EPI
|
|
400
|
+
C = nx.Graph()
|
|
401
|
+
for idx, comm in enumerate(comms):
|
|
402
|
+
members = list(comm)
|
|
403
|
+
epi_mean = list_mean(epi[n] for n in members)
|
|
404
|
+
C.add_node(idx)
|
|
405
|
+
_set_attr(C.nodes[idx], ALIAS_EPI, epi_mean)
|
|
406
|
+
C.nodes[idx]["members"] = members
|
|
407
|
+
for i in C.nodes():
|
|
408
|
+
for j in C.nodes():
|
|
409
|
+
if i < j:
|
|
410
|
+
w = abs(
|
|
411
|
+
_get_attr(C.nodes[i], ALIAS_EPI, 0.0)
|
|
412
|
+
- _get_attr(C.nodes[j], ALIAS_EPI, 0.0)
|
|
413
|
+
)
|
|
414
|
+
C.add_edge(i, j, weight=w)
|
|
415
|
+
mst_c = nx.minimum_spanning_tree(C, weight="weight")
|
|
416
|
+
new_edges = set(mst_c.edges())
|
|
417
|
+
for u in C.nodes():
|
|
418
|
+
epi_u = _get_attr(C.nodes[u], ALIAS_EPI, 0.0)
|
|
419
|
+
others = [v for v in C.nodes() if v != u]
|
|
420
|
+
others.sort(key=lambda v: abs(epi_u - _get_attr(C.nodes[v], ALIAS_EPI, 0.0)))
|
|
421
|
+
for v in others[:k_val]:
|
|
422
|
+
if rnd.random() < p_rewire:
|
|
423
|
+
new_edges.add(tuple(sorted((u, v))))
|
|
424
|
+
|
|
425
|
+
# Reemplazar nodos y aristas del grafo original por comunidades
|
|
426
|
+
G.remove_edges_from(list(G.edges()))
|
|
427
|
+
G.remove_nodes_from(list(G.nodes()))
|
|
428
|
+
for idx in C.nodes():
|
|
429
|
+
data = dict(C.nodes[idx])
|
|
430
|
+
G.add_node(idx, **data)
|
|
431
|
+
G.add_edges_from(new_edges)
|
|
432
|
+
|
|
433
|
+
if G.graph.get("REMESH_LOG_EVENTS", DEFAULTS["REMESH_LOG_EVENTS"]):
|
|
434
|
+
ev = G.graph.setdefault("history", {}).setdefault("remesh_events", [])
|
|
435
|
+
mapping = {idx: C.nodes[idx].get("members", []) for idx in C.nodes()}
|
|
436
|
+
ev.append({
|
|
437
|
+
"mode": "community",
|
|
438
|
+
"n_before": n_before,
|
|
439
|
+
"n_after": G.number_of_nodes(),
|
|
440
|
+
"mapping": mapping,
|
|
441
|
+
})
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
# Default/mode knn/mst operate on nodos originales
|
|
445
|
+
new_edges = set(mst.edges())
|
|
446
|
+
if mode == "knn":
|
|
447
|
+
k_val = int(k) if k is not None else int(G.graph.get("REMESH_COMMUNITY_K", DEFAULTS.get("REMESH_COMMUNITY_K", 2)))
|
|
448
|
+
k_val = max(1, k_val)
|
|
449
|
+
for u in nodes:
|
|
450
|
+
sims = sorted(nodes, key=lambda v: abs(epi[u] - epi[v]))
|
|
451
|
+
for v in sims[1 : k_val + 1]:
|
|
452
|
+
if rnd.random() < p_rewire:
|
|
453
|
+
new_edges.add(tuple(sorted((u, v))))
|
|
454
|
+
|
|
455
|
+
G.remove_edges_from(list(G.edges()))
|
|
456
|
+
G.add_edges_from(new_edges)
|
|
457
|
+
|
|
285
458
|
def aplicar_remesh_si_estabilizacion_global(G, pasos_estables_consecutivos: Optional[int] = None) -> None:
|
|
286
459
|
# Ventanas y umbrales
|
|
287
460
|
w_estab = (
|
tnfr/structural.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
"""API de operadores estructurales y secuencias TNFR.
|
|
3
|
+
|
|
4
|
+
Este módulo ofrece:
|
|
5
|
+
- Factoría `create_nfr` para inicializar redes/nodos TNFR.
|
|
6
|
+
- Clases de operador (`Operador` y derivados) con interfaz común.
|
|
7
|
+
- Registro de operadores `OPERADORES`.
|
|
8
|
+
- Utilidades `validate_sequence` y `run_sequence` para ejecutar
|
|
9
|
+
secuencias canónicas de operadores.
|
|
10
|
+
"""
|
|
11
|
+
from typing import Iterable, Tuple, List
|
|
12
|
+
import networkx as nx
|
|
13
|
+
|
|
14
|
+
from .dynamics import (
|
|
15
|
+
set_delta_nfr_hook,
|
|
16
|
+
update_epi_via_nodal_equation,
|
|
17
|
+
dnfr_epi_vf_mixed,
|
|
18
|
+
)
|
|
19
|
+
from .operators import aplicar_glifo
|
|
20
|
+
from .constants import ALIAS_EPI, ALIAS_VF, ALIAS_THETA
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# 1) Factoría NFR
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
def create_nfr(
|
|
28
|
+
name: str,
|
|
29
|
+
*,
|
|
30
|
+
epi: float = 0.0,
|
|
31
|
+
vf: float = 1.0,
|
|
32
|
+
theta: float = 0.0,
|
|
33
|
+
graph: nx.Graph | None = None,
|
|
34
|
+
dnfr_hook=dnfr_epi_vf_mixed,
|
|
35
|
+
) -> Tuple[nx.Graph, str]:
|
|
36
|
+
"""Crea una red (graph) con un nodo NFR inicializado.
|
|
37
|
+
|
|
38
|
+
Devuelve la tupla ``(G, name)`` para conveniencia.
|
|
39
|
+
"""
|
|
40
|
+
G = graph or nx.Graph()
|
|
41
|
+
G.add_node(
|
|
42
|
+
name,
|
|
43
|
+
**{
|
|
44
|
+
ALIAS_EPI[0]: float(epi),
|
|
45
|
+
ALIAS_VF[0]: float(vf),
|
|
46
|
+
ALIAS_THETA[0]: float(theta),
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
set_delta_nfr_hook(G, dnfr_hook)
|
|
50
|
+
return G, name
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# 2) Operadores estructurales como API de primer orden
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Operador:
|
|
59
|
+
"""Base para operadores TNFR.
|
|
60
|
+
|
|
61
|
+
Cada operador define ``name`` (identificador ASCII) y ``glyph`` (glifo
|
|
62
|
+
canónico). La llamada ejecuta el glifo correspondiente sobre el nodo.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
name = "operador"
|
|
66
|
+
glyph = None # tipo: str
|
|
67
|
+
|
|
68
|
+
def __call__(self, G: nx.Graph, node, **kw) -> None:
|
|
69
|
+
if self.glyph is None:
|
|
70
|
+
raise NotImplementedError("Operador sin glifo asignado")
|
|
71
|
+
aplicar_glifo(G, node, self.glyph, **kw)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Derivados concretos -------------------------------------------------------
|
|
75
|
+
class Emision(Operador):
|
|
76
|
+
name = "emision"
|
|
77
|
+
glyph = "A’L"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Recepcion(Operador):
|
|
81
|
+
name = "recepcion"
|
|
82
|
+
glyph = "E’N"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Coherencia(Operador):
|
|
86
|
+
name = "coherencia"
|
|
87
|
+
glyph = "I’L"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Disonancia(Operador):
|
|
91
|
+
name = "disonancia"
|
|
92
|
+
glyph = "O’Z"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Acoplamiento(Operador):
|
|
96
|
+
name = "acoplamiento"
|
|
97
|
+
glyph = "U’M"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Resonancia(Operador):
|
|
101
|
+
name = "resonancia"
|
|
102
|
+
glyph = "R’A"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Silencio(Operador):
|
|
106
|
+
name = "silencio"
|
|
107
|
+
glyph = "SH’A"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Expansion(Operador):
|
|
111
|
+
name = "expansion"
|
|
112
|
+
glyph = "VA’L"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Contraccion(Operador):
|
|
116
|
+
name = "contraccion"
|
|
117
|
+
glyph = "NU’L"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Autoorganizacion(Operador):
|
|
121
|
+
name = "autoorganizacion"
|
|
122
|
+
glyph = "T’HOL"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Mutacion(Operador):
|
|
126
|
+
name = "mutacion"
|
|
127
|
+
glyph = "Z’HIR"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class Transicion(Operador):
|
|
131
|
+
name = "transicion"
|
|
132
|
+
glyph = "NA’V"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Recursividad(Operador):
|
|
136
|
+
name = "recursividad"
|
|
137
|
+
glyph = "RE’MESH"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
OPERADORES = {
|
|
141
|
+
op().name: op
|
|
142
|
+
for op in [
|
|
143
|
+
Emision,
|
|
144
|
+
Recepcion,
|
|
145
|
+
Coherencia,
|
|
146
|
+
Disonancia,
|
|
147
|
+
Acoplamiento,
|
|
148
|
+
Resonancia,
|
|
149
|
+
Silencio,
|
|
150
|
+
Expansion,
|
|
151
|
+
Contraccion,
|
|
152
|
+
Autoorganizacion,
|
|
153
|
+
Mutacion,
|
|
154
|
+
Transicion,
|
|
155
|
+
Recursividad,
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# 3) Motor de secuencias + validador sintáctico
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
_INICIO_VALIDOS = {"emision", "recursividad"}
|
|
166
|
+
_TRAMO_INTERMEDIO = {"disonancia", "acoplamiento", "resonancia"}
|
|
167
|
+
_CIERRE_VALIDO = {"silencio", "transicion", "recursividad"}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def validate_sequence(nombres: List[str]) -> Tuple[bool, str]:
|
|
171
|
+
"""Valida reglas mínimas de la sintaxis TNFR."""
|
|
172
|
+
if not nombres:
|
|
173
|
+
return False, "secuencia vacía"
|
|
174
|
+
if nombres[0] not in _INICIO_VALIDOS:
|
|
175
|
+
return False, "debe iniciar en emisión o recursividad"
|
|
176
|
+
try:
|
|
177
|
+
i_rec = nombres.index("recepcion")
|
|
178
|
+
i_coh = nombres.index("coherencia", i_rec + 1)
|
|
179
|
+
except ValueError:
|
|
180
|
+
return False, "falta tramo entrada→coherencia"
|
|
181
|
+
if not any(n in _TRAMO_INTERMEDIO for n in nombres[i_coh + 1 :]):
|
|
182
|
+
return False, "falta tramo de tensión/acoplamiento/resonancia"
|
|
183
|
+
if not any(n in _CIERRE_VALIDO for n in nombres[-2:]):
|
|
184
|
+
return False, "falta cierre (silencio/transición/recursividad)"
|
|
185
|
+
return True, "ok"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def run_sequence(G: nx.Graph, node, ops: Iterable[Operador]) -> None:
|
|
189
|
+
"""Ejecuta una secuencia validada de operadores sobre el nodo dado."""
|
|
190
|
+
ops_list = list(ops)
|
|
191
|
+
nombres = [op.name for op in ops_list]
|
|
192
|
+
ok, msg = validate_sequence(nombres)
|
|
193
|
+
if not ok:
|
|
194
|
+
raise ValueError(f"Secuencia no válida: {msg}")
|
|
195
|
+
for op in ops_list:
|
|
196
|
+
op(G, node)
|
|
197
|
+
compute = G.graph.get("compute_delta_nfr")
|
|
198
|
+
if callable(compute):
|
|
199
|
+
compute(G)
|
|
200
|
+
update_epi_via_nodal_equation(G)
|
|
201
|
+
|