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/rng.py CHANGED
@@ -2,17 +2,22 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import random
6
5
  import hashlib
6
+ import random
7
7
  import struct
8
- from collections.abc import Iterator, MutableMapping
9
- from typing import Any, Generic, Hashable, TypeVar
8
+ from typing import Any, cast
10
9
 
10
+ from cachetools import cached # type: ignore[import-untyped]
11
11
 
12
- from cachetools import LRUCache, cached
13
12
  from .constants import DEFAULTS, get_param
14
- from .graph_utils import get_graph
15
13
  from .locking import get_lock
14
+ from .types import GraphLike, TNFRGraph
15
+ from .utils import (
16
+ ScopedCounterCache,
17
+ _SeedHashCache,
18
+ build_cache_manager,
19
+ get_graph,
20
+ )
16
21
 
17
22
  MASK64 = 0xFFFFFFFFFFFFFFFF
18
23
 
@@ -21,147 +26,43 @@ _DEFAULT_CACHE_MAXSIZE = int(DEFAULTS.get("JITTER_CACHE_SIZE", 128))
21
26
  _CACHE_MAXSIZE = _DEFAULT_CACHE_MAXSIZE
22
27
  _CACHE_LOCKED = False
23
28
 
24
- K = TypeVar("K", bound=Hashable)
25
-
26
-
27
- class _SeedHashCache(MutableMapping[tuple[int, int], int]):
28
- """Mutable mapping proxy exposing a configurable LRU cache."""
29
-
30
- def __init__(self, maxsize: int) -> None:
31
- self._maxsize = 0
32
- self._cache: LRUCache[tuple[int, int], int] | None = None
33
- self.configure(maxsize)
34
-
35
- def configure(self, maxsize: int) -> None:
36
- """Configure internal cache size, clearing previous entries."""
37
-
38
- self._maxsize = int(maxsize)
39
- if self._maxsize <= 0:
40
- self._cache = None
41
- else:
42
- self._cache = LRUCache(maxsize=self._maxsize)
43
-
44
- def __getitem__(self, key: tuple[int, int]) -> int:
45
- if self._cache is None:
46
- raise KeyError(key)
47
- return self._cache[key]
48
-
49
- def __setitem__(self, key: tuple[int, int], value: int) -> None:
50
- if self._cache is not None:
51
- self._cache[key] = value
52
-
53
- def __delitem__(self, key: tuple[int, int]) -> None:
54
- if self._cache is None:
55
- raise KeyError(key)
56
- del self._cache[key]
57
-
58
- def __iter__(self) -> Iterator[tuple[int, int]]:
59
- if self._cache is None:
60
- return iter(())
61
- return iter(self._cache)
62
-
63
- def __len__(self) -> int:
64
- if self._cache is None:
65
- return 0
66
- return len(self._cache)
67
-
68
- def clear(self) -> None: # type: ignore[override]
69
- if self._cache is not None:
70
- self._cache.clear()
71
-
72
- @property
73
- def maxsize(self) -> int:
74
- return self._maxsize
75
-
76
- @property
77
- def enabled(self) -> bool:
78
- return self._cache is not None
79
-
80
- @property
81
- def data(self) -> LRUCache[tuple[int, int], int] | None:
82
- """Expose the underlying cache for diagnostics/tests."""
83
-
84
- return self._cache
85
-
86
-
87
- class ScopedCounterCache(Generic[K]):
88
- """Thread-safe LRU cache storing monotonic counters by ``key``."""
89
-
90
- def __init__(self, name: str, max_entries: int) -> None:
91
- if max_entries < 0:
92
- raise ValueError("max_entries must be non-negative")
93
- self._lock = get_lock(name)
94
- self._max_entries = int(max_entries)
95
- self._cache: LRUCache[K, int] = LRUCache(maxsize=self._max_entries)
96
-
97
- @property
98
- def lock(self):
99
- """Return the lock guarding access to the underlying cache."""
100
-
101
- return self._lock
102
-
103
- @property
104
- def max_entries(self) -> int:
105
- """Return the configured maximum number of cached entries."""
106
29
 
107
- return self._max_entries
30
+ _RNG_CACHE_MANAGER = build_cache_manager(default_capacity=_DEFAULT_CACHE_MAXSIZE)
108
31
 
109
- @property
110
- def cache(self) -> LRUCache[K, int]:
111
- """Expose the underlying ``LRUCache`` for inspection."""
112
32
 
