tnfr 4.5.0__py3-none-any.whl → 4.5.2__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.

Files changed (78) hide show
  1. tnfr/__init__.py +91 -89
  2. tnfr/alias.py +546 -0
  3. tnfr/cache.py +578 -0
  4. tnfr/callback_utils.py +388 -0
  5. tnfr/cli/__init__.py +75 -0
  6. tnfr/cli/arguments.py +177 -0
  7. tnfr/cli/execution.py +288 -0
  8. tnfr/cli/utils.py +36 -0
  9. tnfr/collections_utils.py +300 -0
  10. tnfr/config.py +19 -28
  11. tnfr/constants/__init__.py +174 -0
  12. tnfr/constants/core.py +159 -0
  13. tnfr/constants/init.py +31 -0
  14. tnfr/constants/metric.py +110 -0
  15. tnfr/constants_glyphs.py +98 -0
  16. tnfr/dynamics/__init__.py +658 -0
  17. tnfr/dynamics/dnfr.py +733 -0
  18. tnfr/dynamics/integrators.py +267 -0
  19. tnfr/dynamics/sampling.py +31 -0
  20. tnfr/execution.py +201 -0
  21. tnfr/flatten.py +283 -0
  22. tnfr/gamma.py +302 -88
  23. tnfr/glyph_history.py +290 -0
  24. tnfr/grammar.py +285 -96
  25. tnfr/graph_utils.py +84 -0
  26. tnfr/helpers/__init__.py +71 -0
  27. tnfr/helpers/numeric.py +87 -0
  28. tnfr/immutable.py +178 -0
  29. tnfr/import_utils.py +228 -0
  30. tnfr/initialization.py +197 -0
  31. tnfr/io.py +246 -0
  32. tnfr/json_utils.py +162 -0
  33. tnfr/locking.py +37 -0
  34. tnfr/logging_utils.py +116 -0
  35. tnfr/metrics/__init__.py +41 -0
  36. tnfr/metrics/coherence.py +829 -0
  37. tnfr/metrics/common.py +151 -0
  38. tnfr/metrics/core.py +101 -0
  39. tnfr/metrics/diagnosis.py +234 -0
  40. tnfr/metrics/export.py +137 -0
  41. tnfr/metrics/glyph_timing.py +189 -0
  42. tnfr/metrics/reporting.py +148 -0
  43. tnfr/metrics/sense_index.py +120 -0
  44. tnfr/metrics/trig.py +181 -0
  45. tnfr/metrics/trig_cache.py +109 -0
  46. tnfr/node.py +214 -159
  47. tnfr/observers.py +126 -128
  48. tnfr/ontosim.py +134 -134
  49. tnfr/operators/__init__.py +420 -0
  50. tnfr/operators/jitter.py +203 -0
  51. tnfr/operators/remesh.py +485 -0
  52. tnfr/presets.py +46 -14
  53. tnfr/rng.py +254 -0
  54. tnfr/selector.py +210 -0
  55. tnfr/sense.py +284 -131
  56. tnfr/structural.py +207 -79
  57. tnfr/tokens.py +60 -0
  58. tnfr/trace.py +329 -94
  59. tnfr/types.py +43 -17
  60. tnfr/validators.py +70 -24
  61. tnfr/value_utils.py +59 -0
  62. tnfr-4.5.2.dist-info/METADATA +379 -0
  63. tnfr-4.5.2.dist-info/RECORD +67 -0
  64. tnfr/cli.py +0 -322
  65. tnfr/constants.py +0 -277
  66. tnfr/dynamics.py +0 -814
  67. tnfr/helpers.py +0 -264
  68. tnfr/main.py +0 -47
  69. tnfr/metrics.py +0 -597
  70. tnfr/operators.py +0 -525
  71. tnfr/program.py +0 -176
  72. tnfr/scenarios.py +0 -34
  73. tnfr-4.5.0.dist-info/METADATA +0 -109
  74. tnfr-4.5.0.dist-info/RECORD +0 -28
  75. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/WHEEL +0 -0
  76. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/entry_points.txt +0 -0
  77. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/licenses/LICENSE.md +0 -0
  78. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/top_level.txt +0 -0
