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

Potentially problematic release.


This version of tnfr might be problematic. Click here for more details.

Files changed (195) hide show
  1. tnfr/__init__.py +275 -51
  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 +117 -31
  8. tnfr/alias.pyi +108 -0
  9. tnfr/cache.py +6 -572
  10. tnfr/cache.pyi +16 -0
  11. tnfr/callback_utils.py +16 -38
  12. tnfr/callback_utils.pyi +79 -0
  13. tnfr/cli/__init__.py +34 -14
  14. tnfr/cli/__init__.pyi +26 -0
  15. tnfr/cli/arguments.py +211 -28
  16. tnfr/cli/arguments.pyi +27 -0
  17. tnfr/cli/execution.py +470 -50
  18. tnfr/cli/execution.pyi +70 -0
  19. tnfr/cli/utils.py +18 -3
  20. tnfr/cli/utils.pyi +8 -0
  21. tnfr/config/__init__.py +13 -0
  22. tnfr/config/__init__.pyi +10 -0
  23. tnfr/{constants_glyphs.py → config/constants.py} +26 -20
  24. tnfr/config/constants.pyi +12 -0
  25. tnfr/config/feature_flags.py +83 -0
  26. tnfr/{config.py → config/init.py} +11 -7
  27. tnfr/config/init.pyi +8 -0
  28. tnfr/config/operator_names.py +93 -0
  29. tnfr/config/operator_names.pyi +28 -0
  30. tnfr/config/presets.py +84 -0
  31. tnfr/config/presets.pyi +7 -0
  32. tnfr/constants/__init__.py +80 -29
  33. tnfr/constants/__init__.pyi +92 -0
  34. tnfr/constants/aliases.py +31 -0
  35. tnfr/constants/core.py +4 -4
  36. tnfr/constants/core.pyi +17 -0
  37. tnfr/constants/init.py +1 -1
  38. tnfr/constants/init.pyi +12 -0
  39. tnfr/constants/metric.py +7 -15
  40. tnfr/constants/metric.pyi +19 -0
  41. tnfr/dynamics/__init__.py +165 -633
  42. tnfr/dynamics/__init__.pyi +82 -0
  43. tnfr/dynamics/adaptation.py +267 -0
  44. tnfr/dynamics/aliases.py +23 -0
  45. tnfr/dynamics/coordination.py +385 -0
  46. tnfr/dynamics/dnfr.py +2283 -400
  47. tnfr/dynamics/dnfr.pyi +24 -0
  48. tnfr/dynamics/integrators.py +406 -98
  49. tnfr/dynamics/integrators.pyi +34 -0
  50. tnfr/dynamics/runtime.py +881 -0
  51. tnfr/dynamics/sampling.py +10 -5
  52. tnfr/dynamics/sampling.pyi +7 -0
  53. tnfr/dynamics/selectors.py +719 -0
  54. tnfr/execution.py +70 -48
  55. tnfr/execution.pyi +45 -0
  56. tnfr/flatten.py +13 -9
  57. tnfr/flatten.pyi +21 -0
  58. tnfr/gamma.py +66 -53
  59. tnfr/gamma.pyi +34 -0
  60. tnfr/glyph_history.py +110 -52
  61. tnfr/glyph_history.pyi +35 -0
  62. tnfr/glyph_runtime.py +16 -0
  63. tnfr/glyph_runtime.pyi +9 -0
  64. tnfr/immutable.py +69 -28
  65. tnfr/immutable.pyi +34 -0
  66. tnfr/initialization.py +16 -16
  67. tnfr/initialization.pyi +65 -0
  68. tnfr/io.py +6 -240
  69. tnfr/io.pyi +16 -0
  70. tnfr/locking.pyi +7 -0
  71. tnfr/mathematics/__init__.py +81 -0
  72. tnfr/mathematics/backend.py +426 -0
  73. tnfr/mathematics/dynamics.py +398 -0
  74. tnfr/mathematics/epi.py +254 -0
  75. tnfr/mathematics/generators.py +222 -0
  76. tnfr/mathematics/metrics.py +119 -0
  77. tnfr/mathematics/operators.py +233 -0
  78. tnfr/mathematics/operators_factory.py +71 -0
  79. tnfr/mathematics/projection.py +78 -0
  80. tnfr/mathematics/runtime.py +173 -0
  81. tnfr/mathematics/spaces.py +247 -0
  82. tnfr/mathematics/transforms.py +292 -0
  83. tnfr/metrics/__init__.py +10 -10
  84. tnfr/metrics/__init__.pyi +20 -0
  85. tnfr/metrics/coherence.py +993 -324
  86. tnfr/metrics/common.py +23 -16
  87. tnfr/metrics/common.pyi +46 -0
  88. tnfr/metrics/core.py +251 -35
  89. tnfr/metrics/core.pyi +13 -0
  90. tnfr/metrics/diagnosis.py +708 -111
  91. tnfr/metrics/diagnosis.pyi +85 -0
  92. tnfr/metrics/export.py +27 -15
  93. tnfr/metrics/glyph_timing.py +232 -42
  94. tnfr/metrics/reporting.py +33 -22
  95. tnfr/metrics/reporting.pyi +12 -0
  96. tnfr/metrics/sense_index.py +987 -43
  97. tnfr/metrics/sense_index.pyi +9 -0
  98. tnfr/metrics/trig.py +214 -23
  99. tnfr/metrics/trig.pyi +13 -0
  100. tnfr/metrics/trig_cache.py +115 -22
  101. tnfr/metrics/trig_cache.pyi +10 -0
  102. tnfr/node.py +542 -136
  103. tnfr/node.pyi +178 -0
  104. tnfr/observers.py +152 -35
  105. tnfr/observers.pyi +31 -0
  106. tnfr/ontosim.py +23 -19
  107. tnfr/ontosim.pyi +28 -0
  108. tnfr/operators/__init__.py +601 -82
  109. tnfr/operators/__init__.pyi +45 -0
  110. tnfr/operators/definitions.py +513 -0
  111. tnfr/operators/definitions.pyi +78 -0
  112. tnfr/operators/grammar.py +760 -0
  113. tnfr/operators/jitter.py +107 -38
  114. tnfr/operators/jitter.pyi +11 -0
  115. tnfr/operators/registry.py +75 -0
  116. tnfr/operators/registry.pyi +13 -0
  117. tnfr/operators/remesh.py +149 -88
  118. tnfr/py.typed +0 -0
  119. tnfr/rng.py +46 -143
  120. tnfr/rng.pyi +14 -0
  121. tnfr/schemas/__init__.py +8 -0
  122. tnfr/schemas/grammar.json +94 -0
  123. tnfr/selector.py +25 -19
  124. tnfr/selector.pyi +19 -0
  125. tnfr/sense.py +72 -62
  126. tnfr/sense.pyi +23 -0
  127. tnfr/structural.py +522 -262
  128. tnfr/structural.pyi +69 -0
  129. tnfr/telemetry/__init__.py +35 -0
  130. tnfr/telemetry/cache_metrics.py +226 -0
  131. tnfr/telemetry/nu_f.py +423 -0
  132. tnfr/telemetry/nu_f.pyi +123 -0
  133. tnfr/telemetry/verbosity.py +37 -0
  134. tnfr/tokens.py +1 -3
  135. tnfr/tokens.pyi +36 -0
  136. tnfr/trace.py +270 -113
  137. tnfr/trace.pyi +40 -0
  138. tnfr/types.py +574 -6
  139. tnfr/types.pyi +331 -0
  140. tnfr/units.py +69 -0
  141. tnfr/units.pyi +16 -0
  142. tnfr/utils/__init__.py +217 -0
  143. tnfr/utils/__init__.pyi +202 -0
  144. tnfr/utils/cache.py +2395 -0
  145. tnfr/utils/cache.pyi +468 -0
  146. tnfr/utils/chunks.py +104 -0
  147. tnfr/utils/chunks.pyi +21 -0
  148. tnfr/{collections_utils.py → utils/data.py} +147 -90
  149. tnfr/utils/data.pyi +64 -0
  150. tnfr/utils/graph.py +85 -0
  151. tnfr/utils/graph.pyi +10 -0
  152. tnfr/utils/init.py +770 -0
  153. tnfr/utils/init.pyi +78 -0
  154. tnfr/utils/io.py +456 -0
  155. tnfr/{helpers → utils}/numeric.py +51 -24
  156. tnfr/utils/numeric.pyi +21 -0
  157. tnfr/validation/__init__.py +113 -0
  158. tnfr/validation/__init__.pyi +77 -0
  159. tnfr/validation/compatibility.py +95 -0
  160. tnfr/validation/compatibility.pyi +6 -0
  161. tnfr/validation/grammar.py +71 -0
  162. tnfr/validation/grammar.pyi +40 -0
  163. tnfr/validation/graph.py +138 -0
  164. tnfr/validation/graph.pyi +17 -0
  165. tnfr/validation/rules.py +281 -0
  166. tnfr/validation/rules.pyi +55 -0
  167. tnfr/validation/runtime.py +263 -0
  168. tnfr/validation/runtime.pyi +31 -0
  169. tnfr/validation/soft_filters.py +170 -0
  170. tnfr/validation/soft_filters.pyi +37 -0
  171. tnfr/validation/spectral.py +159 -0
  172. tnfr/validation/spectral.pyi +46 -0
  173. tnfr/validation/syntax.py +40 -0
  174. tnfr/validation/syntax.pyi +10 -0
  175. tnfr/validation/window.py +39 -0
  176. tnfr/validation/window.pyi +1 -0
  177. tnfr/viz/__init__.py +9 -0
  178. tnfr/viz/matplotlib.py +246 -0
  179. tnfr-7.0.0.dist-info/METADATA +179 -0
  180. tnfr-7.0.0.dist-info/RECORD +185 -0
  181. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
  182. tnfr/grammar.py +0 -344
  183. tnfr/graph_utils.py +0 -84
  184. tnfr/helpers/__init__.py +0 -71
  185. tnfr/import_utils.py +0 -228
  186. tnfr/json_utils.py +0 -162
  187. tnfr/logging_utils.py +0 -116
  188. tnfr/presets.py +0 -60
  189. tnfr/validators.py +0 -84
  190. tnfr/value_utils.py +0 -59
  191. tnfr-4.5.2.dist-info/METADATA +0 -379
  192. tnfr-4.5.2.dist-info/RECORD +0 -67
  193. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
  194. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
  195. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
