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