tnfr 6.0.0__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 (176) hide show
  1. tnfr/__init__.py +50 -5
  2. tnfr/__init__.pyi +0 -7
  3. tnfr/_compat.py +0 -1
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +44 -2
  6. tnfr/alias.py +14 -13
  7. tnfr/alias.pyi +5 -37
  8. tnfr/cache.py +9 -729
  9. tnfr/cache.pyi +8 -224
  10. tnfr/callback_utils.py +16 -31
  11. tnfr/callback_utils.pyi +3 -29
  12. tnfr/cli/__init__.py +17 -11
  13. tnfr/cli/__init__.pyi +0 -21
  14. tnfr/cli/arguments.py +175 -14
  15. tnfr/cli/arguments.pyi +5 -11
  16. tnfr/cli/execution.py +434 -48
  17. tnfr/cli/execution.pyi +14 -24
  18. tnfr/cli/utils.py +20 -3
  19. tnfr/cli/utils.pyi +5 -5
  20. tnfr/config/__init__.py +2 -1
  21. tnfr/config/__init__.pyi +2 -0
  22. tnfr/config/feature_flags.py +83 -0
  23. tnfr/config/init.py +1 -1
  24. tnfr/config/operator_names.py +1 -14
  25. tnfr/config/presets.py +6 -26
  26. tnfr/constants/__init__.py +10 -13
  27. tnfr/constants/__init__.pyi +10 -22
  28. tnfr/constants/aliases.py +31 -0
  29. tnfr/constants/core.py +4 -3
  30. tnfr/constants/init.py +1 -1
  31. tnfr/constants/metric.py +3 -3
  32. tnfr/dynamics/__init__.py +64 -10
  33. tnfr/dynamics/__init__.pyi +3 -4
  34. tnfr/dynamics/adaptation.py +79 -13
  35. tnfr/dynamics/aliases.py +10 -9
  36. tnfr/dynamics/coordination.py +77 -35
  37. tnfr/dynamics/dnfr.py +575 -274
  38. tnfr/dynamics/dnfr.pyi +1 -10
  39. tnfr/dynamics/integrators.py +47 -33
  40. tnfr/dynamics/integrators.pyi +0 -1
  41. tnfr/dynamics/runtime.py +489 -129
  42. tnfr/dynamics/sampling.py +2 -0
  43. tnfr/dynamics/selectors.py +101 -62
  44. tnfr/execution.py +15 -8
  45. tnfr/execution.pyi +5 -25
  46. tnfr/flatten.py +7 -3
  47. tnfr/flatten.pyi +1 -8
  48. tnfr/gamma.py +22 -26
  49. tnfr/gamma.pyi +0 -6
  50. tnfr/glyph_history.py +37 -26
  51. tnfr/glyph_history.pyi +1 -19
  52. tnfr/glyph_runtime.py +16 -0
  53. tnfr/glyph_runtime.pyi +9 -0
  54. tnfr/immutable.py +20 -15
  55. tnfr/immutable.pyi +4 -7
  56. tnfr/initialization.py +5 -7
  57. tnfr/initialization.pyi +1 -9
  58. tnfr/io.py +6 -305
  59. tnfr/io.pyi +13 -8
  60. tnfr/mathematics/__init__.py +81 -0
  61. tnfr/mathematics/backend.py +426 -0
  62. tnfr/mathematics/dynamics.py +398 -0
  63. tnfr/mathematics/epi.py +254 -0
  64. tnfr/mathematics/generators.py +222 -0
  65. tnfr/mathematics/metrics.py +119 -0
  66. tnfr/mathematics/operators.py +233 -0
  67. tnfr/mathematics/operators_factory.py +71 -0
  68. tnfr/mathematics/projection.py +78 -0
  69. tnfr/mathematics/runtime.py +173 -0
  70. tnfr/mathematics/spaces.py +247 -0
  71. tnfr/mathematics/transforms.py +292 -0
  72. tnfr/metrics/__init__.py +10 -10
  73. tnfr/metrics/coherence.py +123 -94
  74. tnfr/metrics/common.py +22 -13
  75. tnfr/metrics/common.pyi +42 -11
  76. tnfr/metrics/core.py +72 -14
  77. tnfr/metrics/diagnosis.py +48 -57
  78. tnfr/metrics/diagnosis.pyi +3 -7
  79. tnfr/metrics/export.py +3 -5
  80. tnfr/metrics/glyph_timing.py +41 -31
  81. tnfr/metrics/reporting.py +13 -6
  82. tnfr/metrics/sense_index.py +884 -114
  83. tnfr/metrics/trig.py +167 -11
  84. tnfr/metrics/trig.pyi +1 -0
  85. tnfr/metrics/trig_cache.py +112 -15
  86. tnfr/node.py +400 -17
  87. tnfr/node.pyi +55 -38
  88. tnfr/observers.py +111 -8
  89. tnfr/observers.pyi +0 -15
  90. tnfr/ontosim.py +9 -6
  91. tnfr/ontosim.pyi +0 -5
  92. tnfr/operators/__init__.py +529 -42
  93. tnfr/operators/__init__.pyi +14 -0
  94. tnfr/operators/definitions.py +350 -18
  95. tnfr/operators/definitions.pyi +0 -14
  96. tnfr/operators/grammar.py +760 -0
  97. tnfr/operators/jitter.py +28 -22
  98. tnfr/operators/registry.py +7 -12
  99. tnfr/operators/registry.pyi +0 -2
  100. tnfr/operators/remesh.py +38 -61
  101. tnfr/rng.py +17 -300
  102. tnfr/schemas/__init__.py +8 -0
  103. tnfr/schemas/grammar.json +94 -0
  104. tnfr/selector.py +3 -4
  105. tnfr/selector.pyi +1 -1
  106. tnfr/sense.py +22 -24
  107. tnfr/sense.pyi +0 -7
  108. tnfr/structural.py +504 -21
  109. tnfr/structural.pyi +41 -18
  110. tnfr/telemetry/__init__.py +23 -1
  111. tnfr/telemetry/cache_metrics.py +226 -0
  112. tnfr/telemetry/nu_f.py +423 -0
  113. tnfr/telemetry/nu_f.pyi +123 -0
  114. tnfr/tokens.py +1 -4
  115. tnfr/tokens.pyi +1 -6
  116. tnfr/trace.py +20 -53
  117. tnfr/trace.pyi +9 -37
  118. tnfr/types.py +244 -15
  119. tnfr/types.pyi +200 -14
  120. tnfr/units.py +69 -0
  121. tnfr/units.pyi +16 -0
  122. tnfr/utils/__init__.py +107 -48
  123. tnfr/utils/__init__.pyi +80 -11
  124. tnfr/utils/cache.py +1705 -65
  125. tnfr/utils/cache.pyi +370 -58
  126. tnfr/utils/chunks.py +104 -0
  127. tnfr/utils/chunks.pyi +21 -0
  128. tnfr/utils/data.py +95 -5
  129. tnfr/utils/data.pyi +8 -17
  130. tnfr/utils/graph.py +2 -4
  131. tnfr/utils/init.py +31 -7
  132. tnfr/utils/init.pyi +4 -11
  133. tnfr/utils/io.py +313 -14
  134. tnfr/{helpers → utils}/numeric.py +50 -24
  135. tnfr/utils/numeric.pyi +21 -0
  136. tnfr/validation/__init__.py +92 -4
  137. tnfr/validation/__init__.pyi +77 -17
  138. tnfr/validation/compatibility.py +79 -43
  139. tnfr/validation/compatibility.pyi +4 -6
  140. tnfr/validation/grammar.py +55 -133
  141. tnfr/validation/grammar.pyi +37 -8
  142. tnfr/validation/graph.py +138 -0
  143. tnfr/validation/graph.pyi +17 -0
  144. tnfr/validation/rules.py +161 -74
  145. tnfr/validation/rules.pyi +55 -18
  146. tnfr/validation/runtime.py +263 -0
  147. tnfr/validation/runtime.pyi +31 -0
  148. tnfr/validation/soft_filters.py +170 -0
  149. tnfr/validation/soft_filters.pyi +37 -0
  150. tnfr/validation/spectral.py +159 -0
  151. tnfr/validation/spectral.pyi +46 -0
  152. tnfr/validation/syntax.py +28 -139
  153. tnfr/validation/syntax.pyi +7 -4
  154. tnfr/validation/window.py +39 -0
  155. tnfr/validation/window.pyi +1 -0
  156. tnfr/viz/__init__.py +9 -0
  157. tnfr/viz/matplotlib.py +246 -0
  158. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/METADATA +63 -19
  159. tnfr-7.0.0.dist-info/RECORD +185 -0
  160. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
  161. tnfr/constants_glyphs.py +0 -16
  162. tnfr/constants_glyphs.pyi +0 -12
  163. tnfr/grammar.py +0 -25
  164. tnfr/grammar.pyi +0 -13
  165. tnfr/helpers/__init__.py +0 -151
  166. tnfr/helpers/__init__.pyi +0 -66
  167. tnfr/helpers/numeric.pyi +0 -12
  168. tnfr/presets.py +0 -15
  169. tnfr/presets.pyi +0 -7
  170. tnfr/utils/io.pyi +0 -10
  171. tnfr/utils/validators.py +0 -130
  172. tnfr/utils/validators.pyi +0 -19
  173. tnfr-6.0.0.dist-info/RECORD +0 -157
  174. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
  175. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
  176. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