113
- return self._cache
114
-
115
- def configure(
116
- self, *, force: bool = False, max_entries: int | None = None
117
- ) -> None:
118
- """Resize or reset the cache keeping previous settings."""
119
-
120
- size = self._max_entries if max_entries is None else int(max_entries)
121
- if size < 0:
122
- raise ValueError("max_entries must be non-negative")
123
- with self._lock:
124
- if size != self._max_entries:
125
- self._max_entries = size
126
- force = True
127
- if force:
128
- self._cache = LRUCache(maxsize=self._max_entries)
129
-
130
- def clear(self) -> None:
131
- """Clear stored counters preserving ``max_entries``."""
132
-
133
- self.configure(force=True)
134
-
135
- def bump(self, key: K) -> int:
136
- """Return current counter for ``key`` and increment it atomically."""
33
+ _seed_hash_cache = _SeedHashCache(
34
+ manager=_RNG_CACHE_MANAGER,
35
+ default_maxsize=_DEFAULT_CACHE_MAXSIZE,
36
+ )
137
37
 
138
- with self._lock:
139
- value = int(self._cache.get(key, 0))
140
- self._cache[key] = value + 1
141
- return value
142
38
 
143
- def __len__(self) -> int:
144
- return len(self._cache)
39
+ def _compute_seed_hash(seed_int: int, key_int: int) -> int:
40
+ seed_bytes = struct.pack(
41
+ ">QQ",
42
+ seed_int & MASK64,
43
+ key_int & MASK64,
44
+ )
45
+ return int.from_bytes(hashlib.blake2b(seed_bytes, digest_size=8).digest(), "big")
145
46
 
146
47
 
147
- _seed_hash_cache = _SeedHashCache(_CACHE_MAXSIZE)
48
+ @cached(cache=_seed_hash_cache, lock=_RNG_LOCK)
49
+ def _cached_seed_hash(seed_int: int, key_int: int) -> int:
50
+ return _compute_seed_hash(seed_int, key_int)
148
51
 
149
52
 
150
- @cached(cache=_seed_hash_cache, lock=_RNG_LOCK)
151
53
  def seed_hash(seed_int: int, key_int: int) -> int:
152
54
  """Return a 64-bit hash derived from ``seed_int`` and ``key_int``."""
153
55
 
154
- seed_bytes = struct.pack(
155
- ">QQ",
156
- seed_int & MASK64,
157
- key_int & MASK64,
158
- )
159
- return int.from_bytes(
160
- hashlib.blake2b(seed_bytes, digest_size=8).digest(), "big"
161
- )
56
+ if _CACHE_MAXSIZE <= 0 or not _seed_hash_cache.enabled:
57
+ return _compute_seed_hash(seed_int, key_int)
58
+ return _cached_seed_hash(seed_int, key_int)
59
+
162
60
 
61
+ seed_hash.cache_clear = cast(Any, _cached_seed_hash).cache_clear # type: ignore[attr-defined]
62
+ seed_hash.cache = _seed_hash_cache # type: ignore[attr-defined]
163
63
 
164
- def _sync_cache_size(G: Any | None) -> None:
64
+
65
+ def _sync_cache_size(G: TNFRGraph | GraphLike | None) -> None:
165
66
  """Synchronise cache size with ``G`` when needed."""
166
67
 
167
68
  global _CACHE_MAXSIZE
@@ -169,12 +70,14 @@ def _sync_cache_size(G: Any | None) -> None:
169
70
  return
170
71
  size = get_cache_maxsize(G)
171
72
  with _RNG_LOCK:
172
- if size != _CACHE_MAXSIZE:
73
+ if size != _seed_hash_cache.maxsize:
173
74
  _seed_hash_cache.configure(size)
174
- _CACHE_MAXSIZE = size
75
+ _CACHE_MAXSIZE = _seed_hash_cache.maxsize
175
76
 
176
77
 
