tnfr 3.0.3__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 (360) hide show
  1. tnfr/__init__.py +375 -56
  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 +723 -0
  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 +171 -0
  15. tnfr/cache.pyi +13 -0
  16. tnfr/cli/__init__.py +110 -0
  17. tnfr/cli/__init__.pyi +26 -0
  18. tnfr/cli/arguments.py +489 -0
  19. tnfr/cli/arguments.pyi +29 -0
  20. tnfr/cli/execution.py +914 -0
  21. tnfr/cli/execution.pyi +70 -0
  22. tnfr/cli/interactive_validator.py +614 -0
  23. tnfr/cli/utils.py +51 -0
  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/config/constants.py +104 -0
  34. tnfr/config/constants.pyi +12 -0
  35. tnfr/config/defaults.py +54 -0
  36. tnfr/config/defaults_core.py +212 -0
  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 +92 -0
  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 +33 -0
  57. tnfr/constants/init.pyi +12 -0
  58. tnfr/constants/metric.py +104 -0
  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 +238 -0
  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 +3034 -0
  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 +661 -0
  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 +36 -0
  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 +223 -0
  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 +262 -0
  125. tnfr/flatten.pyi +21 -0
  126. tnfr/gamma.py +354 -0
  127. tnfr/gamma.pyi +36 -0
  128. tnfr/glyph_history.py +377 -0
  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 +218 -0
  133. tnfr/immutable.pyi +36 -0
  134. tnfr/initialization.py +203 -0
  135. tnfr/initialization.pyi +65 -0
  136. tnfr/io.py +10 -0
  137. tnfr/io.pyi +13 -0
  138. tnfr/locking.py +37 -0
  139. tnfr/locking.pyi +7 -0
  140. tnfr/mathematics/__init__.py +79 -0
  141. tnfr/mathematics/backend.py +453 -0
  142. tnfr/mathematics/backend.pyi +99 -0
  143. tnfr/mathematics/dynamics.py +408 -0
  144. tnfr/mathematics/dynamics.pyi +90 -0
  145. tnfr/mathematics/epi.py +391 -0
  146. tnfr/mathematics/epi.pyi +65 -0
  147. tnfr/mathematics/generators.py +242 -0
  148. tnfr/mathematics/generators.pyi +29 -0
  149. tnfr/mathematics/metrics.py +119 -0
  150. tnfr/mathematics/metrics.pyi +16 -0
  151. tnfr/mathematics/operators.py +239 -0
  152. tnfr/mathematics/operators.pyi +59 -0
  153. tnfr/mathematics/operators_factory.py +124 -0
  154. tnfr/mathematics/operators_factory.pyi +11 -0
  155. tnfr/mathematics/projection.py +87 -0
  156. tnfr/mathematics/projection.pyi +33 -0
  157. tnfr/mathematics/runtime.py +182 -0
  158. tnfr/mathematics/runtime.pyi +64 -0
  159. tnfr/mathematics/spaces.py +256 -0
  160. tnfr/mathematics/spaces.pyi +83 -0
  161. tnfr/mathematics/transforms.py +305 -0
  162. tnfr/mathematics/transforms.pyi +62 -0
  163. tnfr/metrics/__init__.py +79 -0
  164. tnfr/metrics/__init__.pyi +20 -0
  165. tnfr/metrics/buffer_cache.py +163 -0
  166. tnfr/metrics/buffer_cache.pyi +24 -0
  167. tnfr/metrics/cache_utils.py +214 -0
  168. tnfr/metrics/coherence.py +2009 -0
  169. tnfr/metrics/coherence.pyi +129 -0
  170. tnfr/metrics/common.py +158 -0
  171. tnfr/metrics/common.pyi +35 -0
  172. tnfr/metrics/core.py +316 -0
  173. tnfr/metrics/core.pyi +13 -0
  174. tnfr/metrics/diagnosis.py +833 -0
  175. tnfr/metrics/diagnosis.pyi +86 -0
  176. tnfr/metrics/emergence.py +245 -0
  177. tnfr/metrics/export.py +179 -0
  178. tnfr/metrics/export.pyi +7 -0
  179. tnfr/metrics/glyph_timing.py +379 -0
  180. tnfr/metrics/glyph_timing.pyi +81 -0
  181. tnfr/metrics/learning_metrics.py +280 -0
  182. tnfr/metrics/learning_metrics.pyi +21 -0
  183. tnfr/metrics/phase_coherence.py +351 -0
  184. tnfr/metrics/phase_compatibility.py +349 -0
  185. tnfr/metrics/reporting.py +183 -0
  186. tnfr/metrics/reporting.pyi +25 -0
  187. tnfr/metrics/sense_index.py +1203 -0
  188. tnfr/metrics/sense_index.pyi +9 -0
  189. tnfr/metrics/trig.py +373 -0
  190. tnfr/metrics/trig.pyi +13 -0
  191. tnfr/metrics/trig_cache.py +233 -0
  192. tnfr/metrics/trig_cache.pyi +10 -0
  193. tnfr/multiscale/__init__.py +32 -0
  194. tnfr/multiscale/hierarchical.py +517 -0
  195. tnfr/node.py +763 -0
  196. tnfr/node.pyi +139 -0
  197. tnfr/observers.py +255 -130
  198. tnfr/observers.pyi +31 -0
  199. tnfr/ontosim.py +144 -137
  200. tnfr/ontosim.pyi +28 -0
  201. tnfr/operators/__init__.py +1672 -0
  202. tnfr/operators/__init__.pyi +31 -0
  203. tnfr/operators/algebra.py +277 -0
  204. tnfr/operators/canonical_patterns.py +420 -0
  205. tnfr/operators/cascade.py +267 -0
  206. tnfr/operators/cycle_detection.py +358 -0
  207. tnfr/operators/definitions.py +4108 -0
  208. tnfr/operators/definitions.pyi +78 -0
  209. tnfr/operators/grammar.py +1164 -0
  210. tnfr/operators/grammar.pyi +140 -0
  211. tnfr/operators/hamiltonian.py +710 -0
  212. tnfr/operators/health_analyzer.py +809 -0
  213. tnfr/operators/jitter.py +272 -0
  214. tnfr/operators/jitter.pyi +11 -0
  215. tnfr/operators/lifecycle.py +314 -0
  216. tnfr/operators/metabolism.py +618 -0
  217. tnfr/operators/metrics.py +2138 -0
  218. tnfr/operators/network_analysis/__init__.py +27 -0
  219. tnfr/operators/network_analysis/source_detection.py +186 -0
  220. tnfr/operators/nodal_equation.py +395 -0
  221. tnfr/operators/pattern_detection.py +660 -0
  222. tnfr/operators/patterns.py +669 -0
  223. tnfr/operators/postconditions/__init__.py +38 -0
  224. tnfr/operators/postconditions/mutation.py +236 -0
  225. tnfr/operators/preconditions/__init__.py +1226 -0
  226. tnfr/operators/preconditions/coherence.py +305 -0
  227. tnfr/operators/preconditions/dissonance.py +236 -0
  228. tnfr/operators/preconditions/emission.py +128 -0
  229. tnfr/operators/preconditions/mutation.py +580 -0
  230. tnfr/operators/preconditions/reception.py +125 -0
  231. tnfr/operators/preconditions/resonance.py +364 -0
  232. tnfr/operators/registry.py +74 -0
  233. tnfr/operators/registry.pyi +9 -0
  234. tnfr/operators/remesh.py +1809 -0
  235. tnfr/operators/remesh.pyi +26 -0
  236. tnfr/operators/structural_units.py +268 -0
  237. tnfr/operators/unified_grammar.py +105 -0
  238. tnfr/parallel/__init__.py +54 -0
  239. tnfr/parallel/auto_scaler.py +234 -0
  240. tnfr/parallel/distributed.py +384 -0
  241. tnfr/parallel/engine.py +238 -0
  242. tnfr/parallel/gpu_engine.py +420 -0
  243. tnfr/parallel/monitoring.py +248 -0
  244. tnfr/parallel/partitioner.py +459 -0
  245. tnfr/py.typed +0 -0
  246. tnfr/recipes/__init__.py +22 -0
  247. tnfr/recipes/cookbook.py +743 -0
  248. tnfr/rng.py +178 -0
  249. tnfr/rng.pyi +26 -0
  250. tnfr/schemas/__init__.py +8 -0
  251. tnfr/schemas/grammar.json +94 -0
  252. tnfr/sdk/__init__.py +107 -0
  253. tnfr/sdk/__init__.pyi +19 -0
  254. tnfr/sdk/adaptive_system.py +173 -0
  255. tnfr/sdk/adaptive_system.pyi +21 -0
  256. tnfr/sdk/builders.py +370 -0
  257. tnfr/sdk/builders.pyi +51 -0
  258. tnfr/sdk/fluent.py +1121 -0
  259. tnfr/sdk/fluent.pyi +74 -0
  260. tnfr/sdk/templates.py +342 -0
  261. tnfr/sdk/templates.pyi +41 -0
  262. tnfr/sdk/utils.py +341 -0
  263. tnfr/secure_config.py +46 -0
  264. tnfr/security/__init__.py +70 -0
  265. tnfr/security/database.py +514 -0
  266. tnfr/security/subprocess.py +503 -0
  267. tnfr/security/validation.py +290 -0
  268. tnfr/selector.py +247 -0
  269. tnfr/selector.pyi +19 -0
  270. tnfr/sense.py +378 -0
  271. tnfr/sense.pyi +23 -0
  272. tnfr/services/__init__.py +17 -0
  273. tnfr/services/orchestrator.py +325 -0
  274. tnfr/sparse/__init__.py +39 -0
  275. tnfr/sparse/representations.py +492 -0
  276. tnfr/structural.py +705 -0
  277. tnfr/structural.pyi +83 -0
  278. tnfr/telemetry/__init__.py +35 -0
  279. tnfr/telemetry/cache_metrics.py +226 -0
  280. tnfr/telemetry/cache_metrics.pyi +64 -0
  281. tnfr/telemetry/nu_f.py +422 -0
  282. tnfr/telemetry/nu_f.pyi +108 -0
  283. tnfr/telemetry/verbosity.py +36 -0
  284. tnfr/telemetry/verbosity.pyi +15 -0
  285. tnfr/tokens.py +58 -0
  286. tnfr/tokens.pyi +36 -0
  287. tnfr/tools/__init__.py +20 -0
  288. tnfr/tools/domain_templates.py +478 -0
  289. tnfr/tools/sequence_generator.py +846 -0
  290. tnfr/topology/__init__.py +13 -0
  291. tnfr/topology/asymmetry.py +151 -0
  292. tnfr/trace.py +543 -0
  293. tnfr/trace.pyi +42 -0
  294. tnfr/tutorials/__init__.py +38 -0
  295. tnfr/tutorials/autonomous_evolution.py +285 -0
  296. tnfr/tutorials/interactive.py +1576 -0
  297. tnfr/tutorials/structural_metabolism.py +238 -0
  298. tnfr/types.py +775 -0
  299. tnfr/types.pyi +357 -0
  300. tnfr/units.py +68 -0
  301. tnfr/units.pyi +13 -0
  302. tnfr/utils/__init__.py +282 -0
  303. tnfr/utils/__init__.pyi +215 -0
  304. tnfr/utils/cache.py +4223 -0
  305. tnfr/utils/cache.pyi +470 -0
  306. tnfr/utils/callbacks.py +375 -0
  307. tnfr/utils/callbacks.pyi +49 -0
  308. tnfr/utils/chunks.py +108 -0
  309. tnfr/utils/chunks.pyi +22 -0
  310. tnfr/utils/data.py +428 -0
  311. tnfr/utils/data.pyi +74 -0
  312. tnfr/utils/graph.py +85 -0
  313. tnfr/utils/graph.pyi +10 -0
  314. tnfr/utils/init.py +821 -0
  315. tnfr/utils/init.pyi +80 -0
  316. tnfr/utils/io.py +559 -0
  317. tnfr/utils/io.pyi +66 -0
  318. tnfr/utils/numeric.py +114 -0
  319. tnfr/utils/numeric.pyi +21 -0
  320. tnfr/validation/__init__.py +257 -0
  321. tnfr/validation/__init__.pyi +85 -0
  322. tnfr/validation/compatibility.py +460 -0
  323. tnfr/validation/compatibility.pyi +6 -0
  324. tnfr/validation/config.py +73 -0
  325. tnfr/validation/graph.py +139 -0
  326. tnfr/validation/graph.pyi +18 -0
  327. tnfr/validation/input_validation.py +755 -0
  328. tnfr/validation/invariants.py +712 -0
  329. tnfr/validation/rules.py +253 -0
  330. tnfr/validation/rules.pyi +44 -0
  331. tnfr/validation/runtime.py +279 -0
  332. tnfr/validation/runtime.pyi +28 -0
  333. tnfr/validation/sequence_validator.py +162 -0
  334. tnfr/validation/soft_filters.py +170 -0
  335. tnfr/validation/soft_filters.pyi +32 -0
  336. tnfr/validation/spectral.py +164 -0
  337. tnfr/validation/spectral.pyi +42 -0
  338. tnfr/validation/validator.py +1266 -0
  339. tnfr/validation/window.py +39 -0
  340. tnfr/validation/window.pyi +1 -0
  341. tnfr/visualization/__init__.py +98 -0
  342. tnfr/visualization/cascade_viz.py +256 -0
  343. tnfr/visualization/hierarchy.py +284 -0
  344. tnfr/visualization/sequence_plotter.py +784 -0
  345. tnfr/viz/__init__.py +60 -0
  346. tnfr/viz/matplotlib.py +278 -0
  347. tnfr/viz/matplotlib.pyi +35 -0
  348. tnfr-8.5.0.dist-info/METADATA +573 -0
  349. tnfr-8.5.0.dist-info/RECORD +353 -0
  350. tnfr-8.5.0.dist-info/entry_points.txt +3 -0
  351. tnfr-3.0.3.dist-info/licenses/LICENSE.txt → tnfr-8.5.0.dist-info/licenses/LICENSE.md +1 -1
  352. tnfr/constants.py +0 -183
  353. tnfr/dynamics.py +0 -543
  354. tnfr/helpers.py +0 -198
  355. tnfr/main.py +0 -37
  356. tnfr/operators.py +0 -296
  357. tnfr-3.0.3.dist-info/METADATA +0 -35
  358. tnfr-3.0.3.dist-info/RECORD +0 -13
  359. {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
  360. {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
tnfr/glyph_history.py ADDED
@@ -0,0 +1,377 @@
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
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections import Counter, deque
11
+ from collections.abc import Iterable, Mapping, MutableMapping
12
+ from itertools import islice
13
+ from typing import Any, cast
14
+
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
19
+
20
+ logger = get_logger(__name__)
21
+
22
+ __all__ = (
23
+ "HistoryDict",
24
+ "push_glyph",
25
+ "recent_glyph",
26
+ "ensure_history",
27
+ "current_step_idx",
28
+ "append_metric",
29
+ "count_glyphs",
30
+ )
31
+
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
+ )
40
+
41
+
42
+ def _ensure_history(
43
+ nd: MutableMapping[str, Any], window: int, *, create_zero: bool = False
44
+ ) -> tuple[int, deque[str] | None]:
45
+ """Validate ``window`` and ensure ``nd['glyph_history']`` deque."""
46
+
47
+ from tnfr.validation.window import validate_window
48
+
49
+ v_window = validate_window(window)
50
+ if v_window == 0 and not create_zero:
51
+ return v_window, None
52
+ hist = nd.setdefault("glyph_history", deque(maxlen=v_window))
53
+ if not isinstance(hist, deque) or hist.maxlen != v_window:
54
+ # Rebuild deque from any iterable, ignoring raw strings/bytes and scalars
55
+ if isinstance(hist, (str, bytes, bytearray)):
56
+ items: Iterable[Any] = ()
57
+ else:
58
+ try:
59
+ items = ensure_collection(hist, max_materialize=None)
60
+ except TypeError:
61
+ logger.debug("Discarding non-iterable glyph history value %r", hist)
62
+ items = ()
63
+ hist = deque((str(item) for item in items), maxlen=v_window)
64
+ nd["glyph_history"] = hist
65
+ return v_window, hist
66
+
67
+
68
+ def push_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> None:
69
+ """Add ``glyph`` to node history with maximum size ``window``.
70
+
71
+ ``window`` validation and deque creation are handled by
72
+ :func:`_ensure_history`.
73
+ """
74
+
75
+ _, hist = _ensure_history(nd, window, create_zero=True)
76
+ hist.append(str(glyph))
77
+
78
+
79
+ def recent_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> bool:
80
+ """Return ``True`` if ``glyph`` appeared in last ``window`` emissions.
81
+
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.
94
+ """
95
+ from tnfr.validation.window import validate_window
96
+
97
+ v_window = validate_window(window)
98
+ if v_window == 0:
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
+
106
+ gl = str(glyph)
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
117
+
118
+
119
+ class HistoryDict(dict[str, Any]):
120
+ """Dict specialized for bounded history series and usage counts.
121
+
122
+ Usage counts are tracked explicitly via :meth:`get_increment`. Accessing
123
+ keys through ``__getitem__`` or :meth:`get` does not affect the internal
124
+ counters, avoiding surprising evictions on mere reads. Counting is now
125
+ handled with :class:`collections.Counter` alone, relying on
126
+ :meth:`Counter.most_common` to locate least-used entries when required.
127
+
128
+ Parameters
129
+ ----------
130
+ data:
131
+ Initial mapping to populate the dictionary.
132
+ maxlen:
133
+ Maximum length for history lists stored as values.
134
+ """
135
+
136
+ def __init__(
137
+ self,
138
+ data: Mapping[str, Any] | None = None,
139
+ *,
140
+ maxlen: int = 0,
141
+ ) -> None:
142
+ super().__init__(data or {})
143
+ self._maxlen = maxlen
144
+ self._counts: Counter[str] = Counter()
145
+ if self._maxlen > 0:
146
+ for k, v in list(self.items()):
147
+ if isinstance(v, list):
148
+ super().__setitem__(k, deque(v, maxlen=self._maxlen))
149
+ self._counts[k] = 0
150
+ else:
151
+ for k in self:
152
+ self._counts[k] = 0
153
+ # ``_heap`` is no longer required with ``Counter.most_common``.
154
+
155
+ def _increment(self, key: str) -> None:
156
+ """Increase usage count for ``key``."""
157
+ self._counts[key] += 1
158
+
159
+ def _to_deque(self, val: Any) -> deque[Any]:
160
+ """Coerce ``val`` to a deque respecting ``self._maxlen``.
161
+
162
+ ``Iterable`` inputs (excluding ``str`` and ``bytes``) are expanded into
163
+ the deque, while single values are wrapped. Existing deques are
164
+ returned unchanged.
165
+ """
166
+
167
+ if isinstance(val, deque):
168
+ return val
169
+ if isinstance(val, Iterable) and not isinstance(val, (str, bytes)):
170
+ return deque(val, maxlen=self._maxlen)
171
+ return deque([val], maxlen=self._maxlen)
172
+
173
+ def _resolve_value(self, key: str, default: Any, *, insert: bool) -> Any:
174
+ if insert:
175
+ val = super().setdefault(key, default)
176
+ else:
177
+ val = super().__getitem__(key)
178
+ if self._maxlen > 0:
179
+ if not isinstance(val, Mapping):
180
+ val = self._to_deque(val)
181
+ super().__setitem__(key, val)
182
+ return val
183
+
184
+ def get_increment(self, key: str, default: Any = None) -> Any:
185
+ """Return value for ``key`` and increment its usage counter."""
186
+
187
+ insert = key not in self
188
+ val = self._resolve_value(key, default, insert=insert)
189
+ self._increment(key)
190
+ return val
191
+
192
+ def __getitem__(self, key: str) -> Any: # type: ignore[override]
193
+ """Return the tracked value for ``key`` ensuring deque normalisation."""
194
+
195
+ return self._resolve_value(key, None, insert=False)
196
+
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
+
200
+ try:
201
+ return self._resolve_value(key, None, insert=False)
202
+ except KeyError:
203
+ return default
204
+
205
+ def __setitem__(self, key: str, value: Any) -> None: # type: ignore[override]
206
+ """Store ``value`` for ``key`` while initialising usage tracking."""
207
+
208
+ super().__setitem__(key, value)
209
+ if key not in self._counts:
210
+ self._counts[key] = 0
211
+
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
+
215
+ insert = key not in self
216
+ val = self._resolve_value(key, default, insert=insert)
217
+ if insert:
218
+ self._counts[key] = 0
219
+ return val
220
+
221
+ def pop_least_used(self) -> Any:
222
+ """Remove and return the value with the smallest usage count."""
223
+ while self._counts:
224
+ key = min(self._counts, key=self._counts.get)
225
+ self._counts.pop(key, None)
226
+ if key in self:
227
+ return super().pop(key)
228
+ raise KeyError("HistoryDict is empty; cannot pop least used")
229
+
230
+ def pop_least_used_batch(self, k: int) -> None:
231
+ """Remove up to ``k`` least-used entries from the history."""
232
+
233
+ for _ in range(max(0, int(k))):
234
+ try:
235
+ self.pop_least_used()
236
+ except KeyError:
237
+ break
238
+
239
+
240
+ def ensure_history(G: TNFRGraph) -> HistoryDict | dict[str, Any]:
241
+ """Ensure ``G.graph['history']`` exists and return it.
242
+
243
+ ``HISTORY_MAXLEN`` must be non-negative; otherwise a
244
+ :class:`ValueError` is raised. When ``HISTORY_MAXLEN`` is zero, a regular
245
+ ``dict`` is used.
246
+ """
247
+ maxlen, _ = _ensure_history({}, int(get_param(G, "HISTORY_MAXLEN")))
248
+ hist = G.graph.get("history")
249
+ sentinel_key = "_metrics_history_id"
250
+ replaced = False
251
+ if maxlen == 0:
252
+ if isinstance(hist, HistoryDict):
253
+ hist = dict(hist)
254
+ G.graph["history"] = hist
255
+ replaced = True
256
+ elif hist is None:
257
+ hist = {}
258
+ G.graph["history"] = hist
259
+ replaced = True
260
+ if replaced:
261
+ G.graph.pop(sentinel_key, None)
262
+ if isinstance(hist, MutableMapping):
263
+ _normalise_state_streams(hist)
264
+ return hist
265
+ if not isinstance(hist, HistoryDict) or hist._maxlen != maxlen:
266
+ hist = HistoryDict(hist, maxlen=maxlen)
267
+ G.graph["history"] = hist
268
+ replaced = True
269
+ excess = len(hist) - maxlen
270
+ if excess > 0:
271
+ hist.pop_least_used_batch(excess)
272
+ if replaced:
273
+ G.graph.pop(sentinel_key, None)
274
+ _normalise_state_streams(cast(MutableMapping[str, Any], hist))
275
+ return hist
276
+
277
+
278
+ def current_step_idx(G: TNFRGraph | Mapping[str, Any]) -> int:
279
+ """Return the current step index from ``G`` history."""
280
+
281
+ graph = getattr(G, "graph", G)
282
+ return len(graph.get("history", {}).get("C_steps", []))
283
+
284
+
285
+ def append_metric(hist: MutableMapping[str, list[Any]], key: str, value: Any) -> None:
286
+ """Append ``value`` to ``hist[key]`` list, creating it if missing."""
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
305
+
306
+ hist.setdefault(key, []).append(value)
307
+
308
+
309
+ def count_glyphs(
310
+ G: TNFRGraph, window: int | None = None, *, last_only: bool = False
311
+ ) -> Counter[str]:
312
+ """Count recent glyphs in the network.
313
+
314
+ If ``window`` is ``None``, the full history for each node is used. A
315
+ ``window`` of zero yields an empty :class:`Counter`. Negative values raise
316
+ :class:`ValueError`.
317
+ """
318
+
319
+ if window is not None:
320
+ from tnfr.validation.window import validate_window
321
+
322
+ window = validate_window(window)
323
+ if window == 0:
324
+ return Counter()
325
+
326
+ counts: Counter[str] = Counter()
327
+ for _, nd in G.nodes(data=True):
328
+ if last_only:
329
+ g = last_glyph(nd)
330
+ if g:
331
+ counts[g] += 1
332
+ continue
333
+ hist = nd.get("glyph_history")
334
+ if not hist:
335
+ continue
336
+ if window is None:
337
+ seq = hist
338
+ else:
339
+ start = max(len(hist) - window, 0)
340
+ seq = islice(hist, start, None)
341
+ counts.update(seq)
342
+
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}
tnfr/glyph_history.pyi ADDED
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from collections.abc import Mapping, MutableMapping
5
+ from typing import Any
6
+
7
+ from .types import TNFRGraph
8
+
9
+ __all__: tuple[str, ...]
10
+
11
+ class HistoryDict(dict[str, Any]):
12
+ _maxlen: int
13
+ _counts: Counter[str]
14
+
15
+ def __init__(
16
+ self, data: Mapping[str, Any] | None = ..., *, maxlen: int = ...
17
+ ) -> None: ...
18
+ def get_increment(self, key: str, default: Any = ...) -> Any: ...
19
+ def __getitem__(self, key: str) -> Any: ...
20
+ def get(self, key: str, default: Any | None = ...) -> Any: ...
21
+ def __setitem__(self, key: str, value: Any) -> None: ...
22
+ def setdefault(self, key: str, default: Any | None = ...) -> Any: ...
23
+ def pop_least_used(self) -> Any: ...
24
+ def pop_least_used_batch(self, k: int) -> None: ...
25
+
26
+ def push_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> None: ...
27
+ def recent_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> bool: ...
28
+ def ensure_history(G: TNFRGraph) -> HistoryDict | dict[str, Any]: ...
29
+ def current_step_idx(G: TNFRGraph | Mapping[str, Any]) -> int: ...
30
+ def append_metric(
31
+ hist: MutableMapping[str, list[Any]], key: str, value: Any
32
+ ) -> None: ...
33
+ def count_glyphs(
34
+ G: TNFRGraph, window: int | None = ..., *, last_only: bool = ...
35
+ ) -> Counter[str]: ...
tnfr/glyph_runtime.py ADDED
@@ -0,0 +1,19 @@
1
+ """Runtime helpers for structural operator glyphs decoupled from validation internals.
2
+
3
+ This module provides utilities for working with glyphs (structural symbols like
4
+ AL, EN, IL, etc.) that represent the application of structural operators to nodes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Mapping
10
+ from typing import Any
11
+
12
+ __all__ = ("last_glyph",)
13
+
14
+
15
+ def last_glyph(nd: Mapping[str, Any]) -> str | None:
16
+ """Return the most recent glyph for node or ``None``."""
17
+
18
+ hist = nd.get("glyph_history")
19
+ return hist[-1] if hist else None
tnfr/glyph_runtime.pyi ADDED
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ __all__: tuple[str, ...]
7
+
8
+ def last_glyph(nd: Mapping[str, Any]) -> str | None: ...
tnfr/immutable.py ADDED
@@ -0,0 +1,218 @@
1
+ """Utilities for freezing objects and checking immutability.
2
+
3
+ Handlers registered via :func:`functools.singledispatch` live in this module
4
+ and are triggered indirectly by the dispatcher when matching types are
5
+ encountered.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import threading
11
+ import weakref
12
+ from collections.abc import Mapping
13
+ from contextlib import contextmanager
14
+ from dataclasses import asdict, is_dataclass
15
+ from functools import lru_cache, partial, singledispatch, wraps
16
+ from types import MappingProxyType
17
+ from typing import Any, Callable, Iterable, Iterator, cast
18
+
19
+ from ._compat import TypeAlias
20
+
21
+ # Types considered immutable without further inspection
22
+ IMMUTABLE_SIMPLE = frozenset({int, float, complex, str, bool, bytes, type(None)})
23
+
24
+ FrozenPrimitive: TypeAlias = int | float | complex | str | bool | bytes | None
25
+ """Primitive immutable values handled directly by :func:`_freeze`."""
26
+
27
+ FrozenCollectionItems: TypeAlias = tuple["FrozenSnapshot", ...]
28
+ """Frozen representation for generic iterables."""
29
+
30
+ FrozenMappingItems: TypeAlias = tuple[tuple[Any, "FrozenSnapshot"], ...]
31
+ """Frozen representation for mapping ``items()`` snapshots."""
32
+
33
+ FrozenTaggedCollection: TypeAlias = tuple[str, FrozenCollectionItems]
34
+ """Tagged iterable snapshot identifying the original container type."""
35
+
36
+ FrozenTaggedMapping: TypeAlias = tuple[str, FrozenMappingItems]
37
+ """Tagged mapping snapshot identifying the original mapping flavour."""
38
+
39
+ FrozenSnapshot: TypeAlias = (
40
+ FrozenPrimitive
41
+ | FrozenCollectionItems
42
+ | FrozenTaggedCollection
43
+ | FrozenTaggedMapping
44
+ )
45
+ """Union describing the immutable snapshot returned by :func:`_freeze`."""
46
+
47
+
48
+ @contextmanager
49
+ def _cycle_guard(value: Any, seen: set[int] | None = None) -> Iterator[set[int]]:
50
+ """Context manager that detects reference cycles during freezing."""
51
+ if seen is None:
52
+ seen = set()
53
+ obj_id = id(value)
54
+ if obj_id in seen:
55
+ raise ValueError("cycle detected")
56
+ seen.add(obj_id)
57
+ try:
58
+ yield seen
59
+ finally:
60
+ seen.remove(obj_id)
61
+
62
+
63
+ def _check_cycle(
64
+ func: Callable[[Any, set[int] | None], FrozenSnapshot],
65
+ ) -> Callable[[Any, set[int] | None], FrozenSnapshot]:
66
+ """Apply :func:`_cycle_guard` to ``func``."""
67
+
68
+ @wraps(func)
69
+ def wrapper(value: Any, seen: set[int] | None = None) -> FrozenSnapshot:
70
+ with _cycle_guard(value, seen) as guard_seen:
71
+ return func(value, guard_seen)
72
+
73
+ return wrapper
74
+
75
+
76
+ def _freeze_dataclass(value: Any, seen: set[int]) -> FrozenTaggedMapping:
77
+ params = getattr(type(value), "__dataclass_params__", None)
78
+ frozen = bool(params and params.frozen)
79
+ data = asdict(value)
80
+ tag = "mapping" if frozen else "dict"
81
+ return (tag, tuple((k, _freeze(v, seen)) for k, v in data.items()))
82
+
83
+
84
+ @singledispatch
85
+ @_check_cycle
86
+ def _freeze(value: Any, seen: set[int] | None = None) -> FrozenSnapshot:
87
+ """Recursively convert ``value`` into an immutable representation."""
88
+ if is_dataclass(value) and not isinstance(value, type):
89
+ assert seen is not None
90
+ return _freeze_dataclass(value, seen)
91
+ if type(value) in IMMUTABLE_SIMPLE:
92
+ return value
93
+ raise TypeError
94
+
95
+
96
+ @_freeze.register(tuple)
97
+ @_check_cycle
98
+ def _freeze_tuple(
99
+ value: tuple[Any, ...], seen: set[int] | None = None
100
+ ) -> FrozenCollectionItems: # noqa: F401
101
+ assert seen is not None
102
+ return tuple(_freeze(v, seen) for v in value)
103
+
104
+
105
+ def _freeze_iterable(
106
+ container: Iterable[Any], tag: str, seen: set[int]
107
+ ) -> FrozenTaggedCollection:
108
+ return (tag, tuple(_freeze(v, seen) for v in container))
109
+
110
+
111
+ def _freeze_iterable_with_tag(
112
+ value: Iterable[Any], seen: set[int] | None = None, *, tag: str
113
+ ) -> FrozenTaggedCollection:
114
+ assert seen is not None
115
+ return _freeze_iterable(value, tag, seen)
116
+
117
+
118
+ def _register_iterable(cls: type, tag: str) -> None:
119
+ handler = _check_cycle(partial(_freeze_iterable_with_tag, tag=tag))
120
+ _freeze.register(cls)(
121
+ cast(Callable[[Any, set[int] | None], FrozenSnapshot], handler)
122
+ )
123
+
124
+
125
+ for _cls, _tag in (
126
+ (list, "list"),
127
+ (set, "set"),
128
+ (frozenset, "frozenset"),
129
+ (bytearray, "bytearray"),
130
+ ):
131
+ _register_iterable(_cls, _tag)
132
+
133
+
134
+ @_freeze.register(Mapping)
135
+ @_check_cycle
136
+ def _freeze_mapping(
137
+ value: Mapping[Any, Any], seen: set[int] | None = None
138
+ ) -> FrozenTaggedMapping: # noqa: F401
139
+ assert seen is not None
140
+ tag = "dict" if hasattr(value, "__setitem__") else "mapping"
141
+ return (tag, tuple((k, _freeze(v, seen)) for k, v in value.items()))
142
+
143
+
144
+ def _all_immutable(iterable: Iterable[Any]) -> bool:
145
+ return all(_is_immutable_inner(v) for v in iterable)
146
+
147
+
148
+ # Dispatch table kept immutable to avoid accidental mutation.
149
+ ImmutableTagHandler: TypeAlias = Callable[[tuple[Any, ...]], bool]
150
+
151
+ _IMMUTABLE_TAG_DISPATCH: Mapping[str, ImmutableTagHandler] = MappingProxyType(
152
+ {
153
+ "mapping": lambda v: _all_immutable(v[1]),
154
+ "frozenset": lambda v: _all_immutable(v[1]),
155
+ "list": lambda v: False,
156
+ "set": lambda v: False,
157
+ "bytearray": lambda v: False,
158
+ "dict": lambda v: False,
159
+ }
160
+ )
161
+
162
+
163
+ @lru_cache(maxsize=1024)
164
+ @singledispatch
165
+ def _is_immutable_inner(value: Any) -> bool:
166
+ """Return ``True`` when ``value`` belongs to the canonical immutable set."""
167
+
168
+ return type(value) in IMMUTABLE_SIMPLE
169
+
170
+
171
+ @_is_immutable_inner.register(tuple)
172
+ def _is_immutable_inner_tuple(value: tuple[Any, ...]) -> bool: # noqa: F401
173
+ if value and isinstance(value[0], str):
174
+ handler = _IMMUTABLE_TAG_DISPATCH.get(value[0])
175
+ if handler is not None:
176
+ return handler(value)
177
+ return _all_immutable(value)
178
+
179
+
180
+ @_is_immutable_inner.register(frozenset)
181
+ def _is_immutable_inner_frozenset(value: frozenset[Any]) -> bool: # noqa: F401
182
+ return _all_immutable(value)
183
+
184
+
185
+ _IMMUTABLE_CACHE: weakref.WeakKeyDictionary[Any, bool] = weakref.WeakKeyDictionary()
186
+ _IMMUTABLE_CACHE_LOCK = threading.Lock()
187
+
188
+
189
+ def _is_immutable(value: Any) -> bool:
190
+ """Check recursively if ``value`` is immutable with caching."""
191
+ with _IMMUTABLE_CACHE_LOCK:
192
+ try:
193
+ return _IMMUTABLE_CACHE[value]
194
+ except (KeyError, TypeError):
195
+ pass # Not in cache or value is unhashable
196
+
197
+ try:
198
+ frozen = _freeze(value)
199
+ except (TypeError, ValueError):
200
+ result = False
201
+ else:
202
+ result = _is_immutable_inner(frozen)
203
+
204
+ with _IMMUTABLE_CACHE_LOCK:
205
+ try:
206
+ _IMMUTABLE_CACHE[value] = result
207
+ except TypeError:
208
+ pass # Value is unhashable, cannot cache
209
+
210
+ return result
211
+
212
+
213
+ __all__ = (
214
+ "_freeze",
215
+ "_is_immutable",
216
+ "_is_immutable_inner",
217
+ "_IMMUTABLE_CACHE",
218
+ )
tnfr/immutable.pyi ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Iterator, Mapping, Union
4
+
5
+ from ._compat import TypeAlias
6
+
7
+ FrozenPrimitive: TypeAlias = Union[int, float, complex, str, bool, bytes, None]
8
+ FrozenCollectionItems: TypeAlias = tuple["FrozenSnapshot", ...]
9
+ FrozenMappingItems: TypeAlias = tuple[tuple[Any, "FrozenSnapshot"], ...]
10
+ FrozenTaggedCollection: TypeAlias = tuple[str, FrozenCollectionItems]
11
+ FrozenTaggedMapping: TypeAlias = tuple[str, FrozenMappingItems]
12
+ FrozenSnapshot: TypeAlias = Union[
13
+ FrozenPrimitive,
14
+ FrozenCollectionItems,
15
+ FrozenTaggedCollection,
16
+ FrozenTaggedMapping,
17
+ ]
18
+ ImmutableTagHandler: TypeAlias = Callable[[tuple[Any, ...]], bool]
19
+
20
+ __all__: tuple[str, ...]
21
+
22
+ def __getattr__(name: str) -> Any: ...
23
+ def _cycle_guard(value: Any, seen: set[int] | None = ...) -> Iterator[set[int]]: ...
24
+ def _check_cycle(
25
+ func: Callable[[Any, set[int] | None], FrozenSnapshot],
26
+ ) -> Callable[[Any, set[int] | None], FrozenSnapshot]: ...
27
+ def _freeze(value: Any, seen: set[int] | None = ...) -> FrozenSnapshot: ...
28
+ def _freeze_mapping(
29
+ value: Mapping[Any, Any],
30
+ seen: set[int] | None = ...,
31
+ ) -> FrozenTaggedMapping: ...
32
+ def _is_immutable(value: Any) -> bool: ...
33
+ def _is_immutable_inner(value: Any) -> bool: ...
34
+
35
+ _IMMUTABLE_CACHE: Any
36
+ _IMMUTABLE_TAG_DISPATCH: Mapping[str, ImmutableTagHandler]