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

Files changed (365) hide show
  1. tnfr/__init__.py +334 -50
  2. tnfr/__init__.pyi +33 -0
  3. tnfr/_compat.py +10 -0
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +49 -0
  6. tnfr/_version.pyi +7 -0
  7. tnfr/alias.py +214 -37
  8. tnfr/alias.pyi +108 -0
  9. tnfr/backends/__init__.py +354 -0
  10. tnfr/backends/jax_backend.py +173 -0
  11. tnfr/backends/numpy_backend.py +238 -0
  12. tnfr/backends/optimized_numpy.py +420 -0
  13. tnfr/backends/torch_backend.py +408 -0
  14. tnfr/cache.py +149 -556
  15. tnfr/cache.pyi +13 -0
  16. tnfr/cli/__init__.py +51 -16
  17. tnfr/cli/__init__.pyi +26 -0
  18. tnfr/cli/arguments.py +344 -32
  19. tnfr/cli/arguments.pyi +29 -0
  20. tnfr/cli/execution.py +676 -50
  21. tnfr/cli/execution.pyi +70 -0
  22. tnfr/cli/interactive_validator.py +614 -0
  23. tnfr/cli/utils.py +18 -3
  24. tnfr/cli/utils.pyi +7 -0
  25. tnfr/cli/validate.py +236 -0
  26. tnfr/compat/__init__.py +85 -0
  27. tnfr/compat/dataclass.py +136 -0
  28. tnfr/compat/jsonschema_stub.py +61 -0
  29. tnfr/compat/matplotlib_stub.py +73 -0
  30. tnfr/compat/numpy_stub.py +155 -0
  31. tnfr/config/__init__.py +224 -0
  32. tnfr/config/__init__.pyi +10 -0
  33. tnfr/{constants_glyphs.py → config/constants.py} +26 -20
  34. tnfr/config/constants.pyi +12 -0
  35. tnfr/config/defaults.py +54 -0
  36. tnfr/{constants/core.py → config/defaults_core.py} +59 -6
  37. tnfr/config/defaults_init.py +33 -0
  38. tnfr/config/defaults_metric.py +104 -0
  39. tnfr/config/feature_flags.py +81 -0
  40. tnfr/config/feature_flags.pyi +16 -0
  41. tnfr/config/glyph_constants.py +31 -0
  42. tnfr/config/init.py +77 -0
  43. tnfr/config/init.pyi +8 -0
  44. tnfr/config/operator_names.py +254 -0
  45. tnfr/config/operator_names.pyi +36 -0
  46. tnfr/config/physics_derivation.py +354 -0
  47. tnfr/config/presets.py +83 -0
  48. tnfr/config/presets.pyi +7 -0
  49. tnfr/config/security.py +927 -0
  50. tnfr/config/thresholds.py +114 -0
  51. tnfr/config/tnfr_config.py +498 -0
  52. tnfr/constants/__init__.py +51 -133
  53. tnfr/constants/__init__.pyi +92 -0
  54. tnfr/constants/aliases.py +33 -0
  55. tnfr/constants/aliases.pyi +27 -0
  56. tnfr/constants/init.py +3 -1
  57. tnfr/constants/init.pyi +12 -0
  58. tnfr/constants/metric.py +9 -15
  59. tnfr/constants/metric.pyi +19 -0
  60. tnfr/core/__init__.py +33 -0
  61. tnfr/core/container.py +226 -0
  62. tnfr/core/default_implementations.py +329 -0
  63. tnfr/core/interfaces.py +279 -0
  64. tnfr/dynamics/__init__.py +213 -633
  65. tnfr/dynamics/__init__.pyi +83 -0
  66. tnfr/dynamics/adaptation.py +267 -0
  67. tnfr/dynamics/adaptation.pyi +7 -0
  68. tnfr/dynamics/adaptive_sequences.py +189 -0
  69. tnfr/dynamics/adaptive_sequences.pyi +14 -0
  70. tnfr/dynamics/aliases.py +23 -0
  71. tnfr/dynamics/aliases.pyi +19 -0
  72. tnfr/dynamics/bifurcation.py +232 -0
  73. tnfr/dynamics/canonical.py +229 -0
  74. tnfr/dynamics/canonical.pyi +48 -0
  75. tnfr/dynamics/coordination.py +385 -0
  76. tnfr/dynamics/coordination.pyi +25 -0
  77. tnfr/dynamics/dnfr.py +2699 -398
  78. tnfr/dynamics/dnfr.pyi +26 -0
  79. tnfr/dynamics/dynamic_limits.py +225 -0
  80. tnfr/dynamics/feedback.py +252 -0
  81. tnfr/dynamics/feedback.pyi +24 -0
  82. tnfr/dynamics/fused_dnfr.py +454 -0
  83. tnfr/dynamics/homeostasis.py +157 -0
  84. tnfr/dynamics/homeostasis.pyi +14 -0
  85. tnfr/dynamics/integrators.py +496 -102
  86. tnfr/dynamics/integrators.pyi +36 -0
  87. tnfr/dynamics/learning.py +310 -0
  88. tnfr/dynamics/learning.pyi +33 -0
  89. tnfr/dynamics/metabolism.py +254 -0
  90. tnfr/dynamics/nbody.py +796 -0
  91. tnfr/dynamics/nbody_tnfr.py +783 -0
  92. tnfr/dynamics/propagation.py +326 -0
  93. tnfr/dynamics/runtime.py +908 -0
  94. tnfr/dynamics/runtime.pyi +77 -0
  95. tnfr/dynamics/sampling.py +10 -5
  96. tnfr/dynamics/sampling.pyi +7 -0
  97. tnfr/dynamics/selectors.py +711 -0
  98. tnfr/dynamics/selectors.pyi +85 -0
  99. tnfr/dynamics/structural_clip.py +207 -0
  100. tnfr/errors/__init__.py +37 -0
  101. tnfr/errors/contextual.py +492 -0
  102. tnfr/execution.py +77 -55
  103. tnfr/execution.pyi +45 -0
  104. tnfr/extensions/__init__.py +205 -0
  105. tnfr/extensions/__init__.pyi +18 -0
  106. tnfr/extensions/base.py +173 -0
  107. tnfr/extensions/base.pyi +35 -0
  108. tnfr/extensions/business/__init__.py +71 -0
  109. tnfr/extensions/business/__init__.pyi +11 -0
  110. tnfr/extensions/business/cookbook.py +88 -0
  111. tnfr/extensions/business/cookbook.pyi +8 -0
  112. tnfr/extensions/business/health_analyzers.py +202 -0
  113. tnfr/extensions/business/health_analyzers.pyi +9 -0
  114. tnfr/extensions/business/patterns.py +183 -0
  115. tnfr/extensions/business/patterns.pyi +8 -0
  116. tnfr/extensions/medical/__init__.py +73 -0
  117. tnfr/extensions/medical/__init__.pyi +11 -0
  118. tnfr/extensions/medical/cookbook.py +88 -0
  119. tnfr/extensions/medical/cookbook.pyi +8 -0
  120. tnfr/extensions/medical/health_analyzers.py +181 -0
  121. tnfr/extensions/medical/health_analyzers.pyi +9 -0
  122. tnfr/extensions/medical/patterns.py +163 -0
  123. tnfr/extensions/medical/patterns.pyi +8 -0
  124. tnfr/flatten.py +29 -50
  125. tnfr/flatten.pyi +21 -0
  126. tnfr/gamma.py +66 -53
  127. tnfr/gamma.pyi +36 -0
  128. tnfr/glyph_history.py +144 -57
  129. tnfr/glyph_history.pyi +35 -0
  130. tnfr/glyph_runtime.py +19 -0
  131. tnfr/glyph_runtime.pyi +8 -0
  132. tnfr/immutable.py +70 -30
  133. tnfr/immutable.pyi +36 -0
  134. tnfr/initialization.py +22 -16
  135. tnfr/initialization.pyi +65 -0
  136. tnfr/io.py +5 -241
  137. tnfr/io.pyi +13 -0
  138. tnfr/locking.pyi +7 -0
  139. tnfr/mathematics/__init__.py +79 -0
  140. tnfr/mathematics/backend.py +453 -0
  141. tnfr/mathematics/backend.pyi +99 -0
  142. tnfr/mathematics/dynamics.py +408 -0
  143. tnfr/mathematics/dynamics.pyi +90 -0
  144. tnfr/mathematics/epi.py +391 -0
  145. tnfr/mathematics/epi.pyi +65 -0
  146. tnfr/mathematics/generators.py +242 -0
  147. tnfr/mathematics/generators.pyi +29 -0
  148. tnfr/mathematics/metrics.py +119 -0
  149. tnfr/mathematics/metrics.pyi +16 -0
  150. tnfr/mathematics/operators.py +239 -0
  151. tnfr/mathematics/operators.pyi +59 -0
  152. tnfr/mathematics/operators_factory.py +124 -0
  153. tnfr/mathematics/operators_factory.pyi +11 -0
  154. tnfr/mathematics/projection.py +87 -0
  155. tnfr/mathematics/projection.pyi +33 -0
  156. tnfr/mathematics/runtime.py +182 -0
  157. tnfr/mathematics/runtime.pyi +64 -0
  158. tnfr/mathematics/spaces.py +256 -0
  159. tnfr/mathematics/spaces.pyi +83 -0
  160. tnfr/mathematics/transforms.py +305 -0
  161. tnfr/mathematics/transforms.pyi +62 -0
  162. tnfr/metrics/__init__.py +47 -9
  163. tnfr/metrics/__init__.pyi +20 -0
  164. tnfr/metrics/buffer_cache.py +163 -0
  165. tnfr/metrics/buffer_cache.pyi +24 -0
  166. tnfr/metrics/cache_utils.py +214 -0
  167. tnfr/metrics/coherence.py +1510 -330
  168. tnfr/metrics/coherence.pyi +129 -0
  169. tnfr/metrics/common.py +23 -16
  170. tnfr/metrics/common.pyi +35 -0
  171. tnfr/metrics/core.py +251 -36
  172. tnfr/metrics/core.pyi +13 -0
  173. tnfr/metrics/diagnosis.py +709 -110
  174. tnfr/metrics/diagnosis.pyi +86 -0
  175. tnfr/metrics/emergence.py +245 -0
  176. tnfr/metrics/export.py +60 -18
  177. tnfr/metrics/export.pyi +7 -0
  178. tnfr/metrics/glyph_timing.py +233 -43
  179. tnfr/metrics/glyph_timing.pyi +81 -0
  180. tnfr/metrics/learning_metrics.py +280 -0
  181. tnfr/metrics/learning_metrics.pyi +21 -0
  182. tnfr/metrics/phase_coherence.py +351 -0
  183. tnfr/metrics/phase_compatibility.py +349 -0
  184. tnfr/metrics/reporting.py +63 -28
  185. tnfr/metrics/reporting.pyi +25 -0
  186. tnfr/metrics/sense_index.py +1126 -43
  187. tnfr/metrics/sense_index.pyi +9 -0
  188. tnfr/metrics/trig.py +215 -23
  189. tnfr/metrics/trig.pyi +13 -0
  190. tnfr/metrics/trig_cache.py +148 -24
  191. tnfr/metrics/trig_cache.pyi +10 -0
  192. tnfr/multiscale/__init__.py +32 -0
  193. tnfr/multiscale/hierarchical.py +517 -0
  194. tnfr/node.py +646 -140
  195. tnfr/node.pyi +139 -0
  196. tnfr/observers.py +160 -45
  197. tnfr/observers.pyi +31 -0
  198. tnfr/ontosim.py +23 -19
  199. tnfr/ontosim.pyi +28 -0
  200. tnfr/operators/__init__.py +1358 -106
  201. tnfr/operators/__init__.pyi +31 -0
  202. tnfr/operators/algebra.py +277 -0
  203. tnfr/operators/canonical_patterns.py +420 -0
  204. tnfr/operators/cascade.py +267 -0
  205. tnfr/operators/cycle_detection.py +358 -0
  206. tnfr/operators/definitions.py +4108 -0
  207. tnfr/operators/definitions.pyi +78 -0
  208. tnfr/operators/grammar.py +1164 -0
  209. tnfr/operators/grammar.pyi +140 -0
  210. tnfr/operators/hamiltonian.py +710 -0
  211. tnfr/operators/health_analyzer.py +809 -0
  212. tnfr/operators/jitter.py +107 -38
  213. tnfr/operators/jitter.pyi +11 -0
  214. tnfr/operators/lifecycle.py +314 -0
  215. tnfr/operators/metabolism.py +618 -0
  216. tnfr/operators/metrics.py +2138 -0
  217. tnfr/operators/network_analysis/__init__.py +27 -0
  218. tnfr/operators/network_analysis/source_detection.py +186 -0
  219. tnfr/operators/nodal_equation.py +395 -0
  220. tnfr/operators/pattern_detection.py +660 -0
  221. tnfr/operators/patterns.py +669 -0
  222. tnfr/operators/postconditions/__init__.py +38 -0
  223. tnfr/operators/postconditions/mutation.py +236 -0
  224. tnfr/operators/preconditions/__init__.py +1226 -0
  225. tnfr/operators/preconditions/coherence.py +305 -0
  226. tnfr/operators/preconditions/dissonance.py +236 -0
  227. tnfr/operators/preconditions/emission.py +128 -0
  228. tnfr/operators/preconditions/mutation.py +580 -0
  229. tnfr/operators/preconditions/reception.py +125 -0
  230. tnfr/operators/preconditions/resonance.py +364 -0
  231. tnfr/operators/registry.py +74 -0
  232. tnfr/operators/registry.pyi +9 -0
  233. tnfr/operators/remesh.py +1415 -91
  234. tnfr/operators/remesh.pyi +26 -0
  235. tnfr/operators/structural_units.py +268 -0
  236. tnfr/operators/unified_grammar.py +105 -0
  237. tnfr/parallel/__init__.py +54 -0
  238. tnfr/parallel/auto_scaler.py +234 -0
  239. tnfr/parallel/distributed.py +384 -0
  240. tnfr/parallel/engine.py +238 -0
  241. tnfr/parallel/gpu_engine.py +420 -0
  242. tnfr/parallel/monitoring.py +248 -0
  243. tnfr/parallel/partitioner.py +459 -0
  244. tnfr/py.typed +0 -0
  245. tnfr/recipes/__init__.py +22 -0
  246. tnfr/recipes/cookbook.py +743 -0
  247. tnfr/rng.py +75 -151
  248. tnfr/rng.pyi +26 -0
  249. tnfr/schemas/__init__.py +8 -0
  250. tnfr/schemas/grammar.json +94 -0
  251. tnfr/sdk/__init__.py +107 -0
  252. tnfr/sdk/__init__.pyi +19 -0
  253. tnfr/sdk/adaptive_system.py +173 -0
  254. tnfr/sdk/adaptive_system.pyi +21 -0
  255. tnfr/sdk/builders.py +370 -0
  256. tnfr/sdk/builders.pyi +51 -0
  257. tnfr/sdk/fluent.py +1121 -0
  258. tnfr/sdk/fluent.pyi +74 -0
  259. tnfr/sdk/templates.py +342 -0
  260. tnfr/sdk/templates.pyi +41 -0
  261. tnfr/sdk/utils.py +341 -0
  262. tnfr/secure_config.py +46 -0
  263. tnfr/security/__init__.py +70 -0
  264. tnfr/security/database.py +514 -0
  265. tnfr/security/subprocess.py +503 -0
  266. tnfr/security/validation.py +290 -0
  267. tnfr/selector.py +59 -22
  268. tnfr/selector.pyi +19 -0
  269. tnfr/sense.py +92 -67
  270. tnfr/sense.pyi +23 -0
  271. tnfr/services/__init__.py +17 -0
  272. tnfr/services/orchestrator.py +325 -0
  273. tnfr/sparse/__init__.py +39 -0
  274. tnfr/sparse/representations.py +492 -0
  275. tnfr/structural.py +639 -263
  276. tnfr/structural.pyi +83 -0
  277. tnfr/telemetry/__init__.py +35 -0
  278. tnfr/telemetry/cache_metrics.py +226 -0
  279. tnfr/telemetry/cache_metrics.pyi +64 -0
  280. tnfr/telemetry/nu_f.py +422 -0
  281. tnfr/telemetry/nu_f.pyi +108 -0
  282. tnfr/telemetry/verbosity.py +36 -0
  283. tnfr/telemetry/verbosity.pyi +15 -0
  284. tnfr/tokens.py +2 -4
  285. tnfr/tokens.pyi +36 -0
  286. tnfr/tools/__init__.py +20 -0
  287. tnfr/tools/domain_templates.py +478 -0
  288. tnfr/tools/sequence_generator.py +846 -0
  289. tnfr/topology/__init__.py +13 -0
  290. tnfr/topology/asymmetry.py +151 -0
  291. tnfr/trace.py +300 -126
  292. tnfr/trace.pyi +42 -0
  293. tnfr/tutorials/__init__.py +38 -0
  294. tnfr/tutorials/autonomous_evolution.py +285 -0
  295. tnfr/tutorials/interactive.py +1576 -0
  296. tnfr/tutorials/structural_metabolism.py +238 -0
  297. tnfr/types.py +743 -12
  298. tnfr/types.pyi +357 -0
  299. tnfr/units.py +68 -0
  300. tnfr/units.pyi +13 -0
  301. tnfr/utils/__init__.py +282 -0
  302. tnfr/utils/__init__.pyi +215 -0
  303. tnfr/utils/cache.py +4223 -0
  304. tnfr/utils/cache.pyi +470 -0
  305. tnfr/{callback_utils.py → utils/callbacks.py} +26 -39
  306. tnfr/utils/callbacks.pyi +49 -0
  307. tnfr/utils/chunks.py +108 -0
  308. tnfr/utils/chunks.pyi +22 -0
  309. tnfr/utils/data.py +428 -0
  310. tnfr/utils/data.pyi +74 -0
  311. tnfr/utils/graph.py +85 -0
  312. tnfr/utils/graph.pyi +10 -0
  313. tnfr/utils/init.py +821 -0
  314. tnfr/utils/init.pyi +80 -0
  315. tnfr/utils/io.py +559 -0
  316. tnfr/utils/io.pyi +66 -0
  317. tnfr/{helpers → utils}/numeric.py +51 -24
  318. tnfr/utils/numeric.pyi +21 -0
  319. tnfr/validation/__init__.py +257 -0
  320. tnfr/validation/__init__.pyi +85 -0
  321. tnfr/validation/compatibility.py +460 -0
  322. tnfr/validation/compatibility.pyi +6 -0
  323. tnfr/validation/config.py +73 -0
  324. tnfr/validation/graph.py +139 -0
  325. tnfr/validation/graph.pyi +18 -0
  326. tnfr/validation/input_validation.py +755 -0
  327. tnfr/validation/invariants.py +712 -0
  328. tnfr/validation/rules.py +253 -0
  329. tnfr/validation/rules.pyi +44 -0
  330. tnfr/validation/runtime.py +279 -0
  331. tnfr/validation/runtime.pyi +28 -0
  332. tnfr/validation/sequence_validator.py +162 -0
  333. tnfr/validation/soft_filters.py +170 -0
  334. tnfr/validation/soft_filters.pyi +32 -0
  335. tnfr/validation/spectral.py +164 -0
  336. tnfr/validation/spectral.pyi +42 -0
  337. tnfr/validation/validator.py +1266 -0
  338. tnfr/validation/window.py +39 -0
  339. tnfr/validation/window.pyi +1 -0
  340. tnfr/visualization/__init__.py +98 -0
  341. tnfr/visualization/cascade_viz.py +256 -0
  342. tnfr/visualization/hierarchy.py +284 -0
  343. tnfr/visualization/sequence_plotter.py +784 -0
  344. tnfr/viz/__init__.py +60 -0
  345. tnfr/viz/matplotlib.py +278 -0
  346. tnfr/viz/matplotlib.pyi +35 -0
  347. tnfr-8.5.0.dist-info/METADATA +573 -0
  348. tnfr-8.5.0.dist-info/RECORD +353 -0
  349. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/entry_points.txt +1 -0
  350. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/licenses/LICENSE.md +1 -1
  351. tnfr/collections_utils.py +0 -300
  352. tnfr/config.py +0 -32
  353. tnfr/grammar.py +0 -344
  354. tnfr/graph_utils.py +0 -84
  355. tnfr/helpers/__init__.py +0 -71
  356. tnfr/import_utils.py +0 -228
  357. tnfr/json_utils.py +0 -162
  358. tnfr/logging_utils.py +0 -116
  359. tnfr/presets.py +0 -60
  360. tnfr/validators.py +0 -84
  361. tnfr/value_utils.py +0 -59
  362. tnfr-4.5.2.dist-info/METADATA +0 -379
  363. tnfr-4.5.2.dist-info/RECORD +0 -67
  364. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
  365. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