177
- def make_rng(seed: int, key: int, G: Any | None = None) -> random.Random:
78
+ def make_rng(
79
+ seed: int, key: int, G: TNFRGraph | GraphLike | None = None
80
+ ) -> random.Random:
178
81
  """Return a ``random.Random`` for ``seed`` and ``key``.
179
82
 
180
83
  When ``G`` is provided, ``JITTER_CACHE_SIZE`` is read from ``G`` and the
@@ -188,17 +91,17 @@ def make_rng(seed: int, key: int, G: Any | None = None) -> random.Random:
188
91
 
189
92
  def clear_rng_cache() -> None:
190
93
  """Clear cached seed hashes."""
191
- if _CACHE_MAXSIZE <= 0 or not _seed_hash_cache.enabled:
94
+ if _seed_hash_cache.maxsize <= 0 or not _seed_hash_cache.enabled:
192
95
  return
193
- seed_hash.cache_clear()
96
+ seed_hash.cache_clear() # type: ignore[attr-defined]
194
97
 
195
98
 
196
- def get_cache_maxsize(G: Any) -> int:
99
+ def get_cache_maxsize(G: TNFRGraph | GraphLike) -> int:
197
100
  """Return RNG cache maximum size for ``G``."""
198
101
  return int(get_param(G, "JITTER_CACHE_SIZE"))
199
102
 
200
103
 
201
- def cache_enabled(G: Any | None = None) -> bool:
104
+ def cache_enabled(G: TNFRGraph | GraphLike | None = None) -> bool:
202
105
  """Return ``True`` if RNG caching is enabled.
203
106
 
204
107
  When ``G`` is provided, the cache size is synchronised with
@@ -207,12 +110,12 @@ def cache_enabled(G: Any | None = None) -> bool:
207
110
  # Only synchronise the cache size with ``G`` when caching is enabled. This
208
111
  # preserves explicit calls to :func:`set_cache_maxsize(0)` which are used in
209
112
  # tests to temporarily disable caching regardless of graph defaults.
210
- if _CACHE_MAXSIZE > 0:
113
+ if _seed_hash_cache.maxsize > 0:
211
114
  _sync_cache_size(G)
212
- return _CACHE_MAXSIZE > 0
115
+ return _seed_hash_cache.maxsize > 0
213
116
 
214
117
 
215
- def base_seed(G: Any) -> int:
118
+ def base_seed(G: TNFRGraph | GraphLike) -> int:
216
119
  """Return base RNG seed stored in ``G.graph``."""
217
120
  graph = get_graph(G)
218
121
  return int(graph.get("RANDOM_SEED", 0))
@@ -238,7 +141,7 @@ def set_cache_maxsize(size: int) -> None:
238
141
  raise ValueError("size must be non-negative")
239
142
  with _RNG_LOCK:
240
143
  _seed_hash_cache.configure(new_size)
241
- _CACHE_MAXSIZE = new_size
144
+ _CACHE_MAXSIZE = _seed_hash_cache.maxsize
242
145
  _CACHE_LOCKED = new_size != _DEFAULT_CACHE_MAXSIZE
243
146
 
244
147
 