tnfr/gamma.py CHANGED
@@ -1,44 +1,187 @@
1
- """gamma.py — TNFR canónica
1
+ """Gamma registry."""
2
2
 
3
- Γi(R): acoplamientos de red para la ecuación nodal extendida
4
- ∂EPI/∂t = νf · ΔNFR(t) + Γi(R)
3
+ from __future__ import annotations
4
+ from typing import Any, Callable, NamedTuple
5
+ import math
6
+ import logging
7
+ import hashlib
8
+ from collections.abc import Mapping
9
+ from functools import lru_cache
10
+ from types import MappingProxyType
5
11
 
6
- `Γ` suma un término de acoplamiento dependiente del orden global de fase
7
- `R`. La especificación se toma de ``G.graph['GAMMA']`` (ver
8
- ``DEFAULTS['GAMMA']``) con parámetros como:
12
+ from .constants import DEFAULTS, get_aliases
13
+ from .alias import get_attr
14
+ from .graph_utils import get_graph_mapping
15
+ from .cache import edge_version_cache, node_set_checksum
16
+ from .json_utils import json_dumps
17
+ from .logging_utils import get_logger
18
+ from .metrics.trig_cache import get_trig_cache
9
19
 
10
- * ``type`` – modo de acoplamiento (``none``, ``kuramoto_linear``,
11
- ``kuramoto_bandpass``)
12
- * ``beta`` – ganancia del acoplamiento
13
- * ``R0`` – umbral de activación (solo lineal)
20
+ ALIAS_THETA = get_aliases("THETA")
14
21
 
15
- Provee:
16
- - kuramoto_R_psi(G): (R, ψ) orden de Kuramoto en la red
17
- - GAMMA_REGISTRY: registro de acoplamientos canónicos
18
- - eval_gamma(G, node, t): evalúa Γ para cada nodo según la config
19
- """
20
- from __future__ import annotations
21
- from typing import Dict, Any, Tuple
22
- import math
23
- import cmath
24
22
 
25
- from .constants import ALIAS_THETA
26
- from .helpers import _get_attr
23
+ logger = get_logger(__name__)
24
+
25
+ DEFAULT_GAMMA: Mapping[str, Any] = MappingProxyType(dict(DEFAULTS["GAMMA"]))
26
+
27
+ __all__ = (
28
+ "kuramoto_R_psi",
29
+ "gamma_none",
30
+ "gamma_kuramoto_linear",
31
+ "gamma_kuramoto_bandpass",
32
+ "gamma_kuramoto_tanh",
33
+ "gamma_harmonic",
34
+ "GammaEntry",
35
+ "GAMMA_REGISTRY",
36
+ "eval_gamma",
37
+ )
38
+
39
+
40
+ @lru_cache(maxsize=1)
41
+ def _default_gamma_spec() -> tuple[bytes, str]:
42
+ dumped = json_dumps(dict(DEFAULT_GAMMA), sort_keys=True, to_bytes=True)
43
+ hash_ = hashlib.blake2b(dumped, digest_size=16).hexdigest()
44
+ return dumped, hash_
45
+
27
46
 
47
+ def _ensure_kuramoto_cache(G, t) -> None:
48
+ """Cache ``(R, ψ)`` for the current step ``t`` using
49
+ ``edge_version_cache``."""
50
+ checksum = G.graph.get("_dnfr_nodes_checksum")
51
+ if checksum is None:
52
+ # reuse checksum from cached_nodes_and_A when available
53
+ checksum = node_set_checksum(G)
54
+ nodes_sig = (len(G), checksum)
55
+ max_steps = int(G.graph.get("KURAMOTO_CACHE_STEPS", 1))
28
56
 
