tnfr 3.0.3__py3-none-any.whl → 4.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 +57 -56
- tnfr/cli.py +177 -0
- tnfr/constants.py +41 -11
- tnfr/dynamics.py +87 -31
- tnfr/gamma.py +91 -0
- tnfr/grammar.py +149 -0
- tnfr/helpers.py +43 -15
- tnfr/main.py +20 -10
- tnfr/metrics.py +211 -0
- tnfr/observers.py +19 -7
- tnfr/ontosim.py +12 -9
- tnfr/operators.py +23 -6
- tnfr/presets.py +24 -0
- tnfr/program.py +168 -0
- tnfr/scenarios.py +28 -0
- tnfr/sense.py +215 -0
- tnfr/trace.py +145 -0
- tnfr/types.py +17 -0
- tnfr-4.0.0.dist-info/METADATA +101 -0
- tnfr-4.0.0.dist-info/RECORD +24 -0
- tnfr-4.0.0.dist-info/entry_points.txt +2 -0
- tnfr-3.0.3.dist-info/licenses/LICENSE.txt → tnfr-4.0.0.dist-info/licenses/LICENSE.md +1 -1
- tnfr-3.0.3.dist-info/METADATA +0 -35
- tnfr-3.0.3.dist-info/RECORD +0 -13
- {tnfr-3.0.3.dist-info → tnfr-4.0.0.dist-info}/WHEEL +0 -0
- {tnfr-3.0.3.dist-info → tnfr-4.0.0.dist-info}/top_level.txt +0 -0
tnfr/__init__.py
CHANGED
|
@@ -1,56 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
from . import
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
from . import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
from . import
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
1
|
+
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
"""
|
|
4
|
+
TNFR — Teoría de la Naturaleza Fractal Resonante
|
|
5
|
+
API pública del paquete.
|
|
6
|
+
|
|
7
|
+
Ecuación nodal:
|
|
8
|
+
∂EPI/∂t = νf · ΔNFR(t)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "4.0.0"
|
|
12
|
+
|
|
13
|
+
# Re-exports de la API pública
|
|
14
|
+
from .dynamics import step, run, set_delta_nfr_hook
|
|
15
|
+
from .ontosim import preparar_red
|
|
16
|
+
from .observers import attach_standard_observer, coherencia_global, orden_kuramoto
|
|
17
|
+
from .gamma import GAMMA_REGISTRY, eval_gamma, kuramoto_R_psi
|
|
18
|
+
from .grammar import enforce_canonical_grammar, on_applied_glifo
|
|
19
|
+
from .sense import (
|
|
20
|
+
GLYPHS_CANONICAL, glyph_angle, glyph_unit,
|
|
21
|
+
sigma_vector_node, sigma_vector_global,
|
|
22
|
+
push_sigma_snapshot, sigma_series, sigma_rose,
|
|
23
|
+
register_sigma_callback,
|
|
24
|
+
)
|
|
25
|
+
from .metrics import (
|
|
26
|
+
register_metrics_callbacks,
|
|
27
|
+
Tg_global, Tg_by_node,
|
|
28
|
+
latency_series, glifogram_series,
|
|
29
|
+
glyph_top, glyph_dwell_stats,
|
|
30
|
+
)
|
|
31
|
+
from .trace import register_trace
|
|
32
|
+
from .program import play, seq, block, target, wait, THOL, TARGET, WAIT
|
|
33
|
+
from .cli import main as cli_main
|
|
34
|
+
from .scenarios import build_graph
|
|
35
|
+
from .presets import get_preset
|
|
36
|
+
from .types import NodeState
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"preparar_red",
|
|
40
|
+
"step", "run", "set_delta_nfr_hook",
|
|
41
|
+
"attach_standard_observer", "coherencia_global", "orden_kuramoto",
|
|
42
|
+
"GAMMA_REGISTRY", "eval_gamma", "kuramoto_R_psi",
|
|
43
|
+
"enforce_canonical_grammar", "on_applied_glifo",
|
|
44
|
+
"GLYPHS_CANONICAL", "glyph_angle", "glyph_unit",
|
|
45
|
+
"sigma_vector_node", "sigma_vector_global",
|
|
46
|
+
"push_sigma_snapshot", "sigma_series", "sigma_rose",
|
|
47
|
+
"register_sigma_callback",
|
|
48
|
+
"register_metrics_callbacks",
|
|
49
|
+
"register_trace",
|
|
50
|
+
"Tg_global", "Tg_by_node",
|
|
51
|
+
"latency_series", "glifogram_series",
|
|
52
|
+
"glyph_top", "glyph_dwell_stats",
|
|
53
|
+
"play", "seq", "block", "target", "wait", "THOL", "TARGET", "WAIT",
|
|
54
|
+
"__version__",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
__all__ += ["cli_main", "build_graph", "get_preset", "NodeState"]
|
tnfr/cli.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
try: # pragma: no cover - opcional
|
|
7
|
+
import yaml # type: ignore
|
|
8
|
+
except Exception: # pragma: no cover - yaml es opcional
|
|
9
|
+
yaml = None
|
|
10
|
+
|
|
11
|
+
import networkx as nx
|
|
12
|
+
|
|
13
|
+
from .constants import inject_defaults, DEFAULTS
|
|
14
|
+
from .sense import register_sigma_callback, sigma_series, sigma_rose
|
|
15
|
+
from .metrics import (
|
|
16
|
+
register_metrics_callbacks,
|
|
17
|
+
Tg_global,
|
|
18
|
+
latency_series,
|
|
19
|
+
glifogram_series,
|
|
20
|
+
glyph_top,
|
|
21
|
+
)
|
|
22
|
+
from .trace import register_trace
|
|
23
|
+
from .program import play, seq, block, wait, target
|
|
24
|
+
from .dynamics import step, _update_history
|
|
25
|
+
from .scenarios import build_graph
|
|
26
|
+
from .presets import get_preset
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _save_json(path: str, data: Any) -> None:
|
|
30
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
31
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _load_sequence(path: str) -> List[Any]:
|
|
35
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
36
|
+
text = f.read()
|
|
37
|
+
if path.endswith(".yaml") or path.endswith(".yml"):
|
|
38
|
+
if not yaml:
|
|
39
|
+
raise RuntimeError("pyyaml no está instalado, usa JSON o instala pyyaml")
|
|
40
|
+
data = yaml.safe_load(text)
|
|
41
|
+
else:
|
|
42
|
+
data = json.loads(text)
|
|
43
|
+
|
|
44
|
+
def parse_token(tok: Any):
|
|
45
|
+
if isinstance(tok, str):
|
|
46
|
+
return tok
|
|
47
|
+
if isinstance(tok, dict):
|
|
48
|
+
if "WAIT" in tok:
|
|
49
|
+
return wait(int(tok["WAIT"]))
|
|
50
|
+
if "TARGET" in tok:
|
|
51
|
+
return target(tok["TARGET"])
|
|
52
|
+
if "THOL" in tok:
|
|
53
|
+
spec = tok["THOL"] or {}
|
|
54
|
+
b = [_parse_inner(x) for x in spec.get("body", [])]
|
|
55
|
+
return block(*b, repeat=int(spec.get("repeat", 1)), close=spec.get("close"))
|
|
56
|
+
raise ValueError(f"Token inválido: {tok}")
|
|
57
|
+
|
|
58
|
+
def _parse_inner(x: Any):
|
|
59
|
+
return parse_token(x)
|
|
60
|
+
|
|
61
|
+
return [parse_token(t) for t in data]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _attach_callbacks(G: nx.Graph) -> None:
|
|
65
|
+
inject_defaults(G, DEFAULTS)
|
|
66
|
+
register_sigma_callback(G)
|
|
67
|
+
register_metrics_callbacks(G)
|
|
68
|
+
register_trace(G)
|
|
69
|
+
_update_history(G)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def cmd_run(args: argparse.Namespace) -> int:
|
|
73
|
+
G = build_graph(n=args.nodes, topology=args.topology, seed=args.seed)
|
|
74
|
+
_attach_callbacks(G)
|
|
75
|
+
|
|
76
|
+
if args.preset:
|
|
77
|
+
program = get_preset(args.preset)
|
|
78
|
+
play(G, program)
|
|
79
|
+
else:
|
|
80
|
+
steps = int(args.steps or 100)
|
|
81
|
+
for _ in range(steps):
|
|
82
|
+
step(G)
|
|
83
|
+
|
|
84
|
+
if args.save_history:
|
|
85
|
+
_save_json(args.save_history, G.graph.get("history", {}))
|
|
86
|
+
|
|
87
|
+
if args.summary:
|
|
88
|
+
tg = Tg_global(G, normalize=True)
|
|
89
|
+
lat = latency_series(G)
|
|
90
|
+
print("Top glifos por Tg:", glyph_top(G, k=5))
|
|
91
|
+
if lat["value"]:
|
|
92
|
+
print("Latencia media:", sum(lat["value"]) / max(1, len(lat["value"])) )
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def cmd_sequence(args: argparse.Namespace) -> int:
|
|
97
|
+
G = build_graph(n=args.nodes, topology=args.topology, seed=args.seed)
|
|
98
|
+
_attach_callbacks(G)
|
|
99
|
+
|
|
100
|
+
if args.preset:
|
|
101
|
+
program = get_preset(args.preset)
|
|
102
|
+
elif args.sequence_file:
|
|
103
|
+
program = _load_sequence(args.sequence_file)
|
|
104
|
+
else:
|
|
105
|
+
program = seq("A’L", "E’N", "I’L", block("O’Z", "Z’HIR", "I’L", repeat=1), "R’A", "SH’A")
|
|
106
|
+
|
|
107
|
+
play(G, program)
|
|
108
|
+
|
|
109
|
+
if args.save_history:
|
|
110
|
+
_save_json(args.save_history, G.graph.get("history", {}))
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def cmd_metrics(args: argparse.Namespace) -> int:
|
|
115
|
+
G = build_graph(n=args.nodes, topology=args.topology, seed=args.seed)
|
|
116
|
+
_attach_callbacks(G)
|
|
117
|
+
for _ in range(int(args.steps or 200)):
|
|
118
|
+
step(G)
|
|
119
|
+
|
|
120
|
+
tg = Tg_global(G, normalize=True)
|
|
121
|
+
lat = latency_series(G)
|
|
122
|
+
rose = sigma_rose(G)
|
|
123
|
+
glifo = glifogram_series(G)
|
|
124
|
+
|
|
125
|
+
out = {
|
|
126
|
+
"Tg_global": tg,
|
|
127
|
+
"latency_mean": (sum(lat["value"]) / max(1, len(lat["value"])) ) if lat["value"] else 0.0,
|
|
128
|
+
"rose": rose,
|
|
129
|
+
"glifogram": {k: v[:10] for k, v in glifo.items()},
|
|
130
|
+
}
|
|
131
|
+
if args.save:
|
|
132
|
+
_save_json(args.save, out)
|
|
133
|
+
else:
|
|
134
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
139
|
+
p = argparse.ArgumentParser(prog="tnfr")
|
|
140
|
+
sub = p.add_subparsers(dest="cmd")
|
|
141
|
+
|
|
142
|
+
p_run = sub.add_parser("run", help="Correr escenario libre o preset y opcionalmente exportar history")
|
|
143
|
+
p_run.add_argument("--nodes", type=int, default=24)
|
|
144
|
+
p_run.add_argument("--topology", choices=["ring", "complete", "erdos"], default="ring")
|
|
145
|
+
p_run.add_argument("--steps", type=int, default=200)
|
|
146
|
+
p_run.add_argument("--seed", type=int, default=1)
|
|
147
|
+
p_run.add_argument("--preset", type=str, default=None)
|
|
148
|
+
p_run.add_argument("--save-history", dest="save_history", type=str, default=None)
|
|
149
|
+
p_run.add_argument("--summary", action="store_true")
|
|
150
|
+
p_run.set_defaults(func=cmd_run)
|
|
151
|
+
|
|
152
|
+
p_seq = sub.add_parser("sequence", help="Ejecutar una secuencia (preset o YAML/JSON)")
|
|
153
|
+
p_seq.add_argument("--nodes", type=int, default=24)
|
|
154
|
+
p_seq.add_argument("--topology", choices=["ring", "complete", "erdos"], default="ring")
|
|
155
|
+
p_seq.add_argument("--seed", type=int, default=1)
|
|
156
|
+
p_seq.add_argument("--preset", type=str, default=None)
|
|
157
|
+
p_seq.add_argument("--sequence-file", type=str, default=None)
|
|
158
|
+
p_seq.add_argument("--save-history", dest="save_history", type=str, default=None)
|
|
159
|
+
p_seq.set_defaults(func=cmd_sequence)
|
|
160
|
+
|
|
161
|
+
p_met = sub.add_parser("metrics", help="Correr breve y volcar métricas clave")
|
|
162
|
+
p_met.add_argument("--nodes", type=int, default=24)
|
|
163
|
+
p_met.add_argument("--topology", choices=["ring", "complete", "erdos"], default="ring")
|
|
164
|
+
p_met.add_argument("--steps", type=int, default=300)
|
|
165
|
+
p_met.add_argument("--seed", type=int, default=1)
|
|
166
|
+
p_met.add_argument("--save", type=str, default=None)
|
|
167
|
+
p_met.set_defaults(func=cmd_metrics)
|
|
168
|
+
|
|
169
|
+
args = p.parse_args(argv)
|
|
170
|
+
if not hasattr(args, "func"):
|
|
171
|
+
p.print_help()
|
|
172
|
+
return 1
|
|
173
|
+
return int(args.func(args))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__": # pragma: no cover
|
|
177
|
+
raise SystemExit(main())
|
tnfr/constants.py
CHANGED
|
@@ -10,7 +10,7 @@ from typing import Dict, Any
|
|
|
10
10
|
# -------------------------
|
|
11
11
|
# Parámetros canónicos
|
|
12
12
|
# -------------------------
|
|
13
|
-
DEFAULTS: Dict[str, Any] = {
|
|
13
|
+
DEFAULTS: Dict[str, Any] = {
|
|
14
14
|
# Discretización
|
|
15
15
|
"DT": 1.0,
|
|
16
16
|
|
|
@@ -147,22 +147,52 @@ DEFAULTS: Dict[str, Any] = {
|
|
|
147
147
|
"dnfr_hi": 0.50, "dnfr_lo": 0.10,
|
|
148
148
|
"accel_hi": 0.50, "accel_lo": 0.10
|
|
149
149
|
},
|
|
150
|
-
# Callbacks Γ(R)
|
|
151
|
-
"
|
|
152
|
-
|
|
150
|
+
# Callbacks Γ(R)
|
|
151
|
+
"GAMMA": {
|
|
152
|
+
"type": "none", # 'none' | 'kuramoto_linear' | 'kuramoto_bandpass'
|
|
153
|
+
"beta": 0.0,
|
|
154
|
+
"R0": 0.0,
|
|
155
|
+
},
|
|
156
|
+
"CALLBACKS_STRICT": False, # si True, un error en callback detiene; si False, se loguea y continúa
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Gramática glífica canónica
|
|
160
|
+
DEFAULTS.setdefault("GRAMMAR_CANON", {
|
|
161
|
+
"enabled": True, # activar la gramática canónica
|
|
162
|
+
"zhir_requires_oz_window": 3, # cuántos pasos atrás buscamos O’Z
|
|
163
|
+
"zhir_dnfr_min": 0.05, # si |ΔNFR|_norm < este valor, no permitimos Z’HIR sin O’Z
|
|
164
|
+
"thol_min_len": 2,
|
|
165
|
+
"thol_max_len": 6,
|
|
166
|
+
"thol_close_dnfr": 0.15, # si el campo calma, cerramos con SH’A/NU’L
|
|
167
|
+
"si_high": 0.66, # umbral para elegir NU’L vs SH’A al cerrar
|
|
168
|
+
})
|
|
153
169
|
|
|
154
170
|
|
|
155
171
|
# -------------------------
|
|
156
172
|
# Utilidades
|
|
157
173
|
# -------------------------
|
|
158
174
|
|
|
159
|
-
def attach_defaults(G, override: bool = False) -> None:
|
|
160
|
-
"""Escribe DEFAULTS en G.graph (sin sobreescribir si override=False)."""
|
|
161
|
-
G.graph.setdefault("_tnfr_defaults_attached", False)
|
|
162
|
-
for k, v in DEFAULTS.items():
|
|
163
|
-
if override or k not in G.graph:
|
|
164
|
-
G.graph[k] = v
|
|
165
|
-
G.graph["_tnfr_defaults_attached"] = True
|
|
175
|
+
def attach_defaults(G, override: bool = False) -> None:
|
|
176
|
+
"""Escribe DEFAULTS en G.graph (sin sobreescribir si override=False)."""
|
|
177
|
+
G.graph.setdefault("_tnfr_defaults_attached", False)
|
|
178
|
+
for k, v in DEFAULTS.items():
|
|
179
|
+
if override or k not in G.graph:
|
|
180
|
+
G.graph[k] = v
|
|
181
|
+
G.graph["_tnfr_defaults_attached"] = True
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def inject_defaults(G, defaults: Dict[str, Any] = DEFAULTS, override: bool = False) -> None:
|
|
185
|
+
"""Alias de conveniencia para inyectar ``DEFAULTS`` en ``G.graph``.
|
|
186
|
+
|
|
187
|
+
Permite pasar un diccionario de *defaults* alternativo y mantiene la
|
|
188
|
+
semántica de ``attach_defaults`` existente. Si ``override`` es ``True`` se
|
|
189
|
+
sobreescriben valores ya presentes.
|
|
190
|
+
"""
|
|
191
|
+
G.graph.setdefault("_tnfr_defaults_attached", False)
|
|
192
|
+
for k, v in defaults.items():
|
|
193
|
+
if override or k not in G.graph:
|
|
194
|
+
G.graph[k] = v
|
|
195
|
+
G.graph["_tnfr_defaults_attached"] = True
|
|
166
196
|
|
|
167
197
|
|
|
168
198
|
def merge_overrides(G, **overrides) -> None:
|
tnfr/dynamics.py
CHANGED
|
@@ -13,11 +13,15 @@ Incluye:
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
from typing import Dict, Any, Iterable
|
|
15
15
|
import math
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
from
|
|
20
|
-
from
|
|
16
|
+
from collections import deque
|
|
17
|
+
import networkx as nx
|
|
18
|
+
|
|
19
|
+
from .observers import sincronía_fase, carga_glifica, orden_kuramoto, sigma_vector
|
|
20
|
+
from .operators import aplicar_remesh_si_estabilizacion_global
|
|
21
|
+
from .grammar import select_and_apply_with_grammar
|
|
22
|
+
from .constants import DEFAULTS, ALIAS_VF, ALIAS_THETA, ALIAS_DNFR, ALIAS_EPI, ALIAS_SI, ALIAS_dEPI, ALIAS_D2EPI
|
|
23
|
+
from .gamma import eval_gamma
|
|
24
|
+
from .helpers import (
|
|
21
25
|
clamp, clamp01, list_mean, phase_distance,
|
|
22
26
|
_get_attr, _set_attr, media_vecinal, fase_media,
|
|
23
27
|
invoke_callbacks, reciente_glifo
|
|
@@ -50,6 +54,7 @@ def _write_dnfr_metadata(G, *, weights: dict, hook_name: str, note: str | None =
|
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
def default_compute_delta_nfr(G) -> None:
|
|
57
|
+
"""Calcula ΔNFR mezclando gradientes de fase, EPI y νf según pesos."""
|
|
53
58
|
w = G.graph.get("DNFR_WEIGHTS", DEFAULTS["DNFR_WEIGHTS"]) # dict
|
|
54
59
|
w_phase = float(w.get("phase", 0.34))
|
|
55
60
|
w_epi = float(w.get("epi", 0.33))
|
|
@@ -119,19 +124,49 @@ def dnfr_epi_vf_mixed(G) -> None:
|
|
|
119
124
|
# Ecuación nodal
|
|
120
125
|
# -------------------------
|
|
121
126
|
|
|
122
|
-
def update_epi_via_nodal_equation(G, *, dt: float = None) -> None:
|
|
127
|
+
def update_epi_via_nodal_equation(G, *, dt: float = None, t: float | None = None) -> None:
|
|
128
|
+
"""Ecuación nodal TNFR.
|
|
129
|
+
|
|
130
|
+
Implementa la forma extendida de la ecuación nodal:
|
|
131
|
+
∂EPI/∂t = νf · ΔNFR(t) + Γi(R)
|
|
132
|
+
|
|
133
|
+
Donde:
|
|
134
|
+
- EPI es la Estructura Primaria de Información del nodo.
|
|
135
|
+
- νf es la frecuencia estructural del nodo (Hz_str).
|
|
136
|
+
- ΔNFR(t) es el gradiente nodal (necesidad de reorganización),
|
|
137
|
+
típicamente una mezcla de componentes (p. ej. fase θ, EPI, νf).
|
|
138
|
+
- Γi(R) es el acoplamiento de red opcional en función del orden de Kuramoto R
|
|
139
|
+
(ver gamma.py), usado para modular la integración en red.
|
|
140
|
+
|
|
141
|
+
Referencias TNFR: ecuación nodal (manual), glosario νf/ΔNFR/EPI, operador Γ.
|
|
142
|
+
Efectos secundarios: cachea dEPI y actualiza EPI por integración explícita.
|
|
143
|
+
"""
|
|
144
|
+
if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)):
|
|
145
|
+
raise TypeError("G must be a networkx graph instance")
|
|
123
146
|
if dt is None:
|
|
124
147
|
dt = float(G.graph.get("DT", DEFAULTS["DT"]))
|
|
148
|
+
else:
|
|
149
|
+
if not isinstance(dt, (int, float)):
|
|
150
|
+
raise TypeError("dt must be a number")
|
|
151
|
+
if dt < 0:
|
|
152
|
+
raise ValueError("dt must be non-negative")
|
|
153
|
+
dt = float(dt)
|
|
154
|
+
if t is None:
|
|
155
|
+
t = float(G.graph.get("_t", 0.0))
|
|
156
|
+
else:
|
|
157
|
+
t = float(t)
|
|
125
158
|
for n in G.nodes():
|
|
126
159
|
nd = G.nodes[n]
|
|
127
160
|
vf = _get_attr(nd, ALIAS_VF, 0.0)
|
|
128
161
|
dnfr = _get_attr(nd, ALIAS_DNFR, 0.0)
|
|
129
162
|
dEPI_dt_prev = _get_attr(nd, ALIAS_dEPI, 0.0)
|
|
130
163
|
dEPI_dt = vf * dnfr
|
|
164
|
+
dEPI_dt += eval_gamma(G, n, t)
|
|
131
165
|
epi = _get_attr(nd, ALIAS_EPI, 0.0) + dt * dEPI_dt
|
|
132
166
|
_set_attr(nd, ALIAS_EPI, epi)
|
|
133
167
|
_set_attr(nd, ALIAS_dEPI, dEPI_dt)
|
|
134
168
|
_set_attr(nd, ALIAS_D2EPI, (dEPI_dt - dEPI_dt_prev) / dt if dt != 0 else 0.0)
|
|
169
|
+
G.graph["_t"] = t + dt
|
|
135
170
|
|
|
136
171
|
|
|
137
172
|
# -------------------------
|
|
@@ -175,20 +210,30 @@ def coordinar_fase_global_vecinal(G, fuerza_global: float | None = None, fuerza_
|
|
|
175
210
|
Si no se pasan fuerzas explícitas, adapta kG/kL según estado (disonante / transición / estable).
|
|
176
211
|
Estado se decide por R (Kuramoto) y carga glífica disruptiva reciente.
|
|
177
212
|
"""
|
|
213
|
+
g = G.graph
|
|
214
|
+
defaults = DEFAULTS
|
|
178
215
|
# 0) Si hay fuerzas explícitas, usar y salir del modo adaptativo
|
|
179
216
|
if (fuerza_global is not None) or (fuerza_vecinal is not None):
|
|
180
|
-
kG = float(
|
|
181
|
-
|
|
217
|
+
kG = float(
|
|
218
|
+
fuerza_global
|
|
219
|
+
if fuerza_global is not None
|
|
220
|
+
else g.get("PHASE_K_GLOBAL", defaults["PHASE_K_GLOBAL"])
|
|
221
|
+
)
|
|
222
|
+
kL = float(
|
|
223
|
+
fuerza_vecinal
|
|
224
|
+
if fuerza_vecinal is not None
|
|
225
|
+
else g.get("PHASE_K_LOCAL", defaults["PHASE_K_LOCAL"])
|
|
226
|
+
)
|
|
182
227
|
else:
|
|
183
228
|
# 1) Lectura de configuración
|
|
184
|
-
cfg =
|
|
185
|
-
kG = float(
|
|
186
|
-
kL = float(
|
|
229
|
+
cfg = g.get("PHASE_ADAPT", defaults.get("PHASE_ADAPT", {}))
|
|
230
|
+
kG = float(g.get("PHASE_K_GLOBAL", defaults["PHASE_K_GLOBAL"]))
|
|
231
|
+
kL = float(g.get("PHASE_K_LOCAL", defaults["PHASE_K_LOCAL"]))
|
|
187
232
|
|
|
188
233
|
if bool(cfg.get("enabled", False)):
|
|
189
234
|
# 2) Métricas actuales (no dependemos de history)
|
|
190
235
|
R = orden_kuramoto(G)
|
|
191
|
-
win = int(
|
|
236
|
+
win = int(g.get("GLYPH_LOAD_WINDOW", defaults["GLYPH_LOAD_WINDOW"]))
|
|
192
237
|
dist = carga_glifica(G, window=win)
|
|
193
238
|
disr = float(dist.get("_disruptivos", 0.0)) if dist else 0.0
|
|
194
239
|
|
|
@@ -216,7 +261,9 @@ def coordinar_fase_global_vecinal(G, fuerza_global: float | None = None, fuerza_
|
|
|
216
261
|
kG_t = 0.5 * (kG_min + kG_max)
|
|
217
262
|
kL_t = 0.5 * (kL_min + kL_max)
|
|
218
263
|
|
|
219
|
-
up = float(cfg.get("up", 0.10))
|
|
264
|
+
up = float(cfg.get("up", 0.10))
|
|
265
|
+
down = float(cfg.get("down", 0.07))
|
|
266
|
+
|
|
220
267
|
def _step(curr, target, mn, mx):
|
|
221
268
|
gain = up if target > curr else down
|
|
222
269
|
nxt = curr + gain * (target - curr)
|
|
@@ -226,14 +273,19 @@ def coordinar_fase_global_vecinal(G, fuerza_global: float | None = None, fuerza_
|
|
|
226
273
|
kL = _step(kL, kL_t, kL_min, kL_max)
|
|
227
274
|
|
|
228
275
|
# 5) Persistir en G.graph y log de serie
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
hist =
|
|
232
|
-
hist.setdefault("phase_kG", [])
|
|
233
|
-
hist.setdefault("phase_kL", [])
|
|
234
|
-
hist.setdefault("phase_state", [])
|
|
235
|
-
hist.setdefault("phase_R", [])
|
|
236
|
-
hist.setdefault("phase_disr", [])
|
|
276
|
+
g["PHASE_K_GLOBAL"] = kG
|
|
277
|
+
g["PHASE_K_LOCAL"] = kL
|
|
278
|
+
hist = g.setdefault("history", {})
|
|
279
|
+
hist_kG = hist.setdefault("phase_kG", [])
|
|
280
|
+
hist_kL = hist.setdefault("phase_kL", [])
|
|
281
|
+
hist_state = hist.setdefault("phase_state", [])
|
|
282
|
+
hist_R = hist.setdefault("phase_R", [])
|
|
283
|
+
hist_disr = hist.setdefault("phase_disr", [])
|
|
284
|
+
hist_kG.append(float(kG))
|
|
285
|
+
hist_kL.append(float(kL))
|
|
286
|
+
hist_state.append(state)
|
|
287
|
+
hist_R.append(float(R))
|
|
288
|
+
hist_disr.append(float(disr))
|
|
237
289
|
|
|
238
290
|
# 6) Fase GLOBAL (centroide) para empuje
|
|
239
291
|
X = list(math.cos(_get_attr(G.nodes[n], ALIAS_THETA, 0.0)) for n in G.nodes())
|
|
@@ -394,7 +446,7 @@ def step(G, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool
|
|
|
394
446
|
|
|
395
447
|
# 2) (opcional) Si
|
|
396
448
|
if use_Si:
|
|
397
|
-
from helpers import compute_Si
|
|
449
|
+
from .helpers import compute_Si
|
|
398
450
|
compute_Si(G, inplace=True)
|
|
399
451
|
|
|
400
452
|
# 2b) Normalizadores para selector paramétrico (por paso)
|
|
@@ -403,11 +455,15 @@ def step(G, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool
|
|
|
403
455
|
# 3) Selección glífica + aplicación
|
|
404
456
|
if apply_glyphs:
|
|
405
457
|
selector = G.graph.get("glyph_selector", default_glyph_selector)
|
|
406
|
-
from operators import aplicar_glifo
|
|
458
|
+
from .operators import aplicar_glifo
|
|
407
459
|
window = int(G.graph.get("GLYPH_HYSTERESIS_WINDOW", DEFAULTS["GLYPH_HYSTERESIS_WINDOW"]))
|
|
460
|
+
use_canon = bool(G.graph.get("GRAMMAR_CANON", DEFAULTS.get("GRAMMAR_CANON", {})).get("enabled", False))
|
|
408
461
|
for n in G.nodes():
|
|
409
|
-
|
|
410
|
-
|
|
462
|
+
if use_canon:
|
|
463
|
+
select_and_apply_with_grammar(G, n, selector, window)
|
|
464
|
+
else:
|
|
465
|
+
g = selector(G, n)
|
|
466
|
+
aplicar_glifo(G, n, g, window=window)
|
|
411
467
|
|
|
412
468
|
# 4) Ecuación nodal
|
|
413
469
|
update_epi_via_nodal_equation(G, dt=dt)
|
|
@@ -422,13 +478,13 @@ def step(G, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool
|
|
|
422
478
|
# 7) Observadores ligeros
|
|
423
479
|
_update_history(G)
|
|
424
480
|
# dynamics.py — dentro de step(), justo antes del punto 8)
|
|
425
|
-
epi_hist = G.graph.setdefault("_epi_hist", [])
|
|
426
|
-
epi_hist.append({n: _get_attr(G.nodes[n], ALIAS_EPI, 0.0) for n in G.nodes()})
|
|
427
|
-
# recorta el buffer para que no crezca sin límite
|
|
428
481
|
tau = int(G.graph.get("REMESH_TAU", DEFAULTS["REMESH_TAU"]))
|
|
429
|
-
maxlen = max(2*tau + 5, 64)
|
|
430
|
-
|
|
431
|
-
|
|
482
|
+
maxlen = max(2 * tau + 5, 64)
|
|
483
|
+
epi_hist = G.graph.get("_epi_hist")
|
|
484
|
+
if not isinstance(epi_hist, deque) or epi_hist.maxlen != maxlen:
|
|
485
|
+
epi_hist = deque(list(epi_hist or [])[-maxlen:], maxlen=maxlen)
|
|
486
|
+
G.graph["_epi_hist"] = epi_hist
|
|
487
|
+
epi_hist.append({n: _get_attr(G.nodes[n], ALIAS_EPI, 0.0) for n in G.nodes()})
|
|
432
488
|
|
|
433
489
|
# 8) RE’MESH condicionado
|
|
434
490
|
aplicar_remesh_si_estabilizacion_global(G)
|
tnfr/gamma.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""gamma.py — TNFR canónica
|
|
2
|
+
|
|
3
|
+
Γi(R): acoplamientos de red para la ecuación nodal extendida
|
|
4
|
+
∂EPI/∂t = νf · ΔNFR(t) + Γi(R)
|
|
5
|
+
|
|
6
|
+
Provee:
|
|
7
|
+
- kuramoto_R_psi(G): (R, ψ) orden de Kuramoto en la red
|
|
8
|
+
- GAMMA_REGISTRY: registro de acoplamientos canónicos
|
|
9
|
+
- eval_gamma(G, node, t): evalúa Γ para cada nodo según G.graph['GAMMA']
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
from typing import Dict, Any, Tuple
|
|
13
|
+
import math
|
|
14
|
+
import cmath
|
|
15
|
+
|
|
16
|
+
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
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def kuramoto_R_psi(G) -> Tuple[float, float]:
|
|
28
|
+
"""Devuelve (R, ψ) del orden de Kuramoto usando θ de todos los nodos."""
|
|
29
|
+
acc = 0 + 0j
|
|
30
|
+
n = 0
|
|
31
|
+
for node in G.nodes():
|
|
32
|
+
nd = G.nodes[node]
|
|
33
|
+
th = _get_attr(nd, ALIAS_THETA, 0.0)
|
|
34
|
+
acc += cmath.exp(1j * th)
|
|
35
|
+
n += 1
|
|
36
|
+
if n == 0:
|
|
37
|
+
return 0.0, 0.0
|
|
38
|
+
z = acc / n
|
|
39
|
+
return abs(z), math.atan2(z.imag, z.real)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# -----------------
|
|
43
|
+
# Γi(R) canónicos
|
|
44
|
+
# -----------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def gamma_none(G, node, t, cfg: Dict[str, Any]) -> float:
|
|
48
|
+
return 0.0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def gamma_kuramoto_linear(G, node, t, cfg: Dict[str, Any]) -> float:
|
|
52
|
+
"""Acoplamiento lineal de Kuramoto para Γi(R).
|
|
53
|
+
|
|
54
|
+
Fórmula: Γ = β · (R - R0) · cos(θ_i - ψ)
|
|
55
|
+
- R ∈ [0,1] es el orden global de fase.
|
|
56
|
+
- ψ es la fase media (dirección de coordinación).
|
|
57
|
+
- β, R0 son parámetros (ganancia/umbral).
|
|
58
|
+
|
|
59
|
+
Uso: refuerza integración cuando la red ya exhibe coherencia de fase (R>R0).
|
|
60
|
+
"""
|
|
61
|
+
beta = float(cfg.get("beta", 0.0))
|
|
62
|
+
R0 = float(cfg.get("R0", 0.0))
|
|
63
|
+
R, psi = kuramoto_R_psi(G)
|
|
64
|
+
th_i = _get_attr(G.nodes[node], ALIAS_THETA, 0.0)
|
|
65
|
+
return beta * (R - R0) * math.cos(th_i - psi)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def gamma_kuramoto_bandpass(G, node, t, cfg: Dict[str, Any]) -> float:
|
|
69
|
+
"""Γ = β · R(1-R) · sign(cos(θ_i - ψ))"""
|
|
70
|
+
beta = float(cfg.get("beta", 0.0))
|
|
71
|
+
R, psi = kuramoto_R_psi(G)
|
|
72
|
+
th_i = _get_attr(G.nodes[node], ALIAS_THETA, 0.0)
|
|
73
|
+
sgn = 1.0 if math.cos(th_i - psi) >= 0.0 else -1.0
|
|
74
|
+
return beta * R * (1.0 - R) * sgn
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
GAMMA_REGISTRY = {
|
|
78
|
+
"none": gamma_none,
|
|
79
|
+
"kuramoto_linear": gamma_kuramoto_linear,
|
|
80
|
+
"kuramoto_bandpass": gamma_kuramoto_bandpass,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def eval_gamma(G, node, t) -> float:
|
|
85
|
+
"""Evalúa Γi para `node` según la especificación en G.graph['GAMMA']."""
|
|
86
|
+
spec = G.graph.get("GAMMA", {"type": "none"})
|
|
87
|
+
fn = GAMMA_REGISTRY.get(spec.get("type", "none"), gamma_none)
|
|
88
|
+
try:
|
|
89
|
+
return float(fn(G, node, t, spec))
|
|
90
|
+
except Exception:
|
|
91
|
+
return 0.0
|