tnfr/rng.pyi ADDED
@@ -0,0 +1,14 @@
1
+ from typing import Any
2
+
3
+ __all__: Any
4
+
5
+ def __getattr__(name: str) -> Any: ...
6
+
7
+ ScopedCounterCache: Any
8
+ base_seed: Any
9
+ cache_enabled: Any
10
+ clear_rng_cache: Any
11
+ get_cache_maxsize: Any
12
+ make_rng: Any
13
+ seed_hash: Any
14
+ set_cache_maxsize: Any
@@ -0,0 +1,8 @@
1
+ """JSON schema resources bundled with the TNFR engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = ["package"]
6
+
7
+ # Namespace packages need at least one attribute for static analysers.
8
+ package = __name__
@@ -0,0 +1,94 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://tnfr.io/schemas/grammar.json",
4
+ "title": "TNFR Grammar Configuration",
5
+ "type": "object",
6
+ "definitions": {
7
+ "glyphCode": {
8
+ "description": "Canonical glyph identifier in uppercase notation.",
9
+ "type": "string",
10
+ "pattern": "^[A-Z][A-Z_]*$"
11
+ },
12
+ "probability": {
13
+ "description": "Threshold constrained to the [0, 1] interval.",
14
+ "type": "number",
15
+ "minimum": 0.0,
16
+ "maximum": 1.0
17
+ },
18
+ "cfg_soft": {
19
+ "type": "object",
20
+ "description": "Soft grammar preferences applied before canonical automaton rules.",
21
+ "properties": {
22
+ "window": {
23
+ "description": "History window inspected to avoid short-term glyph repetitions.",
24
+ "type": "integer",
25
+ "minimum": 0
26
+ },
27
+ "avoid_repeats": {
28
+ "description": "Glyph codes that should be substituted when repeated inside the window.",
29
+ "type": "array",
30
+ "items": { "$ref": "#/definitions/glyphCode" },
31
+ "uniqueItems": true
32
+ },
33
+ "fallbacks": {
34
+ "description": "Mapping of glyph codes to their explicit substitution when repetition is detected.",
35
+ "type": "object",
36
+ "additionalProperties": { "$ref": "#/definitions/glyphCode" }
37
+ },
38
+ "force_dnfr": {
39
+ "description": "Minimum |ΔNFR| normalised score that bypasses soft filtering.",
40
+ "$ref": "#/definitions/probability"
41
+ },
42
+ "force_accel": {
43
+ "description": "Minimum acceleration normalised score that bypasses soft filtering.",
44
+ "$ref": "#/definitions/probability"
45
+ }
46
+ },
47
+ "additionalProperties": true
48
+ },
49
+ "cfg_canon": {
50
+ "type": "object",
51
+ "description": "Canonical grammar thresholds enforced by the automaton.",
52
+ "properties": {
53
+ "enabled": {
54
+ "description": "Toggle canonical grammar enforcement during metric runs.",
55
+ "type": "boolean"
56
+ },
57
+ "zhir_requires_oz_window": {
58
+ "description": "Window requiring a DISSONANCE glyph before a MUTATION.",
59
+ "type": "integer",
60
+ "minimum": 0
61
+ },
62
+ "zhir_dnfr_min": {
63
+ "description": "Minimum normalised |ΔNFR| required to allow MUTATION without recent DISSONANCE.",
64
+ "type": "number",
65
+ "minimum": 0.0
66
+ },
67
+ "thol_min_len": {
68
+ "description": "Minimum number of THOL glyphs before canonical closure is allowed.",
69
+ "type": "integer",
70
+ "minimum": 0
71
+ },
72
+ "thol_max_len": {
73
+ "description": "Maximum number of THOL glyphs tolerated before forcing closure.",
74
+ "type": "integer",
75
+ "minimum": 0
76
+ },
77
+ "thol_close_dnfr": {
78
+ "description": "Upper bound on normalised |ΔNFR| that triggers THOL closure.",
79
+ "$ref": "#/definitions/probability"
80
+ },
81
+ "si_high": {
82
+ "description": "Sense index threshold: Si at or above this resolves THOL closures with silence; lower Si forces contraction.",
83
+ "$ref": "#/definitions/probability"
84
+ }
85
+ },
86
+ "additionalProperties": true
87
+ }
88
+ },
89
+ "properties": {
90
+ "cfg_soft": { "$ref": "#/definitions/cfg_soft" },
91
+ "cfg_canon": { "$ref": "#/definitions/cfg_canon" }
92
+ },
93
+ "additionalProperties": false
94
+ }
tnfr/selector.py CHANGED
@@ -8,32 +8,33 @@ from __future__ import annotations
8
8
 
9
9
  import threading
10
10
  from operator import itemgetter
11
- from typing import Any, Mapping, TYPE_CHECKING
11
+ from typing import TYPE_CHECKING, Any, Mapping, cast
12
12
  from weakref import WeakKeyDictionary
13
13
 
14
14
  if TYPE_CHECKING: # pragma: no cover
15
- import networkx as nx # type: ignore[import-untyped]
15
+ import networkx as nx
16
16
 
17
17
  from .constants import DEFAULTS
18
18
  from .constants.core import SELECTOR_THRESHOLD_DEFAULTS
19
- from .helpers.numeric import clamp01
19
+ from .utils import clamp01
20
20
  from .metrics.common import compute_dnfr_accel_max
21
- from .collections_utils import is_non_string_sequence
22
-
21
+ from .types import SelectorNorms, SelectorThresholds, SelectorWeights
22
+ from .utils import is_non_string_sequence
23
23
 
24
24
  HYSTERESIS_GLYPHS: set[str] = {"IL", "OZ", "ZHIR", "THOL", "NAV", "RA"}
25
25
 
26
26
  __all__ = (
27
27
  "_selector_thresholds",
28
- "_norms_para_selector",
28
+ "_selector_norms",
29
29
  "_calc_selector_score",
30
30
  "_apply_selector_hysteresis",
31
31
  )
32
32
 
33
33
 
34
+ _SelectorThresholdItems = tuple[tuple[str, float], ...]
34
35
  _SelectorThresholdCacheEntry = tuple[
35
- tuple[tuple[str, float], ...],
36
- dict[str, float],
36
+ _SelectorThresholdItems,
37
+ SelectorThresholds,
37
38
  ]
38
39
  _SELECTOR_THRESHOLD_CACHE: WeakKeyDictionary[
39
40
  "nx.Graph",
@@ -42,7 +43,7 @@ _SELECTOR_THRESHOLD_CACHE: WeakKeyDictionary[
42
43
  _SELECTOR_THRESHOLD_CACHE_LOCK = threading.Lock()
43
44
 
44
45
 
45
- def _sorted_items(mapping: Mapping[str, float]) -> tuple[tuple[str, float], ...]:
46
+ def _sorted_items(mapping: Mapping[str, float]) -> _SelectorThresholdItems:
46
47
  """Return mapping items sorted by key.
