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/sense.py ADDED
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+ from typing import Dict, Any, List, Tuple
3
+ import math
4
+ from collections import Counter
5
+
6
+ from .constants import DEFAULTS, ALIAS_SI, ALIAS_EPI
7
+ from .helpers import _get_attr, clamp01, register_callback
8
+
9
+ # -------------------------
10
+ # Canon: orden circular de glifos y ángulos
11
+ # -------------------------
12
+ GLYPHS_CANONICAL: List[str] = [
13
+ "A’L", # 0
14
+ "E’N", # 1
15
+ "I’L", # 2
16
+ "U’M", # 3
17
+ "R’A", # 4
18
+ "VA’L", # 5
19
+ "O’Z", # 6
20
+ "Z’HIR",# 7
21
+ "NA’V", # 8
22
+ "T’HOL",# 9
23
+ "NU’L", #10
24
+ "SH’A", #11
25
+ "RE’MESH" #12
26
+ ]
27
+
28
+ _SIGMA_ANGLES: Dict[str, float] = {g: (2.0*math.pi * i / len(GLYPHS_CANONICAL)) for i, g in enumerate(GLYPHS_CANONICAL)}
29
+
30
+ # -------------------------
31
+ # Config por defecto
32
+ # -------------------------
33
+ DEFAULTS.setdefault("SIGMA", {
34
+ "enabled": True,
35
+ "weight": "Si", # "Si" | "EPI" | "1"
36
+ "smooth": 0.0, # EMA sobre el vector global (0=off)
37
+ "history_key": "sigma_global", # dónde guardar en G.graph['history']
38
+ "per_node": False, # si True, guarda trayectoria σ por nodo (más pesado)
39
+ })
40
+
41
+ # -------------------------
42
+ # Utilidades básicas
43
+ # -------------------------
44
+
45
+ def glyph_angle(g: str) -> float:
46
+ return float(_SIGMA_ANGLES.get(g, 0.0))
47
+
48
+
49
+ def glyph_unit(g: str) -> complex:
50
+ a = glyph_angle(g)
51
+ return complex(math.cos(a), math.sin(a))
52
+
53
+
54
+ def _weight(G, n, mode: str) -> float:
55
+ nd = G.nodes[n]
56
+ if mode == "Si":
57
+ return clamp01(_get_attr(nd, ALIAS_SI, 0.5))
58
+ if mode == "EPI":
59
+ return max(0.0, float(_get_attr(nd, ALIAS_EPI, 0.0)))
60
+ return 1.0
61
+
62
+
63
+ def _last_glifo(nd: Dict[str, Any]) -> str | None:
64
+ hist = nd.get("hist_glifos")
65
+ if not hist:
66
+ return None
67
+ try:
68
+ return list(hist)[-1]
69
+ except Exception:
70
+ return None
71
+
72
+
73
+ # -------------------------
74
+ # σ por nodo y σ global
75
+ # -------------------------
76
+
77
+ def sigma_vector_node(G, n, weight_mode: str | None = None) -> Dict[str, float] | None:
78
+ nd = G.nodes[n]
79
+ g = _last_glifo(nd)
80
+ if g is None:
81
+ return None
82
+ w = _weight(G, n, weight_mode or G.graph.get("SIGMA", DEFAULTS["SIGMA"]).get("weight", "Si"))
83
+ z = glyph_unit(g) * w
84
+ x, y = z.real, z.imag
85
+ mag = math.hypot(x, y)
86
+ ang = math.atan2(y, x) if mag > 0 else glyph_angle(g)
87
+ return {"x": float(x), "y": float(y), "mag": float(mag), "angle": float(ang), "glifo": g, "w": float(w)}
88
+
89
+
90
+ def sigma_vector_global(G, weight_mode: str | None = None) -> Dict[str, float]:
91
+ """Vector global del plano del sentido σ.
92
+
93
+ Mapea el último glifo de cada nodo a un vector unitario en S¹, ponderado
94
+ por `Si` (o `EPI`/1), y promedia para obtener:
95
+ - componentes (x, y), magnitud |σ| y ángulo arg(σ).
96
+
97
+ Interpretación TNFR: |σ| mide cuán alineada está la red en su
98
+ **recorrido glífico**; arg(σ) indica la **dirección funcional** dominante
99
+ (p. ej., torno a I’L/RA para consolidación/distribución, O’Z/Z’HIR para cambio).
100
+ """
101
+ cfg = G.graph.get("SIGMA", DEFAULTS["SIGMA"])
102
+ weight_mode = weight_mode or cfg.get("weight", "Si")
103
+ acc = complex(0.0, 0.0)
104
+ cnt = 0
105
+ for n in G.nodes():
106
+ v = sigma_vector_node(G, n, weight_mode)
107
+ if v is None:
108
+ continue
109
+ acc += complex(v["x"], v["y"])
110
+ cnt += 1
111
+ if cnt == 0:
112
+ return {"x": 1.0, "y": 0.0, "mag": 1.0, "angle": 0.0, "n": 0}
113
+ x, y = acc.real / max(1, cnt), acc.imag / max(1, cnt)
114
+ mag = math.hypot(x, y)
115
+ ang = math.atan2(y, x)
116
+ return {"x": float(x), "y": float(y), "mag": float(mag), "angle": float(ang), "n": cnt}
117
+
118
+
119
+ # -------------------------
120
+ # Historia / series
121
+ # -------------------------
122
+
123
+ def _ensure_history(G):
124
+ if "history" not in G.graph:
125
+ G.graph["history"] = {}
126
+ return G.graph["history"]
127
+
128
+
129
+ def push_sigma_snapshot(G, t: float | None = None) -> None:
130
+ cfg = G.graph.get("SIGMA", DEFAULTS["SIGMA"])
131
+ if not cfg.get("enabled", True):
132
+ return
133
+ hist = _ensure_history(G)
134
+ key = cfg.get("history_key", "sigma_global")
135
+
136
+ # Global
137
+ sv = sigma_vector_global(G, cfg.get("weight", "Si"))
138
+
139
+ # Suavizado exponencial (EMA) opcional
140
+ alpha = float(cfg.get("smooth", 0.0))
141
+ if alpha > 0 and hist.get(key):
142
+ prev = hist[key][-1]
143
+ x = (1-alpha)*prev["x"] + alpha*sv["x"]
144
+ y = (1-alpha)*prev["y"] + alpha*sv["y"]
145
+ mag = math.hypot(x, y)
146
+ ang = math.atan2(y, x)
147
+ sv = {"x": x, "y": y, "mag": mag, "angle": ang, "n": sv.get("n", 0)}
148
+
149
+ sv["t"] = float(G.graph.get("_t", 0.0) if t is None else t)
150
+
151
+ hist.setdefault(key, []).append(sv)
152
+
153
+ # Conteo de glifos por paso (útil para rosa glífica)
154
+ counts = Counter()
155
+ for n in G.nodes():
156
+ g = _last_glifo(G.nodes[n])
157
+ if g:
158
+ counts[g] += 1
159
+ hist.setdefault("sigma_counts", []).append({"t": sv["t"], **counts})
160
+
161
+ # Trayectoria por nodo (opcional)
162
+ if cfg.get("per_node", False):
163
+ per = hist.setdefault("sigma_per_node", {})
164
+ for n in G.nodes():
165
+ nd = G.nodes[n]
166
+ g = _last_glifo(nd)
167
+ if not g:
168
+ continue
169
+ a = glyph_angle(g)
170
+ d = per.setdefault(n, [])
171
+ d.append({"t": sv["t"], "g": g, "angle": a})
172
+
173
+
174
+ # -------------------------
175
+ # Registro como callback automático (after_step)
176
+ # -------------------------
177
+
178
+ def register_sigma_callback(G) -> None:
179
+ register_callback(G, when="after_step", func=push_sigma_snapshot, name="sigma_snapshot")
180
+
181
+
182
+ # -------------------------
183
+ # Series de utilidad
184
+ # -------------------------
185
+
186
+ def sigma_series(G, key: str | None = None) -> Dict[str, List[float]]:
187
+ cfg = G.graph.get("SIGMA", DEFAULTS["SIGMA"])
188
+ key = key or cfg.get("history_key", "sigma_global")
189
+ hist = G.graph.get("history", {})
190
+ xs = hist.get(key, [])
191
+ if not xs:
192
+ return {"t": [], "angle": [], "mag": []}
193
+ return {
194
+ "t": [float(x.get("t", i)) for i, x in enumerate(xs)],
195
+ "angle": [float(x["angle"]) for x in xs],
196
+ "mag": [float(x["mag"]) for x in xs],
197
+ }
198
+
199
+
200
+ def sigma_rose(G, steps: int | None = None) -> Dict[str, int]:
201
+ """Histograma de glifos en los últimos `steps` pasos (o todos)."""
202
+ hist = G.graph.get("history", {})
203
+ counts = hist.get("sigma_counts", [])
204
+ if not counts:
205
+ return {g: 0 for g in GLYPHS_CANONICAL}
206
+ if steps is None or steps >= len(counts):
207
+ agg = Counter()
208
+ for row in counts:
209
+ agg.update({k: v for k, v in row.items() if k != "t"})
210
+ out = {g: int(agg.get(g, 0)) for g in GLYPHS_CANONICAL}
211
+ return out
212
+ agg = Counter()
213
+ for row in counts[-int(steps):]:
214
+ agg.update({k: v for k, v in row.items() if k != "t"})
215
+ return {g: int(agg.get(g, 0)) for g in GLYPHS_CANONICAL}
tnfr/trace.py ADDED
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Dict, List, Optional
3
+ from collections import Counter
4
+
5
+ from .constants import DEFAULTS
6
+ from .helpers import register_callback
7
+
8
+ try:
9
+ from .gamma import kuramoto_R_psi
10
+ except Exception: # pragma: no cover
11
+ def kuramoto_R_psi(G):
12
+ return 0.0, 0.0
13
+
14
+ try:
15
+ from .sense import sigma_vector_global
16
+ except Exception: # pragma: no cover
17
+ def sigma_vector_global(G, *args, **kwargs):
18
+ return {"x": 1.0, "y": 0.0, "mag": 1.0, "angle": 0.0, "n": 0}
19
+
20
+ # -------------------------
21
+ # Defaults
22
+ # -------------------------
23
+ DEFAULTS.setdefault("TRACE", {
24
+ "enabled": True,
25
+ "capture": ["gamma", "grammar", "selector", "dnfr_mix", "callbacks", "thol_state", "sigma", "kuramoto", "glifo_counts"],
26
+ "history_key": "trace_meta",
27
+ })
28
+
29
+ # -------------------------
30
+ # Helpers
31
+ # -------------------------
32
+
33
+ def _ensure_history(G):
34
+ if "history" not in G.graph:
35
+ G.graph["history"] = {}
36
+ return G.graph["history"]
37
+
38
+
39
+ def _last_glifo(nd: Dict[str, Any]) -> str | None:
40
+ h = nd.get("hist_glifos")
41
+ if not h:
42
+ return None
43
+ try:
44
+ return list(h)[-1]
45
+ except Exception:
46
+ return None
47
+
48
+
49
+ # -------------------------
50
+ # Snapshots
51
+ # -------------------------
52
+
53
+ def _trace_before(G, *args, **kwargs):
54
+ if not G.graph.get("TRACE", DEFAULTS["TRACE"]).get("enabled", True):
55
+ return
56
+ cfg = G.graph.get("TRACE", DEFAULTS["TRACE"])
57
+ capture: List[str] = list(cfg.get("capture", []))
58
+ hist = _ensure_history(G)
59
+ key = cfg.get("history_key", "trace_meta")
60
+
61
+ meta: Dict[str, Any] = {"t": float(G.graph.get("_t", 0.0)), "phase": "before"}
62
+
63
+ if "gamma" in capture:
64
+ meta["gamma"] = dict(G.graph.get("GAMMA", {}))
65
+
66
+ if "grammar" in capture:
67
+ meta["grammar"] = dict(G.graph.get("GRAMMAR_CANON", {}))
68
+
69
+ if "selector" in capture:
70
+ sel = G.graph.get("glyph_selector")
71
+ meta["selector"] = getattr(sel, "__name__", str(sel)) if sel else None
72
+
73
+ if "dnfr_mix" in capture:
74
+ # tratar de capturar varias convenciones posibles
75
+ mix = G.graph.get("DNFR_MIX") or G.graph.get("DELTA_NFR_MIX") or G.graph.get("NFR_MIX")
76
+ meta["dnfr_mix"] = mix if isinstance(mix, dict) else {"value": mix}
77
+
78
+ if "callbacks" in capture:
79
+ # si el motor guarda los callbacks, exponer nombres por fase
80
+ cb = G.graph.get("_callbacks")
81
+ if isinstance(cb, dict):
82
+ out = {k: [getattr(f, "__name__", "fn") for (_, f, *_rest) in v] if isinstance(v, list) else None for k, v in cb.items()}
83
+ meta["callbacks"] = out
84
+
85
+ if "thol_state" in capture:
86
+ # cuántos nodos tienen bloque T’HOL abierto
87
+ th_open = 0
88
+ for n in G.nodes():
89
+ st = G.nodes[n].get("_GRAM", {})
90
+ if st.get("thol_open", False):
91
+ th_open += 1
92
+ meta["thol_open_nodes"] = th_open
93
+
94
+ hist.setdefault(key, []).append(meta)
95
+
96
+
97
+ def _trace_after(G, *args, **kwargs):
98
+ if not G.graph.get("TRACE", DEFAULTS["TRACE"]).get("enabled", True):
99
+ return
100
+ cfg = G.graph.get("TRACE", DEFAULTS["TRACE"])
101
+ capture: List[str] = list(cfg.get("capture", []))
102
+ hist = _ensure_history(G)
103
+ key = cfg.get("history_key", "trace_meta")
104
+
105
+ meta: Dict[str, Any] = {"t": float(G.graph.get("_t", 0.0)), "phase": "after"}
106
+
107
+ if "kuramoto" in capture:
108
+ R, psi = kuramoto_R_psi(G)
109
+ meta["kuramoto"] = {"R": float(R), "psi": float(psi)}
110
+
111
+ if "sigma" in capture:
112
+ sv = sigma_vector_global(G)
113
+ meta["sigma"] = {"x": float(sv.get("x", 1.0)), "y": float(sv.get("y", 0.0)), "mag": float(sv.get("mag", 1.0)), "angle": float(sv.get("angle", 0.0))}
114
+
115
+ if "glifo_counts" in capture:
116
+ cnt = Counter()
117
+ for n in G.nodes():
118
+ g = _last_glifo(G.nodes[n])
119
+ if g:
120
+ cnt[g] += 1
121
+ meta["glifos"] = dict(cnt)
122
+
123
+ hist.setdefault(key, []).append(meta)
124
+
125
+
126
+ # -------------------------
127
+ # API
128
+ # -------------------------
129
+
130
+ def register_trace(G) -> None:
131
+ """Activa snapshots before/after step y vuelca metadatos operativos en history.
132
+
133
+ Guarda en G.graph['history'][TRACE.history_key] una lista de entradas {'phase': 'before'|'after', ...} con:
134
+ - gamma: especificación activa de Γi(R)
135
+ - grammar: configuración de gramática canónica
136
+ - selector: nombre del selector glífico
137
+ - dnfr_mix: mezcla (si el motor la expone en G.graph)
138
+ - callbacks: callbacks registrados por fase (si están en G.graph['_callbacks'])
139
+ - thol_open_nodes: cuántos nodos tienen bloque T’HOL abierto
140
+ - kuramoto: (R, ψ) de la red
141
+ - sigma: vector global del plano del sentido
142
+ - glifos: conteos por glifo tras el paso
143
+ """
144
+ register_callback(G, when="before_step", func=_trace_before, name="trace_before")
145
+ register_callback(G, when="after_step", func=_trace_after, name="trace_after")
tnfr/types.py ADDED
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+ from typing import Dict, Any
4
+
5
+
6
+ @dataclass
7
+ class NodeState:
8
+ EPI: float = 0.0
9
+ vf: float = 1.0 # νf
10
+ theta: float = 0.0 # θ
11
+ Si: float = 0.5
12
+ extra: Dict[str, Any] = field(default_factory=dict)
13
+
14
+ def to_attrs(self) -> Dict[str, Any]:
15
+ d = {"EPI": self.EPI, "νf": self.vf, "θ": self.theta, "Si": self.Si}
16
+ d.update(self.extra)
17
+ return d
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tnfr
3
- Version: 3.5.0
3
+ Version: 4.0.0
4
4
  Summary: TNFR canónica: dinámica glífica modular sobre redes.