29
- def kuramoto_R_psi(G) -> Tuple[float, float]:
30
- """Devuelve (R, ψ) del orden de Kuramoto usando θ de todos los nodos."""
31
- acc = 0 + 0j
32
- n = 0
33
- for node in G.nodes():
34
- nd = G.nodes[node]
35
- th = _get_attr(nd, ALIAS_THETA, 0.0)
36
- acc += cmath.exp(1j * th)
37
- n += 1
57
+ def builder() -> dict[str, float]:
58
+ R, psi = kuramoto_R_psi(G)
59
+ return {"R": R, "psi": psi}
60
+
61
+ key = (t, nodes_sig)
62
+ entry = edge_version_cache(G, key, builder, max_entries=max_steps)
63
+ G.graph["_kuramoto_cache"] = entry
64
+
65
+
66
+ def kuramoto_R_psi(G) -> tuple[float, float]:
67
+ """Return ``(R, ψ)`` for Kuramoto order using θ from all nodes."""
68
+ max_steps = int(G.graph.get("KURAMOTO_CACHE_STEPS", 1))
69
+ trig = get_trig_cache(G, cache_size=max_steps)
70
+ n = len(trig.theta)
38
71
  if n == 0:
39
72
  return 0.0, 0.0
40
- z = acc / n
41
- return abs(z), math.atan2(z.imag, z.real)
73
+
74
+ cos_sum = sum(trig.cos.values())
75
+ sin_sum = sum(trig.sin.values())
76
+ R = math.hypot(cos_sum, sin_sum) / n
77
+ psi = math.atan2(sin_sum, cos_sum)
78
+ return R, psi
79
+
80
+
81
+ def _kuramoto_common(G, node, _cfg):
82
+ """Return ``(θ_i, R, ψ)`` for Kuramoto-based Γ functions.
83
+
84
+ Reads cached global order ``R`` and mean phase ``ψ`` and obtains node
85
+ phase ``θ_i``. ``_cfg`` is accepted only to keep a homogeneous signature
86
+ with Γ evaluators.
87
+ """
88
+ cache = G.graph.get("_kuramoto_cache", {})
89
+ R = float(cache.get("R", 0.0))
90
+ psi = float(cache.get("psi", 0.0))
91
+ th_i = get_attr(G.nodes[node], ALIAS_THETA, 0.0)
92
+ return th_i, R, psi
93
+
94
+
95
+ def _read_gamma_raw(G) -> Mapping[str, Any] | None:
96
+ """Return raw Γ specification from ``G.graph['GAMMA']``.
97
+
98
+ The returned value is the direct contents of ``G.graph['GAMMA']`` when
99
+ it is a mapping or the result of :func:`get_graph_mapping` if a path is
100
+ provided. Final validation and caching are handled elsewhere.
101
+ """
102
+
103
+ raw = G.graph.get("GAMMA")
104
+ if raw is None or isinstance(raw, Mapping):
105
+ return raw
106
+ return get_graph_mapping(
107
+ G, "GAMMA", "G.graph['GAMMA'] no es un mapeo; se usa {'type': 'none'}"
108
+ )
109
+
110
+
111
+ def _get_gamma_spec(G) -> Mapping[str, Any]:
112
+ """Return validated Γ specification caching results.
113
+
114
+ The raw value from ``G.graph['GAMMA']`` is cached together with the
115
+ normalized specification and its hash. When the raw value is unchanged,
116
+ the cached spec is returned without re-reading or re-validating,
117
+ preventing repeated warnings or costly hashing.
118
+ """
119
+
120
+ raw = G.graph.get("GAMMA")
121
+ cached_raw = G.graph.get("_gamma_raw")
122
+ cached_spec = G.graph.get("_gamma_spec")
123
+ cached_hash = G.graph.get("_gamma_spec_hash")
124
+
125
+ def _hash_mapping(mapping: Mapping[str, Any]) -> str:
126
+ dumped = json_dumps(mapping, sort_keys=True, to_bytes=True)
127
+ return hashlib.blake2b(dumped, digest_size=16).hexdigest()
128
+
129
+ mapping_hash: str | None = None
130
+ if isinstance(raw, Mapping):
131
+ mapping_hash = _hash_mapping(raw)
132
+ if (
133
+ raw is cached_raw
134
+ and cached_spec is not None
135
+ and cached_hash == mapping_hash
136
+ ):
137
+ return cached_spec
138
+ elif raw is cached_raw and cached_spec is not None and cached_hash is not None:
139
+ return cached_spec
140
+
141
+ if raw is None:
142
+ spec = DEFAULT_GAMMA
143
+ _, cur_hash = _default_gamma_spec()
144
+ elif isinstance(raw, Mapping):
145
+ spec = raw
146
+ cur_hash = mapping_hash if mapping_hash is not None else _hash_mapping(spec)
147
+ else:
148
+ spec_raw = _read_gamma_raw(G)
149
+ if isinstance(spec_raw, Mapping) and spec_raw is not None:
150
+ spec = spec_raw
151
+ cur_hash = _hash_mapping(spec)
152
+ else:
153
+ spec = DEFAULT_GAMMA
154
+ _, cur_hash = _default_gamma_spec()
155
+
156
+ # Store raw input, validated spec and its hash for future calls
157
+ G.graph["_gamma_raw"] = raw
158
+ G.graph["_gamma_spec"] = spec
159
+ G.graph["_gamma_spec_hash"] = cur_hash
160
+ return spec
161
+
162
+
163
+ # -----------------
164
+ # Helpers
165
+ # -----------------
166
+
167
+
168
+ def _gamma_params(
169
+ cfg: Mapping[str, Any], **defaults: float
170
+ ) -> tuple[float, ...]:
171
+ """Return normalized Γ parameters from ``cfg``.
172
+
173
+ Parameters are retrieved from ``cfg`` using the keys in ``defaults`` and
174
+ converted to ``float``. If a key is missing, its value from ``defaults`` is
175
+ used. Values convertible to ``float`` (e.g. strings) are accepted.
176
+
177
+ Example
178
+ -------
179
+ >>> beta, R0 = _gamma_params(cfg, beta=0.0, R0=0.0)
180
+ """
181
+
182
+ return tuple(
183
+ float(cfg.get(name, default)) for name, default in defaults.items()
184
+ )
42
185
 