tnfr/gamma.py CHANGED
@@ -1,26 +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
13
13
  from .alias import get_theta_attr
14
+ from .constants import DEFAULTS
15
+ from .utils import json_dumps
16
+ from .metrics.trig_cache import get_trig_cache
14
17
  from .types import GammaSpec, NodeId, TNFRGraph
15
18
  from .utils import (
16
19
  edge_version_cache,
17
20
  get_graph_mapping,
18
21
  get_logger,
19
- json_dumps,
20
22
  node_set_checksum,
21
23
  )
22
- from .metrics.trig_cache import get_trig_cache
23
-
24
24
 
25
25
  logger = get_logger(__name__)
26
26
 
@@ -47,8 +47,7 @@ def _default_gamma_spec() -> tuple[bytes, str]:
47
47
 
48
48
 
49
49
  def _ensure_kuramoto_cache(G: TNFRGraph, t: float | int) -> None:
50
- """Cache ``(R, ψ)`` for the current step ``t`` using
51
- ``edge_version_cache``."""
50
+ """Cache ``(R, ψ)`` for the current step ``t`` using ``edge_version_cache``."""
52
51
  checksum = G.graph.get("_dnfr_nodes_checksum")
53
52
  if checksum is None:
54
53
  # reuse checksum from cached_nodes_and_A when available