tnfr/glyph_history.py CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any
6
- from collections import deque, Counter
5
+ from collections import Counter, deque
6
+ from collections.abc import Iterable, Mapping, MutableMapping
7
7
  from itertools import islice
8
- from collections.abc import Iterable, Mapping
9
- from functools import lru_cache
8
+ from typing import Any, cast
10
9
 
11
- from .constants import get_param
12
- from .collections_utils import ensure_collection
13
- from .logging_utils import get_logger
10
+ from .constants import get_param, normalise_state_token
11
+ from .glyph_runtime import last_glyph
12
+ from .types import TNFRGraph
13
+ from .utils import ensure_collection, get_logger
14
14
 
15
15
  logger = get_logger(__name__)
16
16
 
@@ -21,28 +21,28 @@ __all__ = (
21
21
  "ensure_history",
22
22
  "current_step_idx",
23
23
  "append_metric",
24
- "last_glyph",
25
24
  "count_glyphs",
26
25
  )
27
26
 
28
27
 
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)
28
+ _NU_F_HISTORY_KEYS = (
29
+ "nu_f_rate_hz_str",
30
+ "nu_f_rate_hz",
31
+ "nu_f_ci_lower_hz_str",
32
+ "nu_f_ci_upper_hz_str",
33
+ "nu_f_ci_lower_hz",
34
+ "nu_f_ci_upper_hz",
35
+ )
38
36
 