47
48
 
48
49
  Parameters
@@ -59,8 +60,8 @@ def _sorted_items(mapping: Mapping[str, float]) -> tuple[tuple[str, float], ...]
59
60
 
60
61
 
61
62
  def _compute_selector_thresholds(
62
- thr_sel_items: tuple[tuple[str, float], ...],
63
- ) -> dict[str, float]:
63
+ thr_sel_items: _SelectorThresholdItems,
64
+ ) -> SelectorThresholds:
64
65
  """Construct selector thresholds for a graph.
65
66
 
66
67
  Parameters
@@ -79,10 +80,10 @@ def _compute_selector_thresholds(
79
80
  for key, default in SELECTOR_THRESHOLD_DEFAULTS.items():
80
81
  val = thr_sel.get(key, default)
81
82
  out[key] = clamp01(float(val))
82
- return out
83
+ return cast(SelectorThresholds, out)
83
84
 
84
85
 
85
- def _selector_thresholds(G: "nx.Graph") -> dict[str, float]:
86
+ def _selector_thresholds(G: "nx.Graph") -> SelectorThresholds:
86
87
  """Return normalised thresholds for Si, ΔNFR and acceleration.
87
88
 
88
89
  Parameters
@@ -114,8 +115,8 @@ def _selector_thresholds(G: "nx.Graph") -> dict[str, float]:
114
115
  return thresholds
115
116
 
116
117
 
117
- def _norms_para_selector(G: "nx.Graph") -> dict:
118
- """Compute and cache norms for ΔNFR and acceleration.
118
+ def _selector_norms(G: "nx.Graph") -> SelectorNorms:
119
+ """Compute and cache selector norms for ΔNFR and acceleration.
119
120
 
120
121
  Parameters
121
122
  ----------
@@ -134,7 +135,7 @@ def _norms_para_selector(G: "nx.Graph") -> dict:
134
135
 
135
136
 
136
137
  def _calc_selector_score(
137
- Si: float, dnfr: float, accel: float, weights: dict[str, float]
138
+ Si: float, dnfr: float, accel: float, weights: SelectorWeights
138
139
  ) -> float:
139
140
  """Compute weighted selector score.
140
141
 
@@ -167,7 +168,7 @@ def _apply_selector_hysteresis(
167
168
  dnfr: float,
168
169
  accel: float,
169
170
  thr: dict[str, float],
170
- margin: float,
171
+ margin: float | None,
171
172
  ) -> str | None:
172
173
  """Apply hysteresis when values are near thresholds.
173
174
 
@@ -183,8 +184,10 @@ def _apply_selector_hysteresis(
183
184
  Normalised acceleration.
184
185
  thr : dict[str, float]
185
186
  Thresholds returned by :func:`_selector_thresholds`.
186
- margin : float
187
- Distance from thresholds below which the previous glyph is reused.
187
+ margin : float or None
188
+ When positive, distance from thresholds below which the previous
189
+ glyph is reused. Falsy margins disable hysteresis entirely, letting
190
+ selectors bypass the reuse logic.
188
191
 
189
192
  Returns
190
193
  -------
@@ -192,6 +195,9 @@ def _apply_selector_hysteresis(
192
195
  Previous glyph if hysteresis applies, otherwise ``None``.
193
196
  """
194
197
  # Batch extraction reduces dictionary lookups inside loops.
198
+ if not margin:
199
+ return None
200
+
195
201
  si_hi, si_lo, dnfr_hi, dnfr_lo, accel_hi, accel_lo = itemgetter(
196
202
  "si_hi", "si_lo", "dnfr_hi", "dnfr_lo", "accel_hi", "accel_lo"
197
203
  )(thr)
tnfr/selector.pyi ADDED
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping
4
+
5
+ __all__: Any
6
+
7
+ def __getattr__(name: str) -> Any: ...
8
+ def _apply_selector_hysteresis(
9
+ nd: dict[str, Any],
10
+ Si: float,
11
+ dnfr: float,
12
+ accel: float,
13
+ thr: Mapping[str, float],
14
+ margin: float | None,
15
+ ) -> str | None: ...
16
+
17
+ _calc_selector_score: Any
18
+ _selector_norms: Any
19
+ _selector_thresholds: Any