tnfr/flatten.pyi ADDED
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Iterator, Sequence
4
+ from typing import Any
5
+
6
+ from .tokens import THOL, Token
7
+
8
+ __all__: list[str]
9
+
10
+ def __getattr__(name: str) -> Any: ...
11
+
12
+ class THOLEvaluator(Iterator[Token | object]):
13
+ def __init__(self, item: THOL, *, max_materialize: int | None = ...) -> None: ...
14
+ def __iter__(self) -> THOLEvaluator: ...
15
+ def __next__(self) -> Token | object: ...
16
+
17
+ def parse_program_tokens(
18
+ obj: Iterable[Any] | Sequence[Any] | Any,
19
+ *,
20
+ max_materialize: int | None = ...,
21
+ ) -> list[Token]: ...
tnfr/gamma.py CHANGED
@@ -1,24 +1,26 @@
1
1
  """Gamma registry."""
2
2
 
3
3
  from __future__ import annotations
4
- from typing import Any, Callable, NamedTuple
5
- import math
6
- import logging
4
+
7
5
  import hashlib
6
+ import logging
7
+ import math
8
8
  from collections.abc import Mapping
9
9
  from functools import lru_cache
10
10
  from types import MappingProxyType
11
+ from typing import Any, Callable, NamedTuple
11
12
 
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
13
+ from .alias import get_theta_attr
14
+ from .constants import DEFAULTS
15
+ from .utils import json_dumps
18
16
  from .metrics.trig_cache import get_trig_cache