43
186
 
44
187
  # -----------------
@@ -46,82 +189,153 @@ def kuramoto_R_psi(G) -> Tuple[float, float]:
46
189
  # -----------------
47
190
 
48
191
 
49
- def gamma_none(G, node, t, cfg: Dict[str, Any]) -> float:
192
+ def gamma_none(G, node, t, cfg: dict[str, Any]) -> float:
50
193
  return 0.0
51
194
 
52
195
 
53
- def gamma_kuramoto_linear(G, node, t, cfg: Dict[str, Any]) -> float:
54
- """Acoplamiento lineal de Kuramoto para Γi(R).
196
+ def _gamma_kuramoto(
197
+ G,
198
+ node,
199
+ cfg: Mapping[str, Any],
200
+ builder: Callable[..., float],
201
+ **defaults: float,
202
+ ) -> float:
203
+ """Helper for Kuramoto-based Γ functions.
55
204
 
56
- Fórmula: Γ = β · (R - R0) · cos(θ_i - ψ)
57
- - R [0,1] es el orden global de fase.
58
- - ψ es la fase media (dirección de coordinación).
59
- - β, R0 son parámetros (ganancia/umbral).
60
-
61
- Uso: refuerza integración cuando la red ya exhibe coherencia de fase (R>R0).
205
+ ``builder`` receives ``(θ_i, R, ψ, *params)`` where ``params`` are
206
+ extracted from ``cfg`` according to ``defaults``.
62
207
  """
63
- beta = float(cfg.get("beta", 0.0))
64
- R0 = float(cfg.get("R0", 0.0))
65
- R, psi = kuramoto_R_psi(G)
66
- th_i = _get_attr(G.nodes[node], ALIAS_THETA, 0.0)
208
+
209
+ params = _gamma_params(cfg, **defaults)
210
+ th_i, R, psi = _kuramoto_common(G, node, cfg)
211
+ return builder(th_i, R, psi, *params)
212
+
213
+
214
+ def _builder_linear(th_i: float, R: float, psi: float, beta: float, R0: float) -> float:
67
215
  return beta * (R - R0) * math.cos(th_i - psi)
68
216
 
69
217
 
70
- def gamma_kuramoto_bandpass(G, node, t, cfg: Dict[str, Any]) -> float:
71
- """Γ = β · R(1-R) · sign(cos(θ_i - ψ))"""
72
- beta = float(cfg.get("beta", 0.0))
73
- R, psi = kuramoto_R_psi(G)
74
- th_i = _get_attr(G.nodes[node], ALIAS_THETA, 0.0)
218
+ def _builder_bandpass(th_i: float, R: float, psi: float, beta: float) -> float:
75
219
  sgn = 1.0 if math.cos(th_i - psi) >= 0.0 else -1.0
76
220
  return beta * R * (1.0 - R) * sgn
77
221
 
78
222
 
79
- def gamma_kuramoto_tanh(G, node, t, cfg: Dict[str, Any]) -> float:
80
- """Acoplamiento saturante tipo tanh para Γi(R).
223
+ def _builder_tanh(th_i: float, R: float, psi: float, beta: float, k: float, R0: float) -> float:
224
+ return beta * math.tanh(k * (R - R0)) * math.cos(th_i - psi)
225
+
226
+
227
+ def gamma_kuramoto_linear(G, node, t, cfg: dict[str, Any]) -> float:
228
+ """Linear Kuramoto coupling for Γi(R).
81
229
 
82
- Fórmula: Γ = β · tanh(k·(R - R0)) · cos(θ_i - ψ)
83
- - β: ganancia del acoplamiento
84
- - k: pendiente de la tanh (cuán rápido satura)
85
- - R0: umbral de activación
230
+ Formula: Γ = β · (R - R0) · cos(θ_i - ψ)
231
+ - R [0,1] is the global phase order.
232
+ - ψ is the mean phase (coordination direction).
233
+ - β, R0 are parameters (gain/threshold).
234
+
235
+ Use: reinforces integration when the network already shows phase
236
+ coherence (R>R0).
86
237
  """
