tnfr 3.5.0__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 +51 -17
- tnfr/cli.py +177 -0
- tnfr/constants.py +41 -11
- tnfr/dynamics.py +32 -3
- tnfr/gamma.py +91 -0
- tnfr/grammar.py +149 -0
- tnfr/helpers.py +37 -11
- tnfr/main.py +20 -10
- tnfr/metrics.py +211 -0
- tnfr/operators.py +12 -0
- 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-3.5.0.dist-info → tnfr-4.0.0.dist-info}/METADATA +10 -1
- tnfr-4.0.0.dist-info/RECORD +24 -0
- tnfr-4.0.0.dist-info/entry_points.txt +2 -0
- tnfr-3.5.0.dist-info/RECORD +0 -14
- tnfr-3.5.0.dist-info/entry_points.txt +0 -2
- {tnfr-3.5.0.dist-info → tnfr-4.0.0.dist-info}/WHEEL +0 -0
- {tnfr-3.5.0.dist-info → tnfr-4.0.0.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-3.5.0.dist-info → tnfr-4.0.0.dist-info}/top_level.txt +0 -0
tnfr/__init__.py
CHANGED
|
@@ -1,23 +1,57 @@
|
|
|
1
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__ = "
|
|
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
12
|
|
|
13
13
|
# Re-exports de la API pública
|
|
14
14
|
from .dynamics import step, run, set_delta_nfr_hook
|
|
15
15
|
from .ontosim import preparar_red
|
|
16
16
|
from .observers import attach_standard_observer, coherencia_global, orden_kuramoto
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
@@ -18,7 +18,9 @@ import networkx as nx
|
|
|
18
18
|
|
|
19
19
|
from .observers import sincronía_fase, carga_glifica, orden_kuramoto, sigma_vector
|
|
20
20
|
from .operators import aplicar_remesh_si_estabilizacion_global
|
|
21
|
+
from .grammar import select_and_apply_with_grammar
|
|
21
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
|
|
22
24
|
from .helpers import (
|
|
23
25
|
clamp, clamp01, list_mean, phase_distance,
|
|
24
26
|
_get_attr, _set_attr, media_vecinal, fase_media,
|
|
@@ -52,6 +54,7 @@ def _write_dnfr_metadata(G, *, weights: dict, hook_name: str, note: str | None =
|
|
|
52
54
|
|
|
53
55
|
|
|
54
56
|
def default_compute_delta_nfr(G) -> None:
|
|
57
|
+
"""Calcula ΔNFR mezclando gradientes de fase, EPI y νf según pesos."""
|
|
55
58
|
w = G.graph.get("DNFR_WEIGHTS", DEFAULTS["DNFR_WEIGHTS"]) # dict
|
|
56
59
|
w_phase = float(w.get("phase", 0.34))
|
|
57
60
|
w_epi = float(w.get("epi", 0.33))
|
|
@@ -121,7 +124,23 @@ def dnfr_epi_vf_mixed(G) -> None:
|
|
|
121
124
|
# Ecuación nodal
|
|
122
125
|
# -------------------------
|
|
123
126
|
|
|
124
|
-
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
|
+
"""
|
|
125
144
|
if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)):
|
|
126
145
|
raise TypeError("G must be a networkx graph instance")
|
|
127
146
|
if dt is None:
|
|
@@ -132,16 +151,22 @@ def update_epi_via_nodal_equation(G, *, dt: float = None) -> None:
|
|
|
132
151
|
if dt < 0:
|
|
133
152
|
raise ValueError("dt must be non-negative")
|
|
134
153
|
dt = float(dt)
|
|
154
|
+
if t is None:
|
|
155
|
+
t = float(G.graph.get("_t", 0.0))
|
|
156
|
+
else:
|
|
157
|
+
t = float(t)
|
|
135
158
|
for n in G.nodes():
|
|
136
159
|
nd = G.nodes[n]
|
|
137
160
|
vf = _get_attr(nd, ALIAS_VF, 0.0)
|
|
138
161
|
dnfr = _get_attr(nd, ALIAS_DNFR, 0.0)
|
|
139
162
|
dEPI_dt_prev = _get_attr(nd, ALIAS_dEPI, 0.0)
|
|
140
163
|
dEPI_dt = vf * dnfr
|
|
164
|
+
dEPI_dt += eval_gamma(G, n, t)
|
|
141
165
|
epi = _get_attr(nd, ALIAS_EPI, 0.0) + dt * dEPI_dt
|
|
142
166
|
_set_attr(nd, ALIAS_EPI, epi)
|
|
143
167
|
_set_attr(nd, ALIAS_dEPI, dEPI_dt)
|
|
144
168
|
_set_attr(nd, ALIAS_D2EPI, (dEPI_dt - dEPI_dt_prev) / dt if dt != 0 else 0.0)
|
|
169
|
+
G.graph["_t"] = t + dt
|
|
145
170
|
|
|
146
171
|
|
|
147
172
|
# -------------------------
|
|
@@ -432,9 +457,13 @@ def step(G, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool
|
|
|
432
457
|
selector = G.graph.get("glyph_selector", default_glyph_selector)
|
|
433
458
|
from .operators import aplicar_glifo
|
|
434
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))
|
|
435
461
|
for n in G.nodes():
|
|
436
|
-
|
|
437
|
-
|
|
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)
|
|
438
467
|
|
|
439
468
|
# 4) Ecuación nodal
|
|
440
469
|
update_epi_via_nodal_equation(G, dt=dt)
|
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
|
tnfr/grammar.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Dict, Any, Set
|
|
3
|
+
|
|
4
|
+
from .constants import (
|
|
5
|
+
DEFAULTS,
|
|
6
|
+
ALIAS_SI, ALIAS_DNFR, ALIAS_EPI,
|
|
7
|
+
)
|
|
8
|
+
from .helpers import _get_attr, clamp01, reciente_glifo
|
|
9
|
+
from collections import deque
|
|
10
|
+
|
|
11
|
+
# Glifos nominales (para evitar typos)
|
|
12
|
+
AL = "A’L"; EN = "E’N"; IL = "I’L"; OZ = "O’Z"; UM = "U’M"; RA = "R’A"; SHA = "SH’A"; VAL = "VA’L"; NUL = "NU’L"; THOL = "T’HOL"; ZHIR = "Z’HIR"; NAV = "NA’V"; REMESH = "RE’MESH"
|
|
13
|
+
|
|
14
|
+
# -------------------------
|
|
15
|
+
# Estado de gramática por nodo
|
|
16
|
+
# -------------------------
|
|
17
|
+
|
|
18
|
+
def _gram_state(nd: Dict[str, Any]) -> Dict[str, Any]:
|
|
19
|
+
"""Crea/retorna el estado de gramática nodal.
|
|
20
|
+
Campos:
|
|
21
|
+
- thol_open (bool)
|
|
22
|
+
- thol_len (int)
|
|
23
|
+
"""
|
|
24
|
+
st = nd.setdefault("_GRAM", {"thol_open": False, "thol_len": 0})
|
|
25
|
+
st.setdefault("thol_open", False)
|
|
26
|
+
st.setdefault("thol_len", 0)
|
|
27
|
+
return st
|
|
28
|
+
|
|
29
|
+
# -------------------------
|
|
30
|
+
# Compatibilidades canónicas (siguiente permitido)
|
|
31
|
+
# -------------------------
|
|
32
|
+
CANON_COMPAT: Dict[str, Set[str]] = {
|
|
33
|
+
# Inicio / apertura
|
|
34
|
+
AL: {EN, RA, NAV, VAL, UM},
|
|
35
|
+
EN: {IL, UM, RA, NAV},
|
|
36
|
+
# Estabilización / difusión / acople
|
|
37
|
+
IL: {RA, VAL, UM, SHA},
|
|
38
|
+
UM: {RA, IL, VAL, NAV},
|
|
39
|
+
RA: {IL, VAL, UM, NAV},
|
|
40
|
+
VAL: {UM, RA, IL, NAV},
|
|
41
|
+
# Disonancia → transición → mutación
|
|
42
|
+
OZ: {ZHIR, NAV},
|
|
43
|
+
ZHIR: {IL, NAV},
|
|
44
|
+
NAV: {OZ, ZHIR, RA, IL, UM},
|
|
45
|
+
# Cierres / latencias
|
|
46
|
+
SHA: {AL, EN},
|
|
47
|
+
NUL: {AL, IL},
|
|
48
|
+
# Bloques autoorganizativos
|
|
49
|
+
THOL: {OZ, ZHIR, NAV, RA, IL, UM, SHA, NUL},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Fallbacks canónicos si una transición no está permitida
|
|
53
|
+
CANON_FALLBACK: Dict[str, str] = {
|
|
54
|
+
AL: EN, EN: IL, IL: RA, UM: RA, RA: IL, VAL: RA, OZ: ZHIR, ZHIR: IL, NAV: RA, SHA: AL, NUL: AL, THOL: NAV,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# -------------------------
|
|
58
|
+
# Cierres T’HOL y precondiciones Z’HIR
|
|
59
|
+
# -------------------------
|
|
60
|
+
|
|
61
|
+
def _dnfr_norm(G, nd) -> float:
|
|
62
|
+
# Normalizador robusto: usa historial de |ΔNFR| máx guardado por dynamics (si existe)
|
|
63
|
+
norms = G.graph.get("_sel_norms") or {}
|
|
64
|
+
dmax = float(norms.get("dnfr_max", 1.0)) or 1.0
|
|
65
|
+
return clamp01(abs(_get_attr(nd, ALIAS_DNFR, 0.0)) / dmax)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _si(G, nd) -> float:
|
|
69
|
+
return clamp01(_get_attr(nd, ALIAS_SI, 0.5))
|
|
70
|
+
|
|
71
|
+
# -------------------------
|
|
72
|
+
# Núcleo: forzar gramática sobre un candidato
|
|
73
|
+
# -------------------------
|
|
74
|
+
|
|
75
|
+
def enforce_canonical_grammar(G, n, cand: str) -> str:
|
|
76
|
+
"""Valida/ajusta el glifo candidato según la gramática canónica.
|
|
77
|
+
|
|
78
|
+
Reglas clave:
|
|
79
|
+
- Compatibilidades de transición glífica (recorrido TNFR).
|
|
80
|
+
- O’Z→Z’HIR: la mutación requiere disonancia reciente o |ΔNFR| alto.
|
|
81
|
+
- T’HOL[...]: obliga cierre con SH’A o NU’L cuando el campo se estabiliza
|
|
82
|
+
o se alcanza el largo del bloque; mantiene estado por nodo.
|
|
83
|
+
|
|
84
|
+
Devuelve el glifo efectivo a aplicar.
|
|
85
|
+
"""
|
|
86
|
+
nd = G.nodes[n]
|
|
87
|
+
st = _gram_state(nd)
|
|
88
|
+
cfg = G.graph.get("GRAMMAR_CANON", DEFAULTS.get("GRAMMAR_CANON", {}))
|
|
89
|
+
|
|
90
|
+
# 0) Si vienen glifos fuera del alfabeto, no tocamos
|
|
91
|
+
if cand not in CANON_COMPAT:
|
|
92
|
+
return cand
|
|
93
|
+
|
|
94
|
+
# 1) Precondición O’Z→Z’HIR: mutación requiere disonancia reciente o campo fuerte
|
|
95
|
+
if cand == ZHIR:
|
|
96
|
+
win = int(cfg.get("zhir_requires_oz_window", 3))
|
|
97
|
+
dn_min = float(cfg.get("zhir_dnfr_min", 0.05))
|
|
98
|
+
if not reciente_glifo(nd, OZ, win) and _dnfr_norm(G, nd) < dn_min:
|
|
99
|
+
cand = OZ # forzamos paso por O’Z
|
|
100
|
+
|
|
101
|
+
# 2) Si estamos dentro de T’HOL, control de cierre obligado
|
|
102
|
+
if st.get("thol_open", False):
|
|
103
|
+
st["thol_len"] = int(st.get("thol_len", 0))
|
|
104
|
+
st["thol_len"] += 1
|
|
105
|
+
minlen = int(cfg.get("thol_min_len", 2))
|
|
106
|
+
maxlen = int(cfg.get("thol_max_len", 6))
|
|
107
|
+
close_dn = float(cfg.get("thol_close_dnfr", 0.15))
|
|
108
|
+
if st["thol_len"] >= maxlen or (st["thol_len"] >= minlen and _dnfr_norm(G, nd) <= close_dn):
|
|
109
|
+
cand = NUL if _si(G, nd) >= float(cfg.get("si_high", 0.66)) else SHA
|
|
110
|
+
|
|
111
|
+
# 3) Compatibilidades: si el anterior restringe el siguiente
|
|
112
|
+
prev = None
|
|
113
|
+
hist = nd.get("hist_glifos")
|
|
114
|
+
if hist:
|
|
115
|
+
try:
|
|
116
|
+
prev = list(hist)[-1]
|
|
117
|
+
except Exception:
|
|
118
|
+
prev = None
|
|
119
|
+
if prev in CANON_COMPAT and cand not in CANON_COMPAT[prev]:
|
|
120
|
+
cand = CANON_FALLBACK.get(prev, cand)
|
|
121
|
+
|
|
122
|
+
return cand
|
|
123
|
+
|
|
124
|
+
# -------------------------
|
|
125
|
+
# Post-selección: actualizar estado de gramática
|
|
126
|
+
# -------------------------
|
|
127
|
+
|
|
128
|
+
def on_applied_glifo(G, n, applied: str) -> None:
|
|
129
|
+
nd = G.nodes[n]
|
|
130
|
+
st = _gram_state(nd)
|
|
131
|
+
if applied == THOL:
|
|
132
|
+
st["thol_open"] = True
|
|
133
|
+
st["thol_len"] = 0
|
|
134
|
+
elif applied in (SHA, NUL):
|
|
135
|
+
st["thol_open"] = False
|
|
136
|
+
st["thol_len"] = 0
|
|
137
|
+
else:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
# -------------------------
|
|
141
|
+
# Integración con dynamics.step: helper de selección+aplicación
|
|
142
|
+
# -------------------------
|
|
143
|
+
|
|
144
|
+
def select_and_apply_with_grammar(G, n, selector, window: int) -> None:
|
|
145
|
+
from .operators import aplicar_glifo
|
|
146
|
+
cand = selector(G, n)
|
|
147
|
+
cand = enforce_canonical_grammar(G, n, cand)
|
|
148
|
+
aplicar_glifo(G, n, cand, window=window)
|
|
149
|
+
on_applied_glifo(G, n, cand)
|