19
-
20
- ALIAS_THETA = get_aliases("THETA")
21
-
17
+ from .types import GammaSpec, NodeId, TNFRGraph
18
+ from .utils import (
19
+ edge_version_cache,
20
+ get_graph_mapping,
21
+ get_logger,
22
+ node_set_checksum,
23
+ )
22
24
 
23
25
  logger = get_logger(__name__)
24
26
 
@@ -44,9 +46,8 @@ def _default_gamma_spec() -> tuple[bytes, str]:
44
46
  return dumped, hash_
45
47
 
46
48
 
47
- def _ensure_kuramoto_cache(G, t) -> None:
48
- """Cache ``(R, ψ)`` for the current step ``t`` using
49
- ``edge_version_cache``."""
49
+ def _ensure_kuramoto_cache(G: TNFRGraph, t: float | int) -> None:
50
+ """Cache ``(R, ψ)`` for the current step ``t`` using ``edge_version_cache``."""
50
51
  checksum = G.graph.get("_dnfr_nodes_checksum")
51
52
  if checksum is None:
52
53
  # reuse checksum from cached_nodes_and_A when available
@@ -63,7 +64,7 @@ def _ensure_kuramoto_cache(G, t) -> None:
63
64
  G.graph["_kuramoto_cache"] = entry