@@ -172,9 +171,7 @@ def _get_gamma_spec(G: TNFRGraph) -> GammaSpec:
172
171
  # -----------------
173
172
 
174
173
 
175
- def _gamma_params(
176
- cfg: GammaSpec, **defaults: float
177
- ) -> tuple[float, ...]:
174
+ def _gamma_params(cfg: GammaSpec, **defaults: float) -> tuple[float, ...]:
178
175
  """Return normalized Γ parameters from ``cfg``.
179
176
 
180
177
  Parameters are retrieved from ``cfg`` using the keys in ``defaults`` and
@@ -186,9 +183,7 @@ def _gamma_params(
186
183
  >>> beta, R0 = _gamma_params(cfg, beta=0.0, R0=0.0)
187
184
  """
188
185
 
189
- return tuple(
190
- float(cfg.get(name, default)) for name, default in defaults.items()
191
- )
186
+ return tuple(float(cfg.get(name, default)) for name, default in defaults.items())
192
187
 
193
188
 
194
189
  # -----------------
@@ -196,9 +191,9 @@ def _gamma_params(
196
191
  # -----------------
197
192
 
198
193
 
199
- def gamma_none(
200
- G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec
201
- ) -> 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
+
202
197
  return 0.0
203
198
 
204
199
 
@@ -209,7 +204,7 @@ def _gamma_kuramoto(
209
204
  builder: Callable[..., float],
210
205
  **defaults: float,
211
206
  ) -> float:
212
- """Helper for Kuramoto-based Γ functions.
207
+ """Construct a Kuramoto-based Γ function.
213
208
 
214
209
  ``builder`` receives ``(θ_i, R, ψ, *params)`` where ``params`` are
215
210
  extracted from ``cfg`` according to ``defaults``.
@@ -229,7 +224,9 @@ def _builder_bandpass(th_i: float, R: float, psi: float, beta: float) -> float:
229
224
  return beta * R * (1.0 - R) * sgn
230
225
 
231
226
 
232
- 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:
233
230
  return beta * math.tanh(k * (R - R0)) * math.cos(th_i - psi)
234
231
 
235
232
 
@@ -253,7 +250,7 @@ def gamma_kuramoto_linear(
253
250
  def gamma_kuramoto_bandpass(
254
251
  G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec
255
252
  ) -> float:
256
- """Γ = β · R(1-R) · sign(cos(θ_i - ψ))"""
253
+ """Compute Γ = β · R(1-R) · sign(cos(θ_i - ψ))."""
257
254
 
258
255
  return _gamma_kuramoto(G, node, cfg, _builder_bandpass, beta=0.0)
259
256
 
@@ -272,9 +269,7 @@ def gamma_kuramoto_tanh(
272
269
  return _gamma_kuramoto(G, node, cfg, _builder_tanh, beta=0.0, k=1.0, R0=0.0)
273
270
 
274
271
 
275
- def gamma_harmonic(
276
- G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec
277
- ) -> float:
272
+ def gamma_harmonic(G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec) -> float:
278
273
  """Harmonic forcing aligned with the global phase field.
279
274
 
280
275
  Formula: Γ = β · sin(ω·t + φ) · cos(θ_i - ψ)
@@ -288,6 +283,8 @@ def gamma_harmonic(
288
283
 
289
284
 
290
285
  class GammaEntry(NamedTuple):
286
+ """Lookup entry linking Γ evaluators with their preconditions."""
287
+
291
288
  fn: Callable[[TNFRGraph, NodeId, float | int, GammaSpec], float]
292
289
  needs_kuramoto: bool
293
290
 
@@ -312,8 +309,7 @@ def eval_gamma(
312
309
  strict: bool = False,
313
310
  log_level: int | None = None,
314
311
  ) -> float:
315
- """Evaluate Γi for ``node`` according to ``G.graph['GAMMA']``
316
- specification.
312
+ """Evaluate Γi for ``node`` using ``G.graph['GAMMA']`` specification.
317
313
 
318
314
  If ``strict`` is ``True`` exceptions raised during evaluation are
319
315
  propagated instead of returning ``0.0``. Likewise, if the specified
tnfr/gamma.pyi CHANGED
@@ -11,25 +11,19 @@ class GammaEntry(NamedTuple):
11
11
  GAMMA_REGISTRY: dict[str, GammaEntry]
12
12
 
13
13
  def kuramoto_R_psi(G: TNFRGraph) -> tuple[float, float]: ...
14
-
15
14
  def gamma_none(G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec) -> float: ...
16
-
17
15
  def gamma_kuramoto_linear(
18
16
  G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec
19
17
  ) -> float: ...
20
-
21
18
  def gamma_kuramoto_bandpass(
22
19
  G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec
23
20
  ) -> float: ...
24
-
25
21
  def gamma_kuramoto_tanh(
26
22
  G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec
27
23
  ) -> float: ...
28
-
29
24
  def gamma_harmonic(
30
25
  G: TNFRGraph, node: NodeId, t: float | int, cfg: GammaSpec
31
26
  ) -> float: ...
32
-
33
27
  def eval_gamma(
34
28
  G: TNFRGraph,
35
29
  node: NodeId,
tnfr/glyph_history.py CHANGED
@@ -2,14 +2,15 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, cast
6
- from collections import deque, Counter
7
- from itertools import islice
5
+ from collections import Counter, deque
8
6
  from collections.abc import Iterable, Mapping, MutableMapping
7
+ from itertools import islice
8
+ from typing import Any, cast
9
9
 
10
10
  from .constants import get_param, normalise_state_token
11
- from .utils import ensure_collection, get_logger, validate_window
11
+ from .glyph_runtime import last_glyph
12
12
  from .types import TNFRGraph
13
+ from .utils import ensure_collection, get_logger
13
14
 
14
15
  logger = get_logger(__name__)
15
16
 
@@ -20,16 +21,27 @@ __all__ = (
20
21
  "ensure_history",
21
22
  "current_step_idx",
22
23
  "append_metric",
23
- "last_glyph",
24
24
  "count_glyphs",
25
25
  )
26
26
 
27
27
 
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
+ )
36
+
37
+
28
38
  def _ensure_history(
29
39
  nd: MutableMapping[str, Any], window: int, *, create_zero: bool = False
30
40
  ) -> tuple[int, deque[str] | None]:
31
41
  """Validate ``window`` and ensure ``nd['glyph_history']`` deque."""
32
42
 
43
+ from tnfr.validation.window import validate_window
44
+
33
45
  v_window = validate_window(window)
34
46
  if v_window == 0 and not create_zero:
35
47
  return v_window, None
@@ -42,9 +54,7 @@ def _ensure_history(
42
54
  try:
43
55
  items = ensure_collection(hist, max_materialize=None)
44
56
  except TypeError:
45
- logger.debug(
46
- "Discarding non-iterable glyph history value %r", hist
47
- )
57
+ logger.debug("Discarding non-iterable glyph history value %r", hist)
48
58
  items = ()
49
59
  hist = deque((str(item) for item in items), maxlen=v_window)
50
60
  nd["glyph_history"] = hist
@@ -62,9 +72,7 @@ def push_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> None:
62
72
  hist.append(str(glyph))
63
73
 
64
74
 
65
- def recent_glyph(
66
- nd: MutableMapping[str, Any], glyph: str, window: int
67
- ) -> bool:
75
+ def recent_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> bool:
68
76
  """Return ``True`` if ``glyph`` appeared in last ``window`` emissions.
69
77
 
70
78
  ``window`` validation and deque creation are handled by
@@ -145,26 +153,36 @@ class HistoryDict(dict[str, Any]):
145
153
  return val
146
154
 
147
155
  def get_increment(self, key: str, default: Any = None) -> Any:
156
+ """Return value for ``key`` and increment its usage counter."""
157
+
148
158
  insert = key not in self
149
159
  val = self._resolve_value(key, default, insert=insert)
150
160
  self._increment(key)
151
161
  return val
152
162
 
153
163
  def __getitem__(self, key: str) -> Any: # type: ignore[override]
164
+ """Return the tracked value for ``key`` ensuring deque normalisation."""
165
+
154
166
  return self._resolve_value(key, None, insert=False)
155
167
 
156
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
+
157
171
  try:
158
172
  return self._resolve_value(key, None, insert=False)
159
173
  except KeyError:
160
174
  return default
161
175
 
162
176
  def __setitem__(self, key: str, value: Any) -> None: # type: ignore[override]
177
+ """Store ``value`` for ``key`` while initialising usage tracking."""
178
+
163
179
  super().__setitem__(key, value)
164
180
  if key not in self._counts:
165
181
  self._counts[key] = 0
166
182
 
167
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
+
168
186
  insert = key not in self
169
187
  val = self._resolve_value(key, default, insert=insert)
170
188
  if insert:
@@ -181,6 +199,8 @@ class HistoryDict(dict[str, Any]):
181
199
  raise KeyError("HistoryDict is empty; cannot pop least used")
182
200
 
183
201
  def pop_least_used_batch(self, k: int) -> None:
202
+ """Remove up to ``k`` least-used entries from the history."""
203
+
184
204
  for _ in range(max(0, int(k))):
185
205
  try:
186
206
  self.pop_least_used()
@@ -213,10 +233,7 @@ def ensure_history(G: TNFRGraph) -> HistoryDict | dict[str, Any]:
213
233
  if isinstance(hist, MutableMapping):
214
234
  _normalise_state_streams(hist)
215
235
  return hist
216
- if (
217
- not isinstance(hist, HistoryDict)
218
- or hist._maxlen != maxlen
219
- ):
236
+ if not isinstance(hist, HistoryDict) or hist._maxlen != maxlen:
220
237
  hist = HistoryDict(hist, maxlen=maxlen)
221
238
  G.graph["history"] = hist
222
239
  replaced = True
@@ -226,6 +243,8 @@ def ensure_history(G: TNFRGraph) -> HistoryDict | dict[str, Any]:
226
243
  if replaced:
227
244
  G.graph.pop(sentinel_key, None)
228
245
  _normalise_state_streams(cast(MutableMapping[str, Any], hist))
246
+ for key in _NU_F_HISTORY_KEYS:
247
+ hist.setdefault(key, [])
229
248
  return hist
230
249
 
231
250
 
@@ -236,9 +255,7 @@ def current_step_idx(G: TNFRGraph | Mapping[str, Any]) -> int:
236
255
  return len(graph.get("history", {}).get("C_steps", []))
237
256
 
238
257
 
239
- def append_metric(
240
- hist: MutableMapping[str, list[Any]], key: str, value: Any
241
- ) -> None:
258
+ def append_metric(hist: MutableMapping[str, list[Any]], key: str, value: Any) -> None:
242
259
  """Append ``value`` to ``hist[key]`` list, creating it if missing."""
243
260
  if key == "phase_state" and isinstance(value, str):
244
261
  value = normalise_state_token(value)
@@ -260,14 +277,6 @@ def append_metric(
260
277
  return
261
278
 
262
279
  hist.setdefault(key, []).append(value)
263
-
264
-
265
- def last_glyph(nd: Mapping[str, Any]) -> str | None:
266
- """Return the most recent glyph for node or ``None``."""
267
- hist = nd.get("glyph_history")
268
- return hist[-1] if hist else None
269
-
270
-
271
280
  def count_glyphs(
272
281
  G: TNFRGraph, window: int | None = None, *, last_only: bool = False
273
282
  ) -> Counter[str]:
@@ -279,6 +288,8 @@ def count_glyphs(
279
288
  """
280
289
 
281
290
  if window is not None:
291
+ from tnfr.validation.window import validate_window
292
+
282
293
  window = validate_window(window)
283
294
  if window == 0:
284
295
  return Counter()
tnfr/glyph_history.pyi CHANGED
@@ -8,7 +8,6 @@ from .types import TNFRGraph
8
8
 
9
9
  __all__: tuple[str, ...]
10
10
 
11
-
12
11
  class HistoryDict(dict[str, Any]):
13
12
  _maxlen: int
14
13
  _counts: Counter[str]
@@ -16,7 +15,6 @@ class HistoryDict(dict[str, Any]):
16
15
  def __init__(
17
16
  self, data: Mapping[str, Any] | None = ..., *, maxlen: int = ...
18
17
  ) -> None: ...
19
-
20
18
  def get_increment(self, key: str, default: Any = ...) -> Any: ...
21
19
  def __getitem__(self, key: str) -> Any: ...
22
20
  def get(self, key: str, default: Any | None = ...) -> Any: ...
@@ -25,29 +23,13 @@ class HistoryDict(dict[str, Any]):
25
23
  def pop_least_used(self) -> Any: ...
26
24
  def pop_least_used_batch(self, k: int) -> None: ...
27
25
 
28
-
29
26
  def push_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> None: ...
30
-
31
-
32
- def recent_glyph(
33
- nd: MutableMapping[str, Any], glyph: str, window: int
34
- ) -> bool: ...
35
-
36
-
27
+ def recent_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> bool: ...
37
28
  def ensure_history(G: TNFRGraph) -> HistoryDict | dict[str, Any]: ...
38
-
39
-
40
29
  def current_step_idx(G: TNFRGraph | Mapping[str, Any]) -> int: ...
41
-
42
-
43
30
  def append_metric(
44
31
  hist: MutableMapping[str, list[Any]], key: str, value: Any
45
32
  ) -> None: ...
46
-
47
-
48
- def last_glyph(nd: Mapping[str, Any]) -> str | None: ...
49
-
50
-
51
33
  def count_glyphs(
52
34
  G: TNFRGraph, window: int | None = ..., *, last_only: bool = ...
53
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,21 +7,19 @@ 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
15
  from functools import lru_cache, partial, singledispatch, wraps
13
- from typing import Any, Callable, Iterable, Iterator, cast
14
- from collections.abc import Mapping
15
16
  from types import MappingProxyType
16
- import threading
17
- import weakref
17
+ from typing import Any, Callable, Iterable, Iterator, cast
18
18
 
19
19
  from ._compat import TypeAlias
20
20
 
21
21
  # Types considered immutable without further inspection
22
- IMMUTABLE_SIMPLE = frozenset(
23
- {int, float, complex, str, bool, bytes, type(None)}
24
- )
22
+ IMMUTABLE_SIMPLE = frozenset({int, float, complex, str, bool, bytes, type(None)})
25
23
 
26
24
 
27
25
  FrozenPrimitive: TypeAlias = int | float | complex | str | bool | bytes | None
@@ -40,7 +38,10 @@ FrozenTaggedMapping: TypeAlias = tuple[str, FrozenMappingItems]
40
38
  """Tagged mapping snapshot identifying the original mapping flavour."""
41
39
 
42
40
  FrozenSnapshot: TypeAlias = (
43
- FrozenPrimitive | FrozenCollectionItems | FrozenTaggedCollection | FrozenTaggedMapping
41
+ FrozenPrimitive
42
+ | FrozenCollectionItems
43
+ | FrozenTaggedCollection
44
+ | FrozenTaggedMapping
44
45
  )
45
46
  """Union describing the immutable snapshot returned by :func:`_freeze`."""
46
47
 
@@ -61,9 +62,9 @@ def _cycle_guard(value: Any, seen: set[int] | None = None) -> Iterator[set[int]]
61
62
 
62
63
 
63
64
  def _check_cycle(
64
- func: Callable[[Any, set[int] | None], FrozenSnapshot]
65
+ func: Callable[[Any, set[int] | None], FrozenSnapshot],
65
66
  ) -> Callable[[Any, set[int] | None], FrozenSnapshot]:
66
- """Decorator applying :func:`_cycle_guard` to ``func``."""
67
+ """Apply :func:`_cycle_guard` to ``func``."""
67
68
 
68
69
  @wraps(func)
69
70
  def wrapper(value: Any, seen: set[int] | None = None) -> FrozenSnapshot:
@@ -95,7 +96,9 @@ def _freeze(value: Any, seen: set[int] | None = None) -> FrozenSnapshot:
95
96
 
96
97
  @_freeze.register(tuple)
97
98
  @_check_cycle
98
- def _freeze_tuple(value: tuple[Any, ...], seen: set[int] | None = None) -> FrozenCollectionItems: # noqa: F401
99
+ def _freeze_tuple(
100
+ value: tuple[Any, ...], seen: set[int] | None = None
101
+ ) -> FrozenCollectionItems: # noqa: F401
99
102
  assert seen is not None
100
103
  return tuple(_freeze(v, seen) for v in value)
101
104
 
@@ -115,7 +118,9 @@ def _freeze_iterable_with_tag(
115
118
 
116
119
  def _register_iterable(cls: type, tag: str) -> None:
117
120
  handler = _check_cycle(partial(_freeze_iterable_with_tag, tag=tag))
118
- _freeze.register(cls)(cast(Callable[[Any, set[int] | None], FrozenSnapshot], handler))
121
+ _freeze.register(cls)(
122
+ cast(Callable[[Any, set[int] | None], FrozenSnapshot], handler)
123
+ )
119
124
 
120
125
 
121
126
  for _cls, _tag in (
@@ -159,6 +164,8 @@ _IMMUTABLE_TAG_DISPATCH: Mapping[str, ImmutableTagHandler] = MappingProxyType(
159
164
  @lru_cache(maxsize=1024)
160
165
  @singledispatch
161
166
  def _is_immutable_inner(value: Any) -> bool:
167
+ """Return ``True`` when ``value`` belongs to the canonical immutable set."""
168
+
162
169
  return type(value) in IMMUTABLE_SIMPLE
163
170
 
164
171
 
@@ -176,9 +183,7 @@ def _is_immutable_inner_frozenset(value: frozenset[Any]) -> bool: # noqa: F401
176
183
  return _all_immutable(value)
177
184
 
178
185
 
179
- _IMMUTABLE_CACHE: weakref.WeakKeyDictionary[Any, bool] = (
180
- weakref.WeakKeyDictionary()
181
- )
186
+ _IMMUTABLE_CACHE: weakref.WeakKeyDictionary[Any, bool] = weakref.WeakKeyDictionary()
182
187
  _IMMUTABLE_CACHE_LOCK = threading.Lock()
183
188
 
184
189
 
tnfr/immutable.pyi CHANGED
@@ -8,29 +8,26 @@ FrozenMappingItems: TypeAlias = tuple[tuple[Any, "FrozenSnapshot"], ...]
8
8
  FrozenTaggedCollection: TypeAlias = tuple[str, FrozenCollectionItems]
9
9
  FrozenTaggedMapping: TypeAlias = tuple[str, FrozenMappingItems]
10
10
  FrozenSnapshot: TypeAlias = (
11
- FrozenPrimitive | FrozenCollectionItems | FrozenTaggedCollection | FrozenTaggedMapping
11
+ FrozenPrimitive
12
+ | FrozenCollectionItems
13
+ | FrozenTaggedCollection
14
+ | FrozenTaggedMapping
12
15
  )
13
16
  ImmutableTagHandler: TypeAlias = Callable[[tuple[Any, ...]], bool]
14
17
 
15
18
  __all__: tuple[str, ...]
16
19
 
17
20
  def __getattr__(name: str) -> Any: ...
18
-
19
21
  def _cycle_guard(value: Any, seen: set[int] | None = ...) -> Iterator[set[int]]: ...
20
-
21
22
  def _check_cycle(
22
23
  func: Callable[[Any, set[int] | None], FrozenSnapshot],
23
24
  ) -> Callable[[Any, set[int] | None], FrozenSnapshot]: ...
24
-
25
25
  def _freeze(value: Any, seen: set[int] | None = ...) -> FrozenSnapshot: ...
26
-
27
26
  def _freeze_mapping(
28
27
  value: Mapping[Any, Any],
29
28
  seen: set[int] | None = ...,
30
29
  ) -> FrozenTaggedMapping: ...
31
-
32
30
  def _is_immutable(value: Any) -> bool: ...
33
-
34
31
  def _is_immutable_inner(value: Any) -> bool: ...
35
32
 
36
33
  _IMMUTABLE_CACHE: Any
tnfr/initialization.py CHANGED
@@ -1,13 +1,13 @@
1
1
  """Node initialization."""
2
2
 
3
3
  from __future__ import annotations
4
- import random
5
- from typing import TYPE_CHECKING, cast
6
4
 
5
+ import random
7
6
  from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, cast
8
8
 
9
- from .constants import VF_KEY, THETA_KEY, get_graph_param
10
- from .helpers.numeric import clamp
9
+ from .constants import THETA_KEY, VF_KEY, get_graph_param
10
+ from .utils import clamp
11
11
  from .rng import make_rng
12
12
  from .types import NodeInitAttrMap
13
13
 
@@ -53,9 +53,7 @@ class InitParams:
53
53
  vf_uniform_max=get_graph_param(G, "INIT_VF_MAX"),
54
54
  vf_mean=get_graph_param(G, "INIT_VF_MEAN"),
55
55
  vf_std=get_graph_param(G, "INIT_VF_STD"),
56
- clamp_to_limits=get_graph_param(
57
- G, "INIT_VF_CLAMP_TO_LIMITS", bool
58
- ),
56
+ clamp_to_limits=get_graph_param(G, "INIT_VF_CLAMP_TO_LIMITS", bool),
59
57
  si_min=get_graph_param(G, "INIT_SI_MIN"),
60
58
  si_max=get_graph_param(G, "INIT_SI_MAX"),
61
59
  epi_val=get_graph_param(G, "INIT_EPI_VALUE"),
tnfr/initialization.pyi CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
4
3
  import random
4
+ from dataclasses import dataclass
5
5
 
6
6
  import networkx as nx
7
7
 
@@ -9,7 +9,6 @@ from .types import NodeInitAttrMap
9
9
 
10
10
  __all__: tuple[str, str] = ("InitParams", "init_node_attrs")
11
11
 
12
-
13
12
  @dataclass
14
13
  class InitParams:
15
14
  seed: int | None
@@ -31,7 +30,6 @@ class InitParams:
31
30
  @classmethod
32
31
  def from_graph(cls, G: nx.Graph) -> InitParams: ...
33
32
 
34
-
35
33
  def _init_phase(
36
34
  nd: NodeInitAttrMap,
37
35
  rng: random.Random,
@@ -41,8 +39,6 @@ def _init_phase(
41
39
  th_min: float,
42
40
  th_max: float,
43
41
  ) -> None: ...
44
-
45
-
46
42
  def _init_vf(
47
43
  nd: NodeInitAttrMap,
48
44
  rng: random.Random,
@@ -57,8 +53,6 @@ def _init_vf(
57
53
  vf_max_lim: float,
58
54
  clamp_to_limits: bool,
59
55
  ) -> None: ...
60
-
61
-
62
56
  def _init_si_epi(
63
57
  nd: NodeInitAttrMap,
64
58
  rng: random.Random,
@@ -68,6 +62,4 @@ def _init_si_epi(
68
62
  si_max: float,
69
63
  epi_val: float,
70
64
  ) -> None: ...
71
-
72
-
73
65
  def init_node_attrs(G: nx.Graph, *, override: bool = True) -> nx.Graph: ...