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
@@ -0,0 +1,343 @@
1
+ """Phase coordination helpers for TNFR dynamics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections import deque
7
+ from collections.abc import Mapping, MutableMapping, Sequence
8
+ from concurrent.futures import ProcessPoolExecutor
9
+ from typing import TYPE_CHECKING, Any, TypeVar, cast
10
+
11
+ from ..alias import get_theta_attr, set_theta
12
+ from ..constants import (
13
+ DEFAULTS,
14
+ METRIC_DEFAULTS,
15
+ STATE_DISSONANT,
16
+ STATE_STABLE,
17
+ STATE_TRANSITION,
18
+ normalise_state_token,
19
+ )
20
+ from ..glyph_history import append_metric
21
+ from ..helpers.numeric import angle_diff
22
+ from ..metrics.common import ensure_neighbors_map
23
+ from ..metrics.trig import neighbor_phase_mean_list
24
+ from ..metrics.trig_cache import get_trig_cache
25
+ from ..observers import DEFAULT_GLYPH_LOAD_SPAN, glyph_load, kuramoto_order
26
+ from ..types import NodeId, Phase, TNFRGraph
27
+ from ..utils import get_numpy
28
+ from .._compat import TypeAlias
29
+
30
+ if TYPE_CHECKING: # pragma: no cover - typing imports only
31
+ try:
32
+ import numpy as np_typing
33
+ import numpy.typing as npt
34
+ except ImportError: # pragma: no cover - optional typing dependency
35
+ FloatArray: TypeAlias = Any
36
+ else:
37
+ FloatArray: TypeAlias = npt.NDArray[np_typing.float_]
38
+ else: # pragma: no cover - runtime without numpy typing
39
+ FloatArray: TypeAlias = Any
40
+
41
+ _DequeT = TypeVar("_DequeT")
42
+
43
+ ChunkArgs = tuple[
44
+ Sequence[NodeId],
45
+ Mapping[NodeId, Phase],
46
+ Mapping[NodeId, float],
47
+ Mapping[NodeId, float],
48
+ Mapping[NodeId, Sequence[NodeId]],
49
+ float,
50
+ float,
51
+ float,
52
+ ]
53
+
54
+ __all__ = ("coordinate_global_local_phase",)
55
+
56
+
57
+ def _ensure_hist_deque(
58
+ hist: MutableMapping[str, Any], key: str, maxlen: int
59
+ ) -> deque[_DequeT]:
60
+ """Ensure history entry ``key`` is a deque with ``maxlen``."""
61
+
62
+ dq = hist.setdefault(key, deque(maxlen=maxlen))
63
+ if not isinstance(dq, deque):
64
+ dq = deque(dq, maxlen=maxlen)
65
+ hist[key] = dq
66
+ return cast("deque[_DequeT]", dq)
67
+
68
+
69
+ def _read_adaptive_params(
70
+ g: Mapping[str, Any],
71
+ ) -> tuple[Mapping[str, Any], float, float]:
72
+ """Obtain configuration and current values for phase adaptation."""
73
+
74
+ cfg = g.get("PHASE_ADAPT", DEFAULTS.get("PHASE_ADAPT", {}))
75
+ kG = float(g.get("PHASE_K_GLOBAL", DEFAULTS["PHASE_K_GLOBAL"]))
76
+ kL = float(g.get("PHASE_K_LOCAL", DEFAULTS["PHASE_K_LOCAL"]))
77
+ return cast(Mapping[str, Any], cfg), kG, kL
78
+
79
+
80
+ def _compute_state(G: TNFRGraph, cfg: Mapping[str, Any]) -> tuple[str, float, float]:
81
+ """Return the canonical network state and supporting metrics."""
82
+
83
+ R = kuramoto_order(G)
84
+ dist = glyph_load(G, window=DEFAULT_GLYPH_LOAD_SPAN)
85
+ disr = float(dist.get("_disruptors", 0.0)) if dist else 0.0
86
+
87
+ R_hi = float(cfg.get("R_hi", 0.90))
88
+ R_lo = float(cfg.get("R_lo", 0.60))
89
+ disr_hi = float(cfg.get("disr_hi", 0.50))
90
+ disr_lo = float(cfg.get("disr_lo", 0.25))
91
+ if (R >= R_hi) and (disr <= disr_lo):
92
+ state = STATE_STABLE
93
+ elif (R <= R_lo) or (disr >= disr_hi):
94
+ state = STATE_DISSONANT
95
+ else:
96
+ state = STATE_TRANSITION
97
+ return state, float(R), disr
98
+
99
+
100
+ def _smooth_adjust_k(
101
+ kG: float, kL: float, state: str, cfg: Mapping[str, Any]
102
+ ) -> tuple[float, float]:
103
+ """Smoothly update kG/kL toward targets according to state."""
104
+
105
+ kG_min = float(cfg.get("kG_min", 0.01))
106
+ kG_max = float(cfg.get("kG_max", 0.20))
107
+ kL_min = float(cfg.get("kL_min", 0.05))
108
+ kL_max = float(cfg.get("kL_max", 0.25))
109
+
110
+ state = normalise_state_token(state)
111
+
112
+ if state == STATE_DISSONANT:
113
+ kG_t = kG_max
114
+ kL_t = 0.5 * (
115
+ kL_min + kL_max
116
+ ) # local medio para no perder plasticidad
117
+ elif state == STATE_STABLE:
118
+ kG_t = kG_min
119
+ kL_t = kL_min
120
+ else:
121
+ kG_t = 0.5 * (kG_min + kG_max)
122
+ kL_t = 0.5 * (kL_min + kL_max)
123
+
124
+ up = float(cfg.get("up", 0.10))
125
+ down = float(cfg.get("down", 0.07))
126
+
127
+ def _step(curr: float, target: float, mn: float, mx: float) -> float:
128
+ gain = up if target > curr else down
129
+ nxt = curr + gain * (target - curr)
130
+ return max(mn, min(mx, nxt))
131
+
132
+ return _step(kG, kG_t, kG_min, kG_max), _step(kL, kL_t, kL_min, kL_max)
133
+
134
+
135
+ def _phase_adjust_chunk(args: ChunkArgs) -> list[tuple[NodeId, Phase]]:
136
+ """Return coordinated phase updates for the provided chunk."""
137
+
138
+ (
139
+ nodes,
140
+ theta_map,
141
+ cos_map,
142
+ sin_map,
143
+ neighbors_map,
144
+ thG,
145
+ kG,
146
+ kL,
147
+ ) = args
148
+ updates: list[tuple[NodeId, Phase]] = []
149
+ for node in nodes:
150
+ th = float(theta_map.get(node, 0.0))
151
+ neigh = neighbors_map.get(node, ())
152
+ if neigh:
153
+ thL = neighbor_phase_mean_list(
154
+ neigh,
155
+ cos_map,
156
+ sin_map,
157
+ np=None,
158
+ fallback=th,
159
+ )
160
+ else:
161
+ thL = th
162
+ dG = angle_diff(thG, th)
163
+ dL = angle_diff(thL, th)
164
+ updates.append((node, cast(Phase, th + kG * dG + kL * dL)))
165
+ return updates
166
+
167
+
168
+ def coordinate_global_local_phase(
169
+ G: TNFRGraph,
170
+ global_force: float | None = None,
171
+ local_force: float | None = None,
172
+ *,
173
+ n_jobs: int | None = None,
174
+ ) -> None:
175
+ """Coordinate phase using a blend of global and neighbour coupling."""
176
+
177
+ g = cast(dict[str, Any], G.graph)
178
+ hist = cast(dict[str, Any], g.setdefault("history", {}))
179
+ maxlen = int(
180
+ g.get("PHASE_HISTORY_MAXLEN", METRIC_DEFAULTS["PHASE_HISTORY_MAXLEN"])
181
+ )
182
+ hist_state = cast(deque[str], _ensure_hist_deque(hist, "phase_state", maxlen))
183
+ if hist_state:
184
+ normalised_states = [normalise_state_token(item) for item in hist_state]
185
+ if normalised_states != list(hist_state):
186
+ hist_state.clear()
187
+ hist_state.extend(normalised_states)
188
+ hist_R = cast(deque[float], _ensure_hist_deque(hist, "phase_R", maxlen))
189
+ hist_disr = cast(deque[float], _ensure_hist_deque(hist, "phase_disr", maxlen))
190
+
191
+ if (global_force is not None) or (local_force is not None):
192
+ kG = float(
193
+ global_force
194
+ if global_force is not None
195
+ else g.get("PHASE_K_GLOBAL", DEFAULTS["PHASE_K_GLOBAL"])
196
+ )
197
+ kL = float(
198
+ local_force
199
+ if local_force is not None
200
+ else g.get("PHASE_K_LOCAL", DEFAULTS["PHASE_K_LOCAL"])
201
+ )
202
+ else:
203
+ cfg, kG, kL = _read_adaptive_params(g)
204
+
205
+ if bool(cfg.get("enabled", False)):
206
+ state, R, disr = _compute_state(G, cfg)
207
+ kG, kL = _smooth_adjust_k(kG, kL, state, cfg)
208
+
209
+ hist_state.append(state)
210
+ hist_R.append(float(R))
211
+ hist_disr.append(float(disr))
212
+
213
+ g["PHASE_K_GLOBAL"] = kG
214
+ g["PHASE_K_LOCAL"] = kL
215
+ append_metric(hist, "phase_kG", float(kG))
216
+ append_metric(hist, "phase_kL", float(kL))
217
+
218
+ jobs: int | None
219
+ try:
220
+ jobs = None if n_jobs is None else int(n_jobs)
221
+ except (TypeError, ValueError):
222
+ jobs = None
223
+ if jobs is not None and jobs <= 1:
224
+ jobs = None
225
+
226
+ np = get_numpy()
227
+ if np is not None:
228
+ jobs = None
229
+
230
+ nodes: list[NodeId] = [cast(NodeId, node) for node in G.nodes()]
231
+ num_nodes = len(nodes)
232
+ if not num_nodes:
233
+ return
234
+
235
+ trig = get_trig_cache(G, np=np)
236
+ theta_map = cast(dict[NodeId, Phase], trig.theta)
237
+ cos_map = cast(dict[NodeId, float], trig.cos)
238
+ sin_map = cast(dict[NodeId, float], trig.sin)
239
+
240
+ neighbors_proxy = ensure_neighbors_map(G)
241
+ neighbors_map: dict[NodeId, tuple[NodeId, ...]] = {}
242
+ for n in nodes:
243
+ try:
244
+ neighbors_map[n] = tuple(cast(Sequence[NodeId], neighbors_proxy[n]))
245
+ except KeyError:
246
+ neighbors_map[n] = ()
247
+
248
+ def _theta_value(node: NodeId) -> float:
249
+ cached = theta_map.get(node)
250
+ if cached is not None:
251
+ return float(cached)
252
+ attr_val = get_theta_attr(G.nodes[node], 0.0)
253
+ return float(attr_val if attr_val is not None else 0.0)
254
+
255
+ theta_vals = [_theta_value(n) for n in nodes]
256
+ cos_vals = [
257
+ float(cos_map.get(n, math.cos(theta_vals[idx])))
258
+ for idx, n in enumerate(nodes)
259
+ ]
260
+ sin_vals = [
261
+ float(sin_map.get(n, math.sin(theta_vals[idx])))
262
+ for idx, n in enumerate(nodes)
263
+ ]
264
+
265
+ if np is not None:
266
+ theta_arr = cast(FloatArray, np.fromiter(theta_vals, dtype=float))
267
+ cos_arr = cast(FloatArray, np.fromiter(cos_vals, dtype=float))
268
+ sin_arr = cast(FloatArray, np.fromiter(sin_vals, dtype=float))
269
+ if cos_arr.size:
270
+ mean_cos = float(np.mean(cos_arr))
271
+ mean_sin = float(np.mean(sin_arr))
272
+ thG = float(np.arctan2(mean_sin, mean_cos))
273
+ else:
274
+ thG = 0.0
275
+ neighbor_means = [
276
+ neighbor_phase_mean_list(
277
+ neighbors_map.get(n, ()),
278
+ cos_map,
279
+ sin_map,
280
+ np=np,
281
+ fallback=theta_vals[idx],
282
+ )
283
+ for idx, n in enumerate(nodes)
284
+ ]
285
+ neighbor_arr = cast(FloatArray, np.fromiter(neighbor_means, dtype=float))
286
+ theta_updates = theta_arr + kG * (thG - theta_arr) + kL * (
287
+ neighbor_arr - theta_arr
288
+ )
289
+ for idx, node in enumerate(nodes):
290
+ set_theta(G, node, float(theta_updates[int(idx)]))
291
+ return
292
+
293
+ mean_cos = math.fsum(cos_vals) / num_nodes
294
+ mean_sin = math.fsum(sin_vals) / num_nodes
295
+ thG = math.atan2(mean_sin, mean_cos)
296
+
297
+ if jobs is None:
298
+ for node in nodes:
299
+ th = float(theta_map.get(node, 0.0))
300
+ neigh = neighbors_map.get(node, ())
301
+ if neigh:
302
+ thL = neighbor_phase_mean_list(
303
+ neigh,
304
+ cos_map,
305
+ sin_map,
306
+ np=None,
307
+ fallback=th,
308
+ )
309
+ else:
310
+ thL = th
311
+ dG = angle_diff(thG, th)
312
+ dL = angle_diff(thL, th)
313
+ set_theta(G, node, float(th + kG * dG + kL * dL))
314
+ return
315
+
316
+ chunk_size = max(1, math.ceil(len(nodes) / jobs))
317
+ chunks = [
318
+ nodes[idx : idx + chunk_size]
319
+ for idx in range(0, len(nodes), chunk_size)
320
+ ]
321
+ args: list[ChunkArgs] = [
322
+ (
323
+ chunk,
324
+ theta_map,
325
+ cos_map,
326
+ sin_map,
327
+ neighbors_map,
328
+ thG,
329
+ kG,
330
+ kL,
331
+ )
332
+ for chunk in chunks
333
+ ]
334
+ results: dict[NodeId, Phase] = {}
335
+ with ProcessPoolExecutor(max_workers=jobs) as executor:
336
+ for res in executor.map(_phase_adjust_chunk, args):
337
+ for node, value in res:
338
+ results[node] = value
339
+ for node in nodes:
340
+ new_theta = results.get(node)
341
+ base_theta = theta_map.get(node, 0.0)
342
+ set_theta(G, node, float(new_theta if new_theta is not None else base_theta))
343
+