64
65
 
65
66
 
66
- def kuramoto_R_psi(G) -> tuple[float, float]:
67
+ def kuramoto_R_psi(G: TNFRGraph) -> tuple[float, float]:
67
68
  """Return ``(R, ψ)`` for Kuramoto order using θ from all nodes."""
68
69
  max_steps = int(G.graph.get("KURAMOTO_CACHE_STEPS", 1))
69
70
  trig = get_trig_cache(G, cache_size=max_steps)
@@ -78,7 +79,9 @@ def kuramoto_R_psi(G) -> tuple[float, float]:
78
79
  return R, psi
79
80
 
80
81
 
81
- def _kuramoto_common(G, node, _cfg):
82
+ def _kuramoto_common(
83
+ G: TNFRGraph, node: NodeId, _cfg: GammaSpec
84
+ ) -> tuple[float, float, float]:
82
85
  """Return ``(θ_i, R, ψ)`` for Kuramoto-based Γ functions.
83
86
 
84
87
  Reads cached global order ``R`` and mean phase ``ψ`` and obtains node
@@ -88,11 +91,12 @@ def _kuramoto_common(G, node, _cfg):
88
91
  cache = G.graph.get("_kuramoto_cache", {})
89
92
  R = float(cache.get("R", 0.0))
90
93
  psi = float(cache.get("psi", 0.0))
91
- th_i = get_attr(G.nodes[node], ALIAS_THETA, 0.0)
94
+ th_val = get_theta_attr(G.nodes[node], 0.0)
95
+ th_i = float(th_val if th_val is not None else 0.0)
92
96
  return th_i, R, psi
93
97
 
94
98
 
95
- def _read_gamma_raw(G) -> Mapping[str, Any] | None:
99
+ def _read_gamma_raw(G: TNFRGraph) -> GammaSpec | None:
96
100
  """Return raw Γ specification from ``G.graph['GAMMA']``.