5
5
  Author: fmg
6
6
  License: MIT
@@ -81,6 +81,9 @@ Dynamic: license-file
81
81
 
82
82
  **Mastering these pieces will let you extend the simulation, build analysis pipelines and connect the theory with computational applications.**
83
83
 
84
+ ## Optional Node environment
85
+ The repository includes a minimal `package.json` and `netlify.toml` used for an experimental Remix web demo. They are not required for the core Python package; feel free to ignore them unless you plan to build the demo via `npm run build`.
86
+
84
87
  ## Testing
85
88
 
86
89
  Install the dependencies and project in editable mode before running the test suite with `pytest`:
@@ -89,4 +92,10 @@ Install the dependencies and project in editable mode before running the test su
89
92
  pip install networkx
90
93
  pip install -e .
91
94
  pytest
95
+
96
+ ```
97
+
98
+ ## Installation
99
+ ```
100
+ pip install tnfr
92
101
  ```
@@ -0,0 +1,24 @@
1
+ tnfr/__init__.py,sha256=nN8P2Xm26kvJvqDhmRITnimew3SgBgw4fSUHPOAiYts,1880
2
+ tnfr/cli.py,sha256=fdzmIOVerg6J1j6cBuu7m2-nKFG_WqiNu-e2Wg8eECQ,5980
3
+ tnfr/constants.py,sha256=PUhps2gBXeh_rJIrCF9HpWRdi02sfRXvVp066AOgkyQ,8912
4
+ tnfr/dynamics.py,sha256=FeFv7h4m9e8qhbU9tIRss3K6P2uz4Hfmtbldf2AhfyQ,25286
5
+ tnfr/gamma.py,sha256=1hGqYTBYIYirgmSSSbvSuFWPVy_KGzPNH3eD7wmRUu0,2689
6
+ tnfr/grammar.py,sha256=X50-BzTzaYkFIi-ljcgB_XXC6c2srvqzDqOKa91nsuA,5090
7
+ tnfr/helpers.py,sha256=foJgwyC__6lQeOpPY02slFRggY3tCy1CxeFlEQcI-gY,7014
8
+ tnfr/main.py,sha256=XqjI1YEdF-OqRzTMa5dYIxCig4qyAR-l1FPcyxpC8WY,1926
9
+ tnfr/metrics.py,sha256=XusywHo1-GgLqYy1Jz43Xc4zRsggrn_DwoaFWfAy82M,6640
10
+ tnfr/observers.py,sha256=PTw3hxk7KD-Yx_CvCIU09icuhyYD6uNU6SvF80UvP-Y,5354
11
+ tnfr/ontosim.py,sha256=9GfEtiLIdJOPJUTufcq_MssAA9J8AfChHU6HKb3DIJY,5628
12
+ tnfr/operators.py,sha256=Q2QvtK2oQUuMsq_CPdfaK9eCrhniI1NaWEfwQH08zSk,12936
13
+ tnfr/presets.py,sha256=vaqgqz1pVU93hEOITQn-2CUGfMbN2n47A9GBFmjWRpo,661
14
+ tnfr/program.py,sha256=qGqj74qBqXILm-Yrw73__0uIlx2gJiSF9dDHxOGTbCs,5798
15
+ tnfr/scenarios.py,sha256=FgsXzA6ENRkalENnG5ZuBNFguEGaKZTM6y0quW5fJI4,808
16
+ tnfr/sense.py,sha256=1yVlphcs4o_uOQ51MDqW9dphydMiDNKv1Z1EdgbJgpU,6778
17
+ tnfr/trace.py,sha256=YtqSKJM2tCH5eLa22nObPLnYgsn4Bm6inipY0n_mwXY,4887
18
+ tnfr/types.py,sha256=xyFHp0PptEqPNUekAFH6DcAnyMx4bCQutMyFUXMd2sA,457
19
+ tnfr-4.0.0.dist-info/licenses/LICENSE.md,sha256=SRvvhXLrKtseuK6DARbuJffuXOXqAyk3wvF2n0t1SWA,1109
20
+ tnfr-4.0.0.dist-info/METADATA,sha256=xzcC2N34E82NmkgYly3jpfgzLVgI9fQLNnMiXLWk2RQ,5898
21
+ tnfr-4.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
+ tnfr-4.0.0.dist-info/entry_points.txt,sha256=j4-QRHqeT2WnchHe_mvK7npGTLjlyfLpvRONFe9Z4MU,39
23
+ tnfr-4.0.0.dist-info/top_level.txt,sha256=Q2HJnvc5Rt2VHwVvyBTnNPT4SfmJWnCj7XUxxEvQa7c,5
24
+ tnfr-4.0.0.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tnfr = tnfr.cli:main
@@ -1,14 +0,0 @@
1
- tnfr/__init__.py,sha256=LYIpDLjPnC4MxrviM2-Lg8KudbzXM_56aepRg_BEJG4,581
2
- tnfr/constants.py,sha256=_775sPHussR9vgkWRCLC6dzwgk_1_lLnSlWT8sBWR3U,7677
3
- tnfr/dynamics.py,sha256=btDyC3jIOmND1c1qqA2RDo0f3NraJKZwggO5BqBAmCA,23967
4
- tnfr/helpers.py,sha256=3DT7nb2FBrJxSEFcZ_Q9qiR9JGbjCNJOzA49bzN6hbM,6067
5
- tnfr/main.py,sha256=TEngteuC9MD7Ec9bNGuCC9ym-2ohbh202-HGArCR4tk,1506
6
- tnfr/observers.py,sha256=PTw3hxk7KD-Yx_CvCIU09icuhyYD6uNU6SvF80UvP-Y,5354
7
- tnfr/ontosim.py,sha256=9GfEtiLIdJOPJUTufcq_MssAA9J8AfChHU6HKb3DIJY,5628
8
- tnfr/operators.py,sha256=ONUC0tzB7y0Ad4MppLjFc8bmlTjPol-l_xdEB_84uaw,12293
9
- tnfr-3.5.0.dist-info/licenses/LICENSE.md,sha256=SRvvhXLrKtseuK6DARbuJffuXOXqAyk3wvF2n0t1SWA,1109
10
- tnfr-3.5.0.dist-info/METADATA,sha256=N420pHD6T9d8kkDwi202Klyh7mu7oIx3-CmwYZo_CwA,5576
11
- tnfr-3.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- tnfr-3.5.0.dist-info/entry_points.txt,sha256=6ZYrYA3Z-kKvqiYddIAj9c8ZcoVlK7pj-qCBvlNtuLE,40
13
- tnfr-3.5.0.dist-info/top_level.txt,sha256=Q2HJnvc5Rt2VHwVvyBTnNPT4SfmJWnCj7XUxxEvQa7c,5
14
- tnfr-3.5.0.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- tnfr = tnfr.main:main
File without changes