87
- beta = float(cfg.get("beta", 0.0))
88
- k = float(cfg.get("k", 1.0))
89
- R0 = float(cfg.get("R0", 0.0))
90
- R, psi = kuramoto_R_psi(G)
91
- th_i = _get_attr(G.nodes[node], ALIAS_THETA, 0.0)
92
- return beta * math.tanh(k * (R - R0)) * math.cos(th_i - psi)
93
238
 
239
+ return _gamma_kuramoto(G, node, cfg, _builder_linear, beta=0.0, R0=0.0)
94
240
 
95
- def gamma_harmonic(G, node, t, cfg: Dict[str, Any]) -> float:
96
- """Forzamiento armónico coherente con el campo global de fase.
97
241
 
98
- Fórmula: Γ = β · sin(ω·t + φ) · cos(θ_i - ψ)
99
- - β: ganancia del acoplamiento
100
- - ω: frecuencia angular del forzante
101
- - φ: fase inicial del forzante
242
+ def gamma_kuramoto_bandpass(G, node, t, cfg: dict[str, Any]) -> float:
243
+ """Γ = β · R(1-R) · sign(cos(θ_i - ψ))"""
244
+
245
+ return _gamma_kuramoto(G, node, cfg, _builder_bandpass, beta=0.0)
246
+
247
+
248
+ def gamma_kuramoto_tanh(G, node, t, cfg: dict[str, Any]) -> float:
249
+ """Saturating tanh coupling for Γi(R).
250
+
251
+ Formula: Γ = β · tanh(k·(R - R0)) · cos(θ_i - ψ)
252
+ - β: coupling gain
253
+ - k: tanh slope (how fast it saturates)
254
+ - R0: activation threshold
102
255
  """
103
- beta = float(cfg.get("beta", 0.0))
104
- omega = float(cfg.get("omega", 1.0))
105
- phi = float(cfg.get("phi", 0.0))
106
- R, psi = kuramoto_R_psi(G)
107
- th = _get_attr(G.nodes[node], ALIAS_THETA, 0.0)
108
- return beta * math.sin(omega * t + phi) * math.cos(th - psi)
109
-
110
-
111
- GAMMA_REGISTRY = {
112
- "none": gamma_none,
113
- "kuramoto_linear": gamma_kuramoto_linear,
114
- "kuramoto_bandpass": gamma_kuramoto_bandpass,
115
- "kuramoto_tanh": gamma_kuramoto_tanh,
116
- "harmonic": gamma_harmonic,
256
+
257
+ return _gamma_kuramoto(G, node, cfg, _builder_tanh, beta=0.0, k=1.0, R0=0.0)
258
+
259
+
260
+ def gamma_harmonic(G, node, t, cfg: dict[str, Any]) -> float:
261
+ """Harmonic forcing aligned with the global phase field.
262
+
263
+ Formula: Γ = β · sin(ω·t + φ) · cos(θ_i - ψ)
264
+ - β: coupling gain
265
+ - ω: angular frequency of the forcing
266
+ - φ: initial phase of the forcing
267
+ """
268
+ beta, omega, phi = _gamma_params(cfg, beta=0.0, omega=1.0, phi=0.0)
269
+ th_i, _, psi = _kuramoto_common(G, node, cfg)
270
+ return beta * math.sin(omega * t + phi) * math.cos(th_i - psi)
271
+
272
+
273
+ class GammaEntry(NamedTuple):
274
+ fn: Callable[[Any, Any, Any, dict[str, Any]], float]
275
+ needs_kuramoto: bool
276
+
277
+
278
+ # ``GAMMA_REGISTRY`` asocia el nombre del acoplamiento con un
279
+ # ``GammaEntry`` donde ``fn`` es la función evaluadora y
280
+ # ``needs_kuramoto`` indica si requiere precomputar el orden global de fase.
281
+ GAMMA_REGISTRY: dict[str, GammaEntry] = {
282
+ "none": GammaEntry(gamma_none, False),
283
+ "kuramoto_linear": GammaEntry(gamma_kuramoto_linear, True),
284
+ "kuramoto_bandpass": GammaEntry(gamma_kuramoto_bandpass, True),
285
+ "kuramoto_tanh": GammaEntry(gamma_kuramoto_tanh, True),
286
+ "harmonic": GammaEntry(gamma_harmonic, True),
117
287
  }
118
288
 
119
289
 
120
- def eval_gamma(G, node, t) -> float:
121
- """Evalúa Γi para `node` según la especificación en G.graph['GAMMA']."""
122
- spec = G.graph.get("GAMMA", {"type": "none"})
123
- fn = GAMMA_REGISTRY.get(spec.get("type", "none"), gamma_none)
290
+ def eval_gamma(
291
+ G,
292
+ node,
293
+ t,
294
+ *,
295
+ strict: bool = False,
296
+ log_level: int | None = None,
297
+ ) -> float:
298
+ """Evaluate Γi for ``node`` according to ``G.graph['GAMMA']``
299
+ specification.
300
+
301
+ If ``strict`` is ``True`` exceptions raised during evaluation are
302
+ propagated instead of returning ``0.0``. Likewise, if the specified
303
+ Γ type is not registered a warning is emitted (o ``ValueError`` en
304
+ modo estricto) y se usa ``gamma_none``.
305
+
306
+ ``log_level`` controls the logging level for captured errors when
307
+ ``strict`` is ``False``. If omitted, ``logging.ERROR`` is used in
308
+ strict mode and ``logging.DEBUG`` otherwise.
309
+ """
310
+ spec = _get_gamma_spec(G)
311
+ spec_type = spec.get("type", "none")
312
+ reg_entry = GAMMA_REGISTRY.get(spec_type)
313
+ if reg_entry is None:
314
+ msg = f"Tipo GAMMA desconocido: {spec_type}"
315
+ if strict:
316
+ raise ValueError(msg)
317
+ logger.warning(msg)
318
+ entry = GammaEntry(gamma_none, False)
319
+ else:
320
+ entry = reg_entry
321
+ if entry.needs_kuramoto:
322
+ _ensure_kuramoto_cache(G, t)
124
323
  try:
125
- return float(fn(G, node, t, spec))
126
- except Exception:
324
+ return float(entry.fn(G, node, t, spec))
325
+ except (ValueError, TypeError, ArithmeticError) as exc:
326
+ level = (
327
+ log_level
328
+ if log_level is not None
329
+ else (logging.ERROR if strict else logging.DEBUG)
330
+ )
331
+ logger.log(
332
+ level,
333
+ "Fallo al evaluar Γi para nodo %s en t=%s: %s: %s",
334
+ node,
335
+ t,
336
+ exc.__class__.__name__,
337
+ exc,
338
+ )
339
+ if strict:
340
+ raise
127
341
  return 0.0