97
101
 
98
102
  The returned value is the direct contents of ``G.graph['GAMMA']`` when
@@ -104,11 +108,13 @@ def _read_gamma_raw(G) -> Mapping[str, Any] | None:
104
108
  if raw is None or isinstance(raw, Mapping):
105
109
  return raw
106
110
  return get_graph_mapping(
107
- G, "GAMMA", "G.graph['GAMMA'] no es un mapeo; se usa {'type': 'none'}"
111
+ G,
112
+ "GAMMA",
113
+ "G.graph['GAMMA'] is not a mapping; using {'type': 'none'}",
108
114
  )
109
115
 
110
116
 
111
- def _get_gamma_spec(G) -> Mapping[str, Any]:
117
+ def _get_gamma_spec(G: TNFRGraph) -> GammaSpec:
112
118
  """Return validated Γ specification caching results.
113
119
 
114
120
  The raw value from ``G.graph['GAMMA']`` is cached together with the
@@ -122,7 +128,7 @@ def _get_gamma_spec(G) -> Mapping[str, Any]:
122
128
  cached_spec = G.graph.get("_gamma_spec")
123
129
  cached_hash = G.graph.get("_gamma_spec_hash")
124
130
 
125
- def _hash_mapping(mapping: Mapping[str, Any]) -> str:
131
+ def _hash_mapping(mapping: GammaSpec) -> str:
126
132
  dumped = json_dumps(mapping, sort_keys=True, to_bytes=True)
127
133
  return hashlib.blake2b(dumped, digest_size=16).hexdigest()
128
134
 
@@ -165,9 +171,7 @@ def _get_gamma_spec(G) -> Mapping[str, Any]:
165
171
  # -----------------
166
172
 
167
173
 
168
- def _gamma_params(
169
- cfg: Mapping[str, Any], **defaults: float
170
- ) -> tuple[float, ...]:
174
+ def _gamma_params(cfg: GammaSpec, **defaults: float) -> tuple[float, ...]:
171
175
  """Return normalized Γ parameters from ``cfg``.
172
176
 
173
177
  Parameters are retrieved from ``cfg`` using the keys in ``defaults`` and