39
37
 
40
38
  def _ensure_history(
41
- nd: dict[str, Any], window: int, *, create_zero: bool = False
42
- ) -> tuple[int, deque | None]:
39
+ nd: MutableMapping[str, Any], window: int, *, create_zero: bool = False
40
+ ) -> tuple[int, deque[str] | None]:
43
41
  """Validate ``window`` and ensure ``nd['glyph_history']`` deque."""
44
42
 
45
- v_window = _validate_window(window)
43
+ from tnfr.validation.window import validate_window
44
+
45
+ v_window = validate_window(window)
46
46
  if v_window == 0 and not create_zero:
47
47
  return v_window, None
48
48
  hist = nd.setdefault("glyph_history", deque(maxlen=v_window))
@@ -54,16 +54,14 @@ def _ensure_history(
54
54
  try:
55
55
  items = ensure_collection(hist, max_materialize=None)
56
56
  except TypeError:
57
- logger.debug(
58
- "Discarding non-iterable glyph history value %r", hist
59
- )
57
+ logger.debug("Discarding non-iterable glyph history value %r", hist)
60
58
  items = ()
61
- hist = deque(items, maxlen=v_window)
59
+ hist = deque((str(item) for item in items), maxlen=v_window)
62
60
  nd["glyph_history"] = hist
63
61
  return v_window, hist
64
62
 
65
63
 
66
- def push_glyph(nd: dict[str, Any], glyph: str, window: int) -> None:
64
+ def push_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> None:
67
65
  """Add ``glyph`` to node history with maximum size ``window``.
68
66
 
69
67
  ``window`` validation and deque creation are handled by
@@ -74,7 +72,7 @@ def push_glyph(nd: dict[str, Any], glyph: str, window: int) -> None:
74
72
  hist.append(str(glyph))
75
73
 
76
74
 
77
- def recent_glyph(nd: dict[str, Any], glyph: str, window: int) -> bool:
75
+ def recent_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> bool:
78
76
  """Return ``True`` if ``glyph`` appeared in last ``window`` emissions.
79
77
 
80
78
  ``window`` validation and deque creation are handled by
@@ -89,7 +87,7 @@ def recent_glyph(nd: dict[str, Any], glyph: str, window: int) -> bool:
89
87
  return gl in hist
90
88
 
91
89
 
92
- class HistoryDict(dict):
90
+ class HistoryDict(dict[str, Any]):
93
91
  """Dict specialized for bounded history series and usage counts.
94
92
 
95
93
  Usage counts are tracked explicitly via :meth:`get_increment`. Accessing
@@ -108,7 +106,7 @@ class HistoryDict(dict):
108
106
 
109
107
  def __init__(
110
108
  self,
111
- data: dict[str, Any] | None = None,
109
+ data: Mapping[str, Any] | None = None,
112
110
  *,
113
111
  maxlen: int = 0,
114
112
  ) -> None:
@@ -129,7 +127,7 @@ class HistoryDict(dict):
129
127
  """Increase usage count for ``key``."""
130
128
  self._counts[key] += 1
131
129
 
132
- def _to_deque(self, val: Any) -> deque:
130
+ def _to_deque(self, val: Any) -> deque[Any]:
133
131
  """Coerce ``val`` to a deque respecting ``self._maxlen``.
134
132
 
135
133
  ``Iterable`` inputs (excluding ``str`` and ``bytes``) are expanded into
@@ -155,26 +153,36 @@ class HistoryDict(dict):
155
153
  return val
156
154
 
157
155
  def get_increment(self, key: str, default: Any = None) -> Any:
156
+ """Return value for ``key`` and increment its usage counter."""
157
+
158
158
  insert = key not in self
159
159
  val = self._resolve_value(key, default, insert=insert)
160
160
  self._increment(key)
161
161
  return val
162
162
 
163
- def __getitem__(self, key): # type: ignore[override]
163
+ def __getitem__(self, key: str) -> Any: # type: ignore[override]
164
+ """Return the tracked value for ``key`` ensuring deque normalisation."""
165
+
164
166
  return self._resolve_value(key, None, insert=False)
165
167
 
166
- def get(self, key, default=None): # type: ignore[override]
168
+ def get(self, key: str, default: Any | None = None) -> Any: # type: ignore[override]
169
+ """Return ``key`` when present; otherwise fall back to ``default``."""
170
+
167
171
  try:
168
172
  return self._resolve_value(key, None, insert=False)
169
173
  except KeyError:
170
174
  return default
171
175
 
172
- def __setitem__(self, key, value): # type: ignore[override]
176
+ def __setitem__(self, key: str, value: Any) -> None: # type: ignore[override]
177
+ """Store ``value`` for ``key`` while initialising usage tracking."""
178
+
173
179
  super().__setitem__(key, value)
174
180
  if key not in self._counts:
175
181
  self._counts[key] = 0
176
182
 
177
- def setdefault(self, key, default=None): # type: ignore[override]
183
+ def setdefault(self, key: str, default: Any | None = None) -> Any: # type: ignore[override]
184
+ """Return existing value for ``key`` or insert ``default`` when absent."""
185
+
178
186
  insert = key not in self
179
187
  val = self._resolve_value(key, default, insert=insert)
180
188
  if insert:
@@ -191,6 +199,8 @@ class HistoryDict(dict):
191
199
  raise KeyError("HistoryDict is empty; cannot pop least used")
192
200
 
193
201
  def pop_least_used_batch(self, k: int) -> None:
202
+ """Remove up to ``k`` least-used entries from the history."""
203
+
194
204
  for _ in range(max(0, int(k))):
195
205
  try:
196
206
  self.pop_least_used()
@@ -198,7 +208,7 @@ class HistoryDict(dict):
198
208
  break
199
209
 
200
210
 
201
- def ensure_history(G) -> dict[str, Any]:
211
+ def ensure_history(G: TNFRGraph) -> HistoryDict | dict[str, Any]:
202
212
  """Ensure ``G.graph['history']`` exists and return it.
203
213
 
204
214
  ``HISTORY_MAXLEN`` must be non-negative; otherwise a
@@ -220,11 +230,10 @@ def ensure_history(G) -> dict[str, Any]:
220
230
  replaced = True
221
231
  if replaced:
222
232
  G.graph.pop(sentinel_key, None)
233
+ if isinstance(hist, MutableMapping):
234
+ _normalise_state_streams(hist)
223
235
  return hist
224
- if (
225
- not isinstance(hist, HistoryDict)
226
- or hist._maxlen != maxlen
227
- ):
236
+ if not isinstance(hist, HistoryDict) or hist._maxlen != maxlen:
228
237
  hist = HistoryDict(hist, maxlen=maxlen)
229
238
  G.graph["history"] = hist
230
239
  replaced = True
@@ -233,31 +242,44 @@ def ensure_history(G) -> dict[str, Any]:
233
242
  hist.pop_least_used_batch(excess)
234
243
  if replaced:
235
244
  G.graph.pop(sentinel_key, None)
245
+ _normalise_state_streams(cast(MutableMapping[str, Any], hist))
246
+ for key in _NU_F_HISTORY_KEYS:
247
+ hist.setdefault(key, [])
236
248
  return hist
237
249
 
238
250
 
239
- def current_step_idx(G) -> int:
251
+ def current_step_idx(G: TNFRGraph | Mapping[str, Any]) -> int:
240
252
  """Return the current step index from ``G`` history."""
241
253
 
242
254
  graph = getattr(G, "graph", G)
243
255
  return len(graph.get("history", {}).get("C_steps", []))
244
256
 
245
-
246
257
 
247
- def append_metric(hist: dict[str, Any], key: str, value: Any) -> None:
258
+ def append_metric(hist: MutableMapping[str, list[Any]], key: str, value: Any) -> None:
248
259
  """Append ``value`` to ``hist[key]`` list, creating it if missing."""
249
- hist.setdefault(key, []).append(value)
250
-
251
-
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
256
-
260
+ if key == "phase_state" and isinstance(value, str):
261
+ value = normalise_state_token(value)
262
+ elif key == "nodal_diag" and isinstance(value, Mapping):
263
+ snapshot: dict[Any, Any] = {}
264
+ for node, payload in value.items():
265
+ if isinstance(payload, Mapping):
266
+ state_value = payload.get("state")
267
+ if isinstance(payload, MutableMapping):
268
+ updated = payload
269
+ else:
270
+ updated = dict(payload)
271
+ if isinstance(state_value, str):
272
+ updated["state"] = normalise_state_token(state_value)
273
+ snapshot[node] = updated
274
+ else:
275
+ snapshot[node] = payload
276
+ hist.setdefault(key, []).append(snapshot)
277
+ return
257
278
 
279
+ hist.setdefault(key, []).append(value)
258
280
  def count_glyphs(
259
- G, window: int | None = None, *, last_only: bool = False
260
- ) -> Counter:
281
+ G: TNFRGraph, window: int | None = None, *, last_only: bool = False
282
+ ) -> Counter[str]:
261
283
  """Count recent glyphs in the network.
262
284
 
263
285
  If ``window`` is ``None``, the full history for each node is used. A
@@ -266,7 +288,9 @@ def count_glyphs(
266
288
  """
267
289
 
268
290
  if window is not None:
269
- window = _validate_window(window)
291
+ from tnfr.validation.window import validate_window
292
+
293
+ window = validate_window(window)
270
294
  if window == 0:
271
295
  return Counter()
272
296
 
@@ -288,3 +312,37 @@ def count_glyphs(
288
312
  counts.update(seq)
289
313
 
290
314
  return counts
315
+
316
+
317
+ def _normalise_state_streams(hist: MutableMapping[str, Any]) -> None:
318
+ """Normalise legacy state tokens stored in telemetry history."""
319
+
320
+ phase_state = hist.get("phase_state")
321
+ if isinstance(phase_state, deque):
322
+ canonical = [normalise_state_token(str(item)) for item in phase_state]
323
+ if canonical != list(phase_state):
324
+ phase_state.clear()
325
+ phase_state.extend(canonical)
326
+ elif isinstance(phase_state, list):
327
+ canonical = [normalise_state_token(str(item)) for item in phase_state]
328
+ if canonical != phase_state:
329
+ hist["phase_state"] = canonical
330
+
331
+ diag_history = hist.get("nodal_diag")
332
+ if isinstance(diag_history, list):
333
+ for snapshot in diag_history:
334
+ if not isinstance(snapshot, Mapping):
335
+ continue
336
+ for node, payload in snapshot.items():
337
+ if not isinstance(payload, Mapping):
338
+ continue
339
+ state_value = payload.get("state")
340
+ if not isinstance(state_value, str):
341
+ continue
342
+ canonical = normalise_state_token(state_value)
343
+ if canonical == state_value:
344
+ continue
345
+ if isinstance(payload, MutableMapping):
346
+ payload["state"] = canonical
347
+ else:
348
+ 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,16 @@
1
+ """Runtime glyph helpers decoupled from validation internals."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ __all__ = ("last_glyph",)
9
+
10
+
11
+ def last_glyph(nd: Mapping[str, Any]) -> str | None:
12
+ """Return the most recent glyph for node or ``None``."""
13
+
14
+ hist = nd.get("glyph_history")
15
+ return hist[-1] if hist else None
16
+
tnfr/glyph_runtime.pyi ADDED
@@ -0,0 +1,9 @@
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: ...
9
+
tnfr/immutable.py CHANGED
@@ -7,23 +7,47 @@ encountered.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import threading
11
+ import weakref
12
+ from collections.abc import Mapping
10
13
  from contextlib import contextmanager
11
14
  from dataclasses import asdict, is_dataclass
12
- from functools import lru_cache, singledispatch, wraps, partial
13
- from typing import Any, Callable
14
- from collections.abc import Mapping
15
+ from functools import lru_cache, partial, singledispatch, wraps
15
16
  from types import MappingProxyType
16
- import threading
17
- import weakref
17
+ from typing import Any, Callable, Iterable, Iterator, cast
18
+
19
+ from ._compat import TypeAlias
18
20
 
19
21
  # Types considered immutable without further inspection
20
- IMMUTABLE_SIMPLE = frozenset(
21
- {int, float, complex, str, bool, bytes, type(None)}
22
+ IMMUTABLE_SIMPLE = frozenset({int, float, complex, str, bool, bytes, type(None)})
23
+
24
+
25
+ FrozenPrimitive: TypeAlias = int | float | complex | str | bool | bytes | None
26
+ """Primitive immutable values handled directly by :func:`_freeze`."""
27
+
28
+ FrozenCollectionItems: TypeAlias = tuple["FrozenSnapshot", ...]
29
+ """Frozen representation for generic iterables."""
30
+
31
+ FrozenMappingItems: TypeAlias = tuple[tuple[Any, "FrozenSnapshot"], ...]
32
+ """Frozen representation for mapping ``items()`` snapshots."""
33
+
34
+ FrozenTaggedCollection: TypeAlias = tuple[str, FrozenCollectionItems]
35
+ """Tagged iterable snapshot identifying the original container type."""
36
+
37
+ FrozenTaggedMapping: TypeAlias = tuple[str, FrozenMappingItems]
38
+ """Tagged mapping snapshot identifying the original mapping flavour."""
39
+
40
+ FrozenSnapshot: TypeAlias = (
41
+ FrozenPrimitive
42
+ | FrozenCollectionItems
43
+ | FrozenTaggedCollection
44
+ | FrozenTaggedMapping
22
45
  )
46
+ """Union describing the immutable snapshot returned by :func:`_freeze`."""
23
47
 
24
48
 
25
49
  @contextmanager
26
- def _cycle_guard(value: Any, seen: set[int] | None = None):
50
+ def _cycle_guard(value: Any, seen: set[int] | None = None) -> Iterator[set[int]]:
27
51
  """Context manager that detects reference cycles during freezing."""
28
52
  if seen is None:
29
53
  seen = set()
@@ -37,18 +61,20 @@ def _cycle_guard(value: Any, seen: set[int] | None = None):
37
61
  seen.remove(obj_id)
38
62
 
39
63
 
40
- def _check_cycle(func: Callable[[Any, set[int] | None], Any]):
41
- """Decorator applying :func:`_cycle_guard` to ``func``."""
64
+ def _check_cycle(
65
+ func: Callable[[Any, set[int] | None], FrozenSnapshot],
66
+ ) -> Callable[[Any, set[int] | None], FrozenSnapshot]:
67
+ """Apply :func:`_cycle_guard` to ``func``."""
42
68
 
43
69
  @wraps(func)
44
- def wrapper(value: Any, seen: set[int] | None = None):
45
- with _cycle_guard(value, seen) as seen:
46
- return func(value, seen)
70
+ def wrapper(value: Any, seen: set[int] | None = None) -> FrozenSnapshot:
71
+ with _cycle_guard(value, seen) as guard_seen:
72
+ return func(value, guard_seen)
47
73
 
48
74
  return wrapper
49
75
 
50
76
 
51
- def _freeze_dataclass(value: Any, seen: set[int]):
77
+ def _freeze_dataclass(value: Any, seen: set[int]) -> FrozenTaggedMapping:
52
78
  params = getattr(type(value), "__dataclass_params__", None)
53
79
  frozen = bool(params and params.frozen)
54
80
  data = asdict(value)
@@ -58,9 +84,10 @@ def _freeze_dataclass(value: Any, seen: set[int]):
58
84
 
59
85
  @singledispatch
60
86
  @_check_cycle
61
- def _freeze(value: Any, seen: set[int] | None = None):
87
+ def _freeze(value: Any, seen: set[int] | None = None) -> FrozenSnapshot:
62
88
  """Recursively convert ``value`` into an immutable representation."""
63
89
  if is_dataclass(value) and not isinstance(value, type):
90
+ assert seen is not None
64
91
  return _freeze_dataclass(value, seen)
65
92
  if type(value) in IMMUTABLE_SIMPLE:
66
93
  return value
@@ -69,22 +96,31 @@ def _freeze(value: Any, seen: set[int] | None = None):
69
96
 
70
97
  @_freeze.register(tuple)
71
98
  @_check_cycle
72
- def _freeze_tuple(value: tuple, seen: set[int] | None = None): # noqa: F401
99
+ def _freeze_tuple(
100
+ value: tuple[Any, ...], seen: set[int] | None = None
101
+ ) -> FrozenCollectionItems: # noqa: F401
102
+ assert seen is not None
73
103
  return tuple(_freeze(v, seen) for v in value)
74
104
 
75
105
 
76
- def _freeze_iterable(container: Any, tag: str, seen: set[int] | None) -> tuple[str, tuple]:
106
+ def _freeze_iterable(
107
+ container: Iterable[Any], tag: str, seen: set[int]
108
+ ) -> FrozenTaggedCollection:
77
109
  return (tag, tuple(_freeze(v, seen) for v in container))
78
110
 
79
111
 
80
112
  def _freeze_iterable_with_tag(
81
- value: Any, seen: set[int] | None = None, *, tag: str
82
- ) -> tuple[str, tuple]:
113
+ value: Iterable[Any], seen: set[int] | None = None, *, tag: str
114
+ ) -> FrozenTaggedCollection:
115
+ assert seen is not None
83
116
  return _freeze_iterable(value, tag, seen)
84
117
 
85
118
 
86
119
  def _register_iterable(cls: type, tag: str) -> None:
87
- _freeze.register(cls)(_check_cycle(partial(_freeze_iterable_with_tag, tag=tag)))
120
+ handler = _check_cycle(partial(_freeze_iterable_with_tag, tag=tag))
121
+ _freeze.register(cls)(
122
+ cast(Callable[[Any, set[int] | None], FrozenSnapshot], handler)
123
+ )
88
124
 
89
125
 
90
126
  for _cls, _tag in (
@@ -98,17 +134,22 @@ for _cls, _tag in (
98
134
 
99
135
  @_freeze.register(Mapping)
100
136
  @_check_cycle
101
- def _freeze_mapping(value: Mapping, seen: set[int] | None = None): # noqa: F401
137
+ def _freeze_mapping(
138
+ value: Mapping[Any, Any], seen: set[int] | None = None
139
+ ) -> FrozenTaggedMapping: # noqa: F401
140
+ assert seen is not None
102
141
  tag = "dict" if hasattr(value, "__setitem__") else "mapping"
103
142
  return (tag, tuple((k, _freeze(v, seen)) for k, v in value.items()))
104
143
 
105
144
 
106
- def _all_immutable(iterable) -> bool:
145
+ def _all_immutable(iterable: Iterable[Any]) -> bool:
107
146
  return all(_is_immutable_inner(v) for v in iterable)
108
147
 
109
148
 
110
149
  # Dispatch table kept immutable to avoid accidental mutation.
111
- _IMMUTABLE_TAG_DISPATCH: Mapping[str, Callable[[tuple], bool]] = MappingProxyType(
150
+ ImmutableTagHandler: TypeAlias = Callable[[tuple[Any, ...]], bool]
151
+
152
+ _IMMUTABLE_TAG_DISPATCH: Mapping[str, ImmutableTagHandler] = MappingProxyType(
112
153
  {
113
154
  "mapping": lambda v: _all_immutable(v[1]),
114
155
  "frozenset": lambda v: _all_immutable(v[1]),
@@ -123,11 +164,13 @@ _IMMUTABLE_TAG_DISPATCH: Mapping[str, Callable[[tuple], bool]] = MappingProxyTyp
123
164
  @lru_cache(maxsize=1024)
124
165
  @singledispatch
125
166
  def _is_immutable_inner(value: Any) -> bool:
167
+ """Return ``True`` when ``value`` belongs to the canonical immutable set."""
168
+
126
169
  return type(value) in IMMUTABLE_SIMPLE
127
170
 
128
171
 
129
172
  @_is_immutable_inner.register(tuple)
130
- def _is_immutable_inner_tuple(value: tuple) -> bool: # noqa: F401
173
+ def _is_immutable_inner_tuple(value: tuple[Any, ...]) -> bool: # noqa: F401
131
174
  if value and isinstance(value[0], str):
132
175
  handler = _IMMUTABLE_TAG_DISPATCH.get(value[0])
133
176
  if handler is not None:
@@ -136,13 +179,11 @@ def _is_immutable_inner_tuple(value: tuple) -> bool: # noqa: F401
136
179
 
137
180
 
138
181
  @_is_immutable_inner.register(frozenset)
139
- def _is_immutable_inner_frozenset(value: frozenset) -> bool: # noqa: F401
182
+ def _is_immutable_inner_frozenset(value: frozenset[Any]) -> bool: # noqa: F401
140
183
  return _all_immutable(value)
141
184
 
142
185
 
143
- _IMMUTABLE_CACHE: weakref.WeakKeyDictionary[Any, bool] = (
144
- weakref.WeakKeyDictionary()
145
- )
186
+ _IMMUTABLE_CACHE: weakref.WeakKeyDictionary[Any, bool] = weakref.WeakKeyDictionary()
146
187
  _IMMUTABLE_CACHE_LOCK = threading.Lock()
147
188
 
148
189
 
tnfr/immutable.pyi ADDED
@@ -0,0 +1,34 @@
1
+ from typing import Any, Callable, Iterator, Mapping
2
+
3
+ from ._compat import TypeAlias
4
+
5
+ FrozenPrimitive: TypeAlias = int | float | complex | str | bool | bytes | None
6
+ FrozenCollectionItems: TypeAlias = tuple["FrozenSnapshot", ...]
7
+ FrozenMappingItems: TypeAlias = tuple[tuple[Any, "FrozenSnapshot"], ...]
8
+ FrozenTaggedCollection: TypeAlias = tuple[str, FrozenCollectionItems]
9
+ FrozenTaggedMapping: TypeAlias = tuple[str, FrozenMappingItems]
10
+ FrozenSnapshot: TypeAlias = (
11
+ FrozenPrimitive
12
+ | FrozenCollectionItems
13
+ | FrozenTaggedCollection
14
+ | FrozenTaggedMapping
15
+ )
16
+ ImmutableTagHandler: TypeAlias = Callable[[tuple[Any, ...]], bool]
17
+
18
+ __all__: tuple[str, ...]
19
+
20
+ def __getattr__(name: str) -> Any: ...
21
+ def _cycle_guard(value: Any, seen: set[int] | None = ...) -> Iterator[set[int]]: ...
22
+ def _check_cycle(
23
+ func: Callable[[Any, set[int] | None], FrozenSnapshot],
24
+ ) -> Callable[[Any, set[int] | None], FrozenSnapshot]: ...
25
+ def _freeze(value: Any, seen: set[int] | None = ...) -> FrozenSnapshot: ...
26
+ def _freeze_mapping(
27
+ value: Mapping[Any, Any],
28
+ seen: set[int] | None = ...,
29
+ ) -> FrozenTaggedMapping: ...
30
+ def _is_immutable(value: Any) -> bool: ...
31
+ def _is_immutable_inner(value: Any) -> bool: ...
32
+
33
+ _IMMUTABLE_CACHE: Any
34
+ _IMMUTABLE_TAG_DISPATCH: Mapping[str, ImmutableTagHandler]