@@ -179,28 +183,28 @@ def _gamma_params(
179
183
  >>> beta, R0 = _gamma_params(cfg, beta=0.0, R0=0.0)
180
184
  """
181
185
 
182
- return tuple(
183
- float(cfg.get(name, default)) for name, default in defaults.items()
184
- )
186
+ return tuple(float(cfg.get(name, default)) for name, default in defaults.items())
185
187
 
186
188
 
187
189
  # -----------------
188
- # Γi(R) canónicos
190
+ # Canonical Γi(R)
189
191
  # -----------------
190
192
 
191
193
 
192
- def gamma_none(G, node, t, cfg: dict[str, Any]) -> float:
194
+ def gamma_none(G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec) -> float:
195
+ """Return ``0.0`` to disable Γ forcing for the given node."""
196
+
193
197
  return 0.0
194
198
 
195
199
 
196
200
  def _gamma_kuramoto(
197
- G,
198
- node,
199
- cfg: Mapping[str, Any],
201
+ G: TNFRGraph,
202
+ node: NodeId,
203
+ cfg: GammaSpec,
200
204
  builder: Callable[..., float],
201
205
  **defaults: float,
202
206
  ) -> float:
203
- """Helper for Kuramoto-based Γ functions.
207
+ """Construct a Kuramoto-based Γ function.
204
208
 
205
209
  ``builder`` receives ``(θ_i, R, ψ, *params)`` where ``params`` are
206
210
  extracted from ``cfg`` according to ``defaults``.
@@ -220,11 +224,15 @@ def _builder_bandpass(th_i: float, R: float, psi: float, beta: float) -> float:
220
224
  return beta * R * (1.0 - R) * sgn
221
225
 
222
226
 
223
- def _builder_tanh(th_i: float, R: float, psi: float, beta: float, k: float, R0: float) -> float:
227
+ def _builder_tanh(
228
+ th_i: float, R: float, psi: float, beta: float, k: float, R0: float
229
+ ) -> float:
224
230
  return beta * math.tanh(k * (R - R0)) * math.cos(th_i - psi)
225
231
 
226
232
 
227
- def gamma_kuramoto_linear(G, node, t, cfg: dict[str, Any]) -> float:
233
+ def gamma_kuramoto_linear(
234
+ G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec
235
+ ) -> float:
228
236
  """Linear Kuramoto coupling for Γi(R).
229
237
 
230
238
  Formula: Γ = β · (R - R0) · cos(θ_i - ψ)
@@ -239,13 +247,17 @@ def gamma_kuramoto_linear(G, node, t, cfg: dict[str, Any]) -> float:
239
247
  return _gamma_kuramoto(G, node, cfg, _builder_linear, beta=0.0, R0=0.0)
240
248
 
241
249
 
242
- def gamma_kuramoto_bandpass(G, node, t, cfg: dict[str, Any]) -> float:
243
- """Γ = β · R(1-R) · sign(cos(θ_i - ψ))"""
250
+ def gamma_kuramoto_bandpass(
251
+ G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec
252
+ ) -> float:
253
+ """Compute Γ = β · R(1-R) · sign(cos(θ_i - ψ))."""
244
254
 
245
255
  return _gamma_kuramoto(G, node, cfg, _builder_bandpass, beta=0.0)
246
256
 
247
257
 
248
- def gamma_kuramoto_tanh(G, node, t, cfg: dict[str, Any]) -> float:
258
+ def gamma_kuramoto_tanh(
259
+ G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec
260
+ ) -> float:
249
261
  """Saturating tanh coupling for Γi(R).
250
262
 
251
263
  Formula: Γ = β · tanh(k·(R - R0)) · cos(θ_i - ψ)
@@ -257,7 +269,7 @@ def gamma_kuramoto_tanh(G, node, t, cfg: dict[str, Any]) -> float:
257
269
  return _gamma_kuramoto(G, node, cfg, _builder_tanh, beta=0.0, k=1.0, R0=0.0)
258
270
 
259
271
 
260
- def gamma_harmonic(G, node, t, cfg: dict[str, Any]) -> float:
272
+ def gamma_harmonic(G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec) -> float:
261
273
  """Harmonic forcing aligned with the global phase field.
262
274
 
263
275
  Formula: Γ = β · sin(ω·t + φ) · cos(θ_i - ψ)
@@ -271,13 +283,15 @@ def gamma_harmonic(G, node, t, cfg: dict[str, Any]) -> float:
271
283
 
272
284
 
273
285
  class GammaEntry(NamedTuple):
274
- fn: Callable[[Any, Any, Any, dict[str, Any]], float]
286
+ """Lookup entry linking Γ evaluators with their preconditions."""
287
+
288
+ fn: Callable[[TNFRGraph, NodeId, float | int, GammaSpec], float]
275
289
  needs_kuramoto: bool
276
290
 
277
291
 
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.
292
+ # ``GAMMA_REGISTRY`` associates each coupling name with a ``GammaEntry`` where
293
+ # ``fn`` is the evaluation function and ``needs_kuramoto`` indicates whether
294
+ # the global phase order must be precomputed.
281
295
  GAMMA_REGISTRY: dict[str, GammaEntry] = {
282
296
  "none": GammaEntry(gamma_none, False),
283
297
  "kuramoto_linear": GammaEntry(gamma_kuramoto_linear, True),
@@ -288,20 +302,19 @@ GAMMA_REGISTRY: dict[str, GammaEntry] = {
288
302
 
289
303
 
290
304
  def eval_gamma(
291
- G,
292
- node,
293
- t,
305
+ G: TNFRGraph,
306
+ node: NodeId,
307
+ t: float | int,
294
308
  *,
295
309
  strict: bool = False,
296
310
  log_level: int | None = None,
297
311
  ) -> float:
298
- """Evaluate Γi for ``node`` according to ``G.graph['GAMMA']``
299
- specification.
312
+ """Evaluate Γi for ``node`` using ``G.graph['GAMMA']`` specification.
300
313
 
301
314
  If ``strict`` is ``True`` exceptions raised during evaluation are
302
315
  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``.
316
+ Γ type is not registered a warning is emitted (or ``ValueError`` in
317
+ strict mode) and ``gamma_none`` is used.
305
318
 
306
319
  ``log_level`` controls the logging level for captured errors when
307
320
  ``strict`` is ``False``. If omitted, ``logging.ERROR`` is used in
@@ -311,7 +324,7 @@ def eval_gamma(
311
324
  spec_type = spec.get("type", "none")
312
325
  reg_entry = GAMMA_REGISTRY.get(spec_type)
313
326
  if reg_entry is None:
314
- msg = f"Tipo GAMMA desconocido: {spec_type}"
327
+ msg = f"Unknown GAMMA type: {spec_type}"
315
328
  if strict:
316
329
  raise ValueError(msg)
317
330
  logger.warning(msg)
@@ -330,7 +343,7 @@ def eval_gamma(
330
343
  )
331
344
  logger.log(
332
345
  level,
333
- "Fallo al evaluar Γi para nodo %s en t=%s: %s: %s",
346
+ "Failed to evaluate Γi for node %s at t=%s: %s: %s",
334
347
  node,
335
348
  t,
336
349
  exc.__class__.__name__,
tnfr/gamma.pyi ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, NamedTuple
4
+
5
+ from .types import GammaSpec, NodeId, TNFRGraph
6
+
7
+ __all__: tuple[str, ...]
8
+
9
+ class GammaEntry(NamedTuple):
10
+ fn: Callable[[TNFRGraph, NodeId, float | int, GammaSpec], float]
11
+ needs_kuramoto: bool
12
+
13
+ GAMMA_REGISTRY: dict[str, GammaEntry]
14
+
15
+ def kuramoto_R_psi(G: TNFRGraph) -> tuple[float, float]: ...
16
+ def gamma_none(G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec) -> float: ...
17
+ def gamma_kuramoto_linear(
18
+ G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec
19
+ ) -> float: ...
20
+ def gamma_kuramoto_bandpass(
21
+ G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec
22
+ ) -> float: ...
23
+ def gamma_kuramoto_tanh(
24
+ G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec
25
+ ) -> float: ...
26
+ def gamma_harmonic(
27
+ G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec
28
+ ) -> float: ...
29
+ def eval_gamma(
30
+ G: TNFRGraph,
31
+ node: NodeId,
32
+ t: float | int,
33
+ *,
34
+ strict: bool = ...,
35
+ log_level: int | None = ...,
36
+ ) -> float: ...
tnfr/glyph_history.py CHANGED
@@ -1,16 +1,21 @@
1
- """Utilities for tracking glyph emission history and related metrics."""
1
+ """Utilities for tracking structural operator emission history and related metrics.
2
+
3
+ This module tracks the history of glyphs (structural symbols like AL, EN, IL, etc.)
4
+ that are emitted when structural operators (Emission, Reception, Coherence, etc.)
5
+ are applied to nodes in the TNFR network.
6
+ """
2
7
 
3
8
  from __future__ import annotations
4
9
 
5
- from typing import Any
6
- from collections import deque, Counter
10
+ from collections import Counter, deque
11
+ from collections.abc import Iterable, Mapping, MutableMapping
7
12
  from itertools import islice
8
- from collections.abc import Iterable, Mapping
9
- from functools import lru_cache
13
+ from typing import Any, cast
10
14
 
11
- from .constants import get_param
12
- from .collections_utils import ensure_collection
13
- from .logging_utils import get_logger
15
+ from .constants import get_param, normalise_state_token
16
+ from .glyph_runtime import last_glyph
17
+ from .types import TNFRGraph
18
+ from .utils import ensure_collection, get_logger
14
19
 
15
20
  logger = get_logger(__name__)
16
21
 
@@ -21,28 +26,27 @@ __all__ = (
21
26
  "ensure_history",
22
27
  "current_step_idx",
23
28
  "append_metric",
24
- "last_glyph",
25
29
  "count_glyphs",
26
30
  )
27
31
 
28
-
29
- @lru_cache(maxsize=1)
30
- def _resolve_validate_window():
31
- from .validators import validate_window
32
-
33
- return validate_window
34
-
35
-
36
- def _validate_window(window: int, *, positive: bool = False) -> int:
37
- return _resolve_validate_window()(window, positive=positive)
32
+ _NU_F_HISTORY_KEYS = (
33
+ "nu_f_rate_hz_str",
34
+ "nu_f_rate_hz",
35
+ "nu_f_ci_lower_hz_str",
36
+ "nu_f_ci_upper_hz_str",
37
+ "nu_f_ci_lower_hz",
38
+ "nu_f_ci_upper_hz",
39
+ )
38
40
 
39
41
 
40
42
  def _ensure_history(
41
- nd: dict[str, Any], window: int, *, create_zero: bool = False
42
- ) -> tuple[int, deque | None]:
43
+ nd: MutableMapping[str, Any], window: int, *, create_zero: bool = False
44
+ ) -> tuple[int, deque[str] | None]:
43
45
  """Validate ``window`` and ensure ``nd['glyph_history']`` deque."""
44
46
 
45
- v_window = _validate_window(window)
47
+ from tnfr.validation.window import validate_window
48
+
49
+ v_window = validate_window(window)
46
50
  if v_window == 0 and not create_zero:
47
51
  return v_window, None
48
52
  hist = nd.setdefault("glyph_history", deque(maxlen=v_window))
@@ -54,16 +58,14 @@ def _ensure_history(
54
58
  try:
55
59
  items = ensure_collection(hist, max_materialize=None)
56
60
  except TypeError:
57
- logger.debug(
58
- "Discarding non-iterable glyph history value %r", hist
59
- )
61
+ logger.debug("Discarding non-iterable glyph history value %r", hist)
60
62
  items = ()
61
- hist = deque(items, maxlen=v_window)
63
+ hist = deque((str(item) for item in items), maxlen=v_window)
62
64
  nd["glyph_history"] = hist
63
65
  return v_window, hist
64
66
 
65
67
 
66
- def push_glyph(nd: dict[str, Any], glyph: str, window: int) -> None:
68
+ def push_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> None:
67
69
  """Add ``glyph`` to node history with maximum size ``window``.
68
70
 
69
71
  ``window`` validation and deque creation are handled by
@@ -74,22 +76,47 @@ def push_glyph(nd: dict[str, Any], glyph: str, window: int) -> None:
74
76
  hist.append(str(glyph))
75
77
 
76
78
 
77
- def recent_glyph(nd: dict[str, Any], glyph: str, window: int) -> bool:
79
+ def recent_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> bool:
78
80
  """Return ``True`` if ``glyph`` appeared in last ``window`` emissions.
79
81
 
80
- ``window`` validation and deque creation are handled by
81
- :func:`_ensure_history`. A ``window`` of zero returns ``False`` and
82
- leaves ``nd`` unchanged. Negative values raise :class:`ValueError`.
82
+ This is a **read-only** operation that checks the existing history without
83
+ modifying it. If ``window`` is zero, returns ``False``. Negative values
84
+ raise :class:`ValueError`.
85
+
86
+ Notes
87
+ -----
88
+ This function intentionally does NOT call ``_ensure_history`` to avoid
89
+ accidentally truncating the glyph_history deque when checking with a
90
+ smaller window than the deque's maxlen. This preserves the canonical
91
+ principle that reading history should not modify it.
92
+
93
+ Reuses ``validate_window`` and ``ensure_collection`` utilities.
83
94
  """
95
+ from tnfr.validation.window import validate_window
84
96
 
85
- v_window, hist = _ensure_history(nd, window)
97
+ v_window = validate_window(window)
86
98
  if v_window == 0:
87
99
  return False
100
+
101
+ # Read existing history without modifying it
102
+ hist = nd.get("glyph_history")
103
+ if hist is None:
104
+ return False
105
+
88
106
  gl = str(glyph)
89
- return gl in hist
107
+
108
+ # Use canonical ensure_collection to materialize history
109
+ try:
110
+ items = list(ensure_collection(hist, max_materialize=None))
111
+ except (TypeError, ValueError):
112
+ return False
113
+
114
+ # Check only the last v_window items
115
+ recent_items = items[-v_window:] if len(items) > v_window else items
116
+ return gl in recent_items
90
117
 
91
118
 
92
- class HistoryDict(dict):
119
+ class HistoryDict(dict[str, Any]):
93
120
  """Dict specialized for bounded history series and usage counts.
94
121
 
95
122
  Usage counts are tracked explicitly via :meth:`get_increment`. Accessing
@@ -108,7 +135,7 @@ class HistoryDict(dict):
108
135
 
109
136
  def __init__(
110
137
  self,
111
- data: dict[str, Any] | None = None,
138
+ data: Mapping[str, Any] | None = None,
112
139
  *,
113
140
  maxlen: int = 0,
114
141
  ) -> None:
@@ -129,7 +156,7 @@ class HistoryDict(dict):
129
156
  """Increase usage count for ``key``."""
130
157
  self._counts[key] += 1
131
158
 
132
- def _to_deque(self, val: Any) -> deque:
159
+ def _to_deque(self, val: Any) -> deque[Any]:
133
160
  """Coerce ``val`` to a deque respecting ``self._maxlen``.
134
161
 
135
162
  ``Iterable`` inputs (excluding ``str`` and ``bytes``) are expanded into
@@ -155,26 +182,36 @@ class HistoryDict(dict):
155
182
  return val
156
183
 
157
184
  def get_increment(self, key: str, default: Any = None) -> Any:
185
+ """Return value for ``key`` and increment its usage counter."""
186
+
158
187
  insert = key not in self
159
188
  val = self._resolve_value(key, default, insert=insert)
160
189
  self._increment(key)
161
190
  return val
162
191
 
163
- def __getitem__(self, key): # type: ignore[override]
192
+ def __getitem__(self, key: str) -> Any: # type: ignore[override]
193
+ """Return the tracked value for ``key`` ensuring deque normalisation."""
194
+
164
195
  return self._resolve_value(key, None, insert=False)
165
196
 
166
- def get(self, key, default=None): # type: ignore[override]
197
+ def get(self, key: str, default: Any | None = None) -> Any: # type: ignore[override]
198
+ """Return ``key`` when present; otherwise fall back to ``default``."""
199
+
167
200
  try:
168
201
  return self._resolve_value(key, None, insert=False)
169
202
  except KeyError:
170
203
  return default
171
204
 
172
- def __setitem__(self, key, value): # type: ignore[override]
205
+ def __setitem__(self, key: str, value: Any) -> None: # type: ignore[override]
206
+ """Store ``value`` for ``key`` while initialising usage tracking."""
207
+
173
208
  super().__setitem__(key, value)
174
209
  if key not in self._counts:
175
210
  self._counts[key] = 0
176
211
 
177
- def setdefault(self, key, default=None): # type: ignore[override]
212
+ def setdefault(self, key: str, default: Any | None = None) -> Any: # type: ignore[override]
213
+ """Return existing value for ``key`` or insert ``default`` when absent."""
214
+
178
215
  insert = key not in self
179
216
  val = self._resolve_value(key, default, insert=insert)
180
217
  if insert:
@@ -191,6 +228,8 @@ class HistoryDict(dict):
191
228
  raise KeyError("HistoryDict is empty; cannot pop least used")
192
229
 
193
230
  def pop_least_used_batch(self, k: int) -> None:
231
+ """Remove up to ``k`` least-used entries from the history."""
232
+
194
233
  for _ in range(max(0, int(k))):
195
234
  try:
196
235
  self.pop_least_used()
@@ -198,7 +237,7 @@ class HistoryDict(dict):
198
237
  break
199
238
 
200
239
 
201
- def ensure_history(G) -> dict[str, Any]:
240
+ def ensure_history(G: TNFRGraph) -> HistoryDict | dict[str, Any]:
202
241
  """Ensure ``G.graph['history']`` exists and return it.
203
242
 
204
243
  ``HISTORY_MAXLEN`` must be non-negative; otherwise a
@@ -220,11 +259,10 @@ def ensure_history(G) -> dict[str, Any]:
220
259
  replaced = True
221
260
  if replaced:
222
261
  G.graph.pop(sentinel_key, None)
262
+ if isinstance(hist, MutableMapping):
263
+ _normalise_state_streams(hist)
223
264
  return hist
224
- if (
225
- not isinstance(hist, HistoryDict)
226
- or hist._maxlen != maxlen
227
- ):
265
+ if not isinstance(hist, HistoryDict) or hist._maxlen != maxlen:
228
266
  hist = HistoryDict(hist, maxlen=maxlen)
229
267
  G.graph["history"] = hist
230
268
  replaced = True
@@ -233,31 +271,44 @@ def ensure_history(G) -> dict[str, Any]:
233
271
  hist.pop_least_used_batch(excess)
234
272
  if replaced:
235
273
  G.graph.pop(sentinel_key, None)
274
+ _normalise_state_streams(cast(MutableMapping[str, Any], hist))
236
275
  return hist
237
276
 
238
277
 
239
- def current_step_idx(G) -> int:
278
+ def current_step_idx(G: TNFRGraph | Mapping[str, Any]) -> int:
240
279
  """Return the current step index from ``G`` history."""
241
280
 
242
281
  graph = getattr(G, "graph", G)
243
282
  return len(graph.get("history", {}).get("C_steps", []))
244
283
 
245
-
246
284
 
247
- def append_metric(hist: dict[str, Any], key: str, value: Any) -> None:
285
+ def append_metric(hist: MutableMapping[str, list[Any]], key: str, value: Any) -> None:
248
286
  """Append ``value`` to ``hist[key]`` list, creating it if missing."""
249
- hist.setdefault(key, []).append(value)
250
-
287
+ if key == "phase_state" and isinstance(value, str):
288
+ value = normalise_state_token(value)
289
+ elif key == "nodal_diag" and isinstance(value, Mapping):
290
+ snapshot: dict[Any, Any] = {}
291
+ for node, payload in value.items():
292
+ if isinstance(payload, Mapping):
293
+ state_value = payload.get("state")
294
+ if isinstance(payload, MutableMapping):
295
+ updated = payload
296
+ else:
297
+ updated = dict(payload)
298
+ if isinstance(state_value, str):
299
+ updated["state"] = normalise_state_token(state_value)
300
+ snapshot[node] = updated
301
+ else:
302
+ snapshot[node] = payload
303
+ hist.setdefault(key, []).append(snapshot)
304
+ return
251
305
 
252
- def last_glyph(nd: dict[str, Any]) -> str | None:
253
- """Return the most recent glyph for node or ``None``."""
254
- hist = nd.get("glyph_history")
255
- return hist[-1] if hist else None
306
+ hist.setdefault(key, []).append(value)
256
307
 
257
308
 
258
309
  def count_glyphs(
259
- G, window: int | None = None, *, last_only: bool = False
260
- ) -> Counter:
310
+ G: TNFRGraph, window: int | None = None, *, last_only: bool = False
311
+ ) -> Counter[str]:
261
312
  """Count recent glyphs in the network.
262
313
 
263
314
  If ``window`` is ``None``, the full history for each node is used. A
@@ -266,7 +317,9 @@ def count_glyphs(
266
317
  """
267
318
 
268
319
  if window is not None:
269
- window = _validate_window(window)
320
+ from tnfr.validation.window import validate_window
321
+
322
+ window = validate_window(window)
270
323
  if window == 0:
271
324
  return Counter()
272
325
 
@@ -288,3 +341,37 @@ def count_glyphs(
288
341
  counts.update(seq)
289
342
 
290
343
  return counts
344
+
345
+
346
+ def _normalise_state_streams(hist: MutableMapping[str, Any]) -> None:
347
+ """Normalise legacy state tokens stored in telemetry history."""
348
+
349
+ phase_state = hist.get("phase_state")
350
+ if isinstance(phase_state, deque):
351
+ canonical = [normalise_state_token(str(item)) for item in phase_state]
352
+ if canonical != list(phase_state):
353
+ phase_state.clear()
354
+ phase_state.extend(canonical)
355
+ elif isinstance(phase_state, list):
356
+ canonical = [normalise_state_token(str(item)) for item in phase_state]
357
+ if canonical != phase_state:
358
+ hist["phase_state"] = canonical
359
+
360
+ diag_history = hist.get("nodal_diag")
361
+ if isinstance(diag_history, list):
362
+ for snapshot in diag_history:
363
+ if not isinstance(snapshot, Mapping):
364
+ continue
365
+ for node, payload in snapshot.items():
366
+ if not isinstance(payload, Mapping):
367
+ continue
368
+ state_value = payload.get("state")
369
+ if not isinstance(state_value, str):
370
+ continue
371
+ canonical = normalise_state_token(state_value)
372
+ if canonical == state_value:
373
+ continue
374
+ if isinstance(payload, MutableMapping):
375
+ payload["state"] = canonical
376
+ else:
377
+ snapshot[node] = {**payload, "state": canonical}