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/rng.py CHANGED
@@ -5,18 +5,19 @@ from __future__ import annotations
5
5
  import hashlib
6
6
  import random
7
7
  import struct
8
- import threading
9
- from collections.abc import Iterator, MutableMapping
10
- from dataclasses import dataclass
11
- from typing import Any, Generic, Hashable, TypeVar, cast
12
-
8
+ from typing import Any, cast
13
9
 
14
10
  from cachetools import cached # type: ignore[import-untyped]
11
+
15
12
  from .constants import DEFAULTS, get_param
16
- from .cache import CacheManager, InstrumentedLRUCache
17
- from .utils import get_graph
18
13
  from .locking import get_lock
19
14
  from .types import GraphLike, TNFRGraph
15
+ from .utils import (
16
+ ScopedCounterCache,
17
+ _SeedHashCache,
18
+ build_cache_manager,
19
+ get_graph,
20
+ )
20
21
 
21
22
  MASK64 = 0xFFFFFFFFFFFFFFFF
22
23
 
@@ -25,296 +26,14 @@ _DEFAULT_CACHE_MAXSIZE = int(DEFAULTS.get("JITTER_CACHE_SIZE", 128))
25
26
  _CACHE_MAXSIZE = _DEFAULT_CACHE_MAXSIZE
26
27
  _CACHE_LOCKED = False
27
28
 
28
- K = TypeVar("K", bound=Hashable)
29
- V = TypeVar("V")
30
-
31
-
32
- @dataclass
33
- class _SeedCacheState:
34
- cache: InstrumentedLRUCache[tuple[int, int], int] | None
35
- maxsize: int
36
-
37
-
38
- @dataclass
39
- class _CounterState(Generic[K]):
40
- cache: InstrumentedLRUCache[K, int]
41
- locks: dict[K, threading.RLock]
42
- max_entries: int
43
-
44
-
45
- _RNG_CACHE_MANAGER = CacheManager(default_capacity=_DEFAULT_CACHE_MAXSIZE)
46
-
47
-
48
- class _SeedHashCache(MutableMapping[tuple[int, int], int]):
49
- """Mutable mapping proxy exposing a configurable LRU cache."""
50
-
51
- def __init__(
52
- self,
53
- *,
54
- manager: CacheManager | None = None,
55
- state_key: str = "seed_hash_cache",
56
- default_maxsize: int = _DEFAULT_CACHE_MAXSIZE,
57
- ) -> None:
58
- self._manager = manager or _RNG_CACHE_MANAGER
59
- self._state_key = state_key
60
- self._default_maxsize = int(default_maxsize)
61
- if not self._manager.has_override(self._state_key):
62
- self._manager.configure(
63
- overrides={self._state_key: self._default_maxsize}
64
- )
65
- self._manager.register(
66
- self._state_key,
67
- self._create_state,
68
- reset=self._reset_state,
69
- )
70
-
71
- def _resolved_size(self, requested: int | None = None) -> int:
72
- size = self._manager.get_capacity(
73
- self._state_key,
74
- requested=requested,
75
- fallback=self._default_maxsize,
76
- )
77
- if size is None:
78
- return 0
79
- return int(size)
80
-
81
- def _create_state(self) -> _SeedCacheState:
82
- size = self._resolved_size()
83
- if size <= 0:
84
- return _SeedCacheState(cache=None, maxsize=0)
85
- return _SeedCacheState(
86
- cache=InstrumentedLRUCache(
87
- size,
88
- manager=self._manager,
89
- metrics_key=self._state_key,
90
- ),
91
- maxsize=size,
92
- )
93
-
94
- def _reset_state(self, state: _SeedCacheState | None) -> _SeedCacheState:
95
- return self._create_state()
96
-
97
- def _get_state(self, *, create: bool = True) -> _SeedCacheState | None:
98
- state = self._manager.get(self._state_key, create=create)
99
- if state is None:
100
- return None
101
- if not isinstance(state, _SeedCacheState):
102
- state = self._create_state()
103
- self._manager.store(self._state_key, state)
104
- return state
105
-
106
- def configure(self, maxsize: int) -> None:
107
- size = int(maxsize)
108
- if size < 0:
109
- raise ValueError("maxsize must be non-negative")
110
- self._manager.configure(overrides={self._state_key: size})
111
- self._manager.update(self._state_key, lambda _: self._create_state())
112
-
113
- def __getitem__(self, key: tuple[int, int]) -> int:
114
- state = self._get_state()
115
- if state is None or state.cache is None:
116
- raise KeyError(key)
117
- value = state.cache[key]
118
- self._manager.increment_hit(self._state_key)
119
- return value
120
-
121
- def __setitem__(self, key: tuple[int, int], value: int) -> None:
122
- state = self._get_state()
123
- if state is not None and state.cache is not None:
124
- state.cache[key] = value
125
-
126
- def __delitem__(self, key: tuple[int, int]) -> None:
127
- state = self._get_state()
128
- if state is None or state.cache is None:
129
- raise KeyError(key)
130
- del state.cache[key]
131
-
132
- def __iter__(self) -> Iterator[tuple[int, int]]:
133
- state = self._get_state(create=False)
134
- if state is None or state.cache is None:
135
- return iter(())
136
- return iter(state.cache)
137
-
138
- def __len__(self) -> int:
139
- state = self._get_state(create=False)
140
- if state is None or state.cache is None:
141
- return 0
142
- return len(state.cache)
143
-
144
- def clear(self) -> None: # type: ignore[override]
145
- self._manager.clear(self._state_key)
146
-
147
- @property
148
- def maxsize(self) -> int:
149
- state = self._get_state()
150
- return 0 if state is None else state.maxsize
151
-
152
- @property
153
- def enabled(self) -> bool:
154
- state = self._get_state(create=False)
155
- return bool(state and state.cache is not None)
156
-
157
- @property
158
- def data(self) -> InstrumentedLRUCache[tuple[int, int], int] | None:
159
- """Expose the underlying cache for diagnostics/tests."""
160
-
161
- state = self._get_state(create=False)
162
- return None if state is None else state.cache
163
-
164
-
165
- class ScopedCounterCache(Generic[K]):
166
- """Thread-safe LRU cache storing monotonic counters by ``key``."""
167
-
168
- def __init__(
169
- self,
170
- name: str,
171
- max_entries: int | None = None,
172
- *,
173
- manager: CacheManager | None = None,
174
- default_max_entries: int = _DEFAULT_CACHE_MAXSIZE,
175
- ) -> None:
176
- self._name = name
177
- self._manager = manager or _RNG_CACHE_MANAGER
178
- self._state_key = f"scoped_counter:{name}"
179
- self._default_max_entries = int(default_max_entries)
180
- requested = None if max_entries is None else int(max_entries)
181
- if requested is not None and requested < 0:
182
- raise ValueError("max_entries must be non-negative")
183
- if not self._manager.has_override(self._state_key):
184
- fallback = requested
185
- if fallback is None:
186
- fallback = self._default_max_entries
187
- self._manager.configure(overrides={self._state_key: fallback})
188
- elif requested is not None:
189
- self._manager.configure(overrides={self._state_key: requested})
190
- self._manager.register(
191
- self._state_key,
192
- self._create_state,
193
- lock_factory=lambda: get_lock(name),
194
- reset=self._reset_state,
195
- )
196
-
197
- def _resolved_entries(self, requested: int | None = None) -> int:
198
- size = self._manager.get_capacity(
199
- self._state_key,
200
- requested=requested,
201
- fallback=self._default_max_entries,
202
- )
203
- if size is None:
204
- return 0
205
- return int(size)
206
-
207
- def _create_state(self, requested: int | None = None) -> _CounterState[K]:
208
- size = self._resolved_entries(requested)
209
- locks: dict[K, threading.RLock] = {}
210
- return _CounterState(
211
- cache=InstrumentedLRUCache(
212
- size,
213
- manager=self._manager,
214
- metrics_key=self._state_key,
215
- locks=locks,
216
- ),
217
- locks=locks,
218
- max_entries=size,
219
- )
220
-
221
- def _reset_state(self, state: _CounterState[K] | None) -> _CounterState[K]:
222
- return self._create_state()
223
-
224
- def _get_state(self) -> _CounterState[K]:
225
- state = self._manager.get(self._state_key)
226
- if not isinstance(state, _CounterState):
227
- state = self._create_state(0)
228
- self._manager.store(self._state_key, state)
229
- return state
230
-
231
- @property
232
- def lock(self) -> threading.Lock | threading.RLock:
233
- """Return the lock guarding access to the underlying cache."""
234
-
235
- return self._manager.get_lock(self._state_key)
236
-
237
- @property
238
- def max_entries(self) -> int:
239
- """Return the configured maximum number of cached entries."""
240
-
241
- return self._get_state().max_entries
242
-
243
- @property
244
- def cache(self) -> InstrumentedLRUCache[K, int]:
245
- """Expose the instrumented cache for inspection."""
246
-
247
- return self._get_state().cache
248
-
249
- @property
250
- def locks(self) -> dict[K, threading.RLock]:
251
- """Return the mapping of per-key locks tracked by the cache."""
252
-
253
- return self._get_state().locks
254
-
255
- def configure(
256
- self, *, force: bool = False, max_entries: int | None = None
257
- ) -> None:
258
- """Resize or reset the cache keeping previous settings."""
259
-
260
- if max_entries is None:
261
- size = self._resolved_entries()
262
- update_policy = False
263
- else:
264
- size = int(max_entries)
265
- if size < 0:
266
- raise ValueError("max_entries must be non-negative")
267
- update_policy = True
268
-
269
- def _update(state: _CounterState[K] | None) -> _CounterState[K]:
270
- if not isinstance(state, _CounterState) or force or state.max_entries != size:
271
- locks: dict[K, threading.RLock] = {}
272
- return _CounterState(
273
- cache=InstrumentedLRUCache(
274
- size,
275
- manager=self._manager,
276
- metrics_key=self._state_key,
277
- locks=locks,
278
- ),
279
- locks=locks,
280
- max_entries=size,
281
- )
282
- return cast(_CounterState[K], state)
283
-
284
- if update_policy:
285
- self._manager.configure(overrides={self._state_key: size})
286
- self._manager.update(self._state_key, _update)
287
-
288
- def clear(self) -> None:
289
- """Clear stored counters preserving ``max_entries``."""
290
-
291
- self.configure(force=True)
292
-
293
- def bump(self, key: K) -> int:
294
- """Return current counter for ``key`` and increment it atomically."""
295
-
296
- result: dict[str, Any] = {}
297
-
298
- def _update(state: _CounterState[K] | None) -> _CounterState[K]:
299
- if not isinstance(state, _CounterState):
300
- state = self._create_state(0)
301
- cache = state.cache
302
- locks = state.locks
303
- if key not in locks:
304
- locks[key] = threading.RLock()
305
- value = int(cache.get(key, 0))
306
- cache[key] = value + 1
307
- result["value"] = value
308
- return state
309
-
310
- self._manager.update(self._state_key, _update)
311
- return int(result.get("value", 0))
312
-
313
- def __len__(self) -> int:
314
- return len(self.cache)
315
-
316
-
317
- _seed_hash_cache = _SeedHashCache()
29
+
30
+ _RNG_CACHE_MANAGER = build_cache_manager(default_capacity=_DEFAULT_CACHE_MAXSIZE)
31
+
32
+
33
+ _seed_hash_cache = _SeedHashCache(
34
+ manager=_RNG_CACHE_MANAGER,
35
+ default_maxsize=_DEFAULT_CACHE_MAXSIZE,
36
+ )
318
37
 
319
38
 
320
39
  def _compute_seed_hash(seed_int: int, key_int: int) -> int:
@@ -323,9 +42,7 @@ def _compute_seed_hash(seed_int: int, key_int: int) -> int:
323
42
  seed_int & MASK64,
324
43
  key_int & MASK64,
325
44
  )
326
- return int.from_bytes(
327
- hashlib.blake2b(seed_bytes, digest_size=8).digest(), "big"
328
- )
45
+ return int.from_bytes(hashlib.blake2b(seed_bytes, digest_size=8).digest(), "big")
329
46
 
330
47
 
331
48
  @cached(cache=_seed_hash_cache, lock=_RNG_LOCK)
@@ -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,7 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  import threading
10
10
  from operator import itemgetter
11
- from typing import Any, Mapping, TYPE_CHECKING, cast
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
@@ -16,11 +16,10 @@ if TYPE_CHECKING: # pragma: no cover
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 .utils import is_non_string_sequence
22
21
  from .types import SelectorNorms, SelectorThresholds, SelectorWeights
23
-
22
+ from .utils import is_non_string_sequence
24
23
 
25
24
  HYSTERESIS_GLYPHS: set[str] = {"IL", "OZ", "ZHIR", "THOL", "NAV", "RA"}
26
25
 
tnfr/selector.pyi CHANGED
@@ -5,7 +5,6 @@ from typing import Any, Mapping
5
5
  __all__: Any
6
6
 
7
7
  def __getattr__(name: str) -> Any: ...
8
-
9
8
  def _apply_selector_hysteresis(
10
9
  nd: dict[str, Any],
11
10
  Si: float,
@@ -14,6 +13,7 @@ def _apply_selector_hysteresis(
14
13
  thr: Mapping[str, float],
15
14
  margin: float | None,
16
15
  ) -> str | None: ...
16
+
17
17
  _calc_selector_score: Any
18
18
  _selector_norms: Any
19
19
  _selector_thresholds: Any
tnfr/sense.py CHANGED
@@ -1,30 +1,29 @@
1
1
  """Sense calculations."""
2
2
 
3
3
  from __future__ import annotations
4
- from typing import Any, Callable, TypeVar
5
- from collections.abc import Iterable, Iterator, Mapping
4
+
6
5
  import math
7
6
  from collections import Counter
7
+ from collections.abc import Iterable, Iterator, Mapping
8
8
  from itertools import tee
9
+ from typing import Any, Callable, TypeVar
9
10
 
10
11
  import networkx as nx
11
12
 
12
- from .constants import get_aliases, get_graph_param
13
13
  from .alias import get_attr
14
- from .helpers.numeric import clamp01, kahan_sum_nd
15
- from .utils import get_numpy
16
14
  from .callback_utils import CallbackEvent, callback_manager
17
- from .glyph_history import (
18
- ensure_history,
19
- last_glyph,
20
- count_glyphs,
21
- append_metric,
22
- )
23
15
  from .config.constants import (
24
16
  ANGLE_MAP,
25
17
  GLYPHS_CANONICAL,
26
18
  )
19
+ from .constants import get_graph_param
20
+ from .constants.aliases import ALIAS_EPI, ALIAS_SI
21
+ from .glyph_history import append_metric, count_glyphs, ensure_history
22
+ from .glyph_runtime import last_glyph
23
+ from .utils import clamp01, kahan_sum_nd
27
24
  from .types import NodeId, SigmaVector, TNFRGraph
25
+ from .utils import get_numpy
26
+
28
27
  # -------------------------
29
28
  # Canon: circular glyph order and angles
30
29
  # -------------------------
@@ -74,9 +73,6 @@ def glyph_unit(g: str) -> complex:
74
73
  return _resolve_glyph(g, GLYPH_UNITS)
75
74
 
76
75
 
77
- ALIAS_SI = get_aliases("SI")
78
- ALIAS_EPI = get_aliases("EPI")
79
-
80
76
  MODE_FUNCS: dict[str, Callable[[Mapping[str, Any]], float]] = {
81
77
  "Si": lambda nd: clamp01(get_attr(nd, ALIAS_SI, 0.5)),
82
78
  "EPI": lambda nd: max(0.0, get_attr(nd, ALIAS_EPI, 0.0)),
@@ -145,7 +141,9 @@ def _sigma_from_iterable(
145
141
  number of processed values under the ``"n"`` key.
146
142
  """
147
143
 
148
- if isinstance(values, Iterable) and not isinstance(values, (str, bytes, bytearray, Mapping)):
144
+ if isinstance(values, Iterable) and not isinstance(
145
+ values, (str, bytes, bytearray, Mapping)
146
+ ):
149
147
  iterator = iter(values)
150
148
  else:
151
149
  iterator = iter((values,))
@@ -195,9 +193,7 @@ def _sigma_from_iterable(
195
193
  }
196
194
 
197
195
 
198
- def _ema_update(
199
- prev: SigmaVector, current: SigmaVector, alpha: float
200
- ) -> SigmaVector:
196
+ def _ema_update(prev: SigmaVector, current: SigmaVector, alpha: float) -> SigmaVector:
201
197
  """Exponential moving average update for σ vectors."""
202
198
  x = (1 - alpha) * prev["x"] + alpha * current["x"]
203
199
  y = (1 - alpha) * prev["y"] + alpha * current["y"]
@@ -231,6 +227,8 @@ def _sigma_from_nodes(
231
227
  def sigma_vector_node(
232
228
  G: TNFRGraph, n: NodeId, weight_mode: str | None = None
233
229
  ) -> SigmaVector | None:
230
+ """Return the σ vector for node ``n`` using the configured weighting."""
231
+
234
232
  cfg = _sigma_cfg(G)
235
233
  nd = G.nodes[n]
236
234
  weight_mode = weight_mode or cfg.get("weight", "Si")
@@ -280,9 +278,7 @@ def sigma_vector_from_graph(
280
278
 
281
279
  cfg = _sigma_cfg(G)
282
280
  weight_mode = weight_mode or cfg.get("weight", "Si")
283
- sv, _ = _sigma_from_nodes(
284
- (nd for _, nd in G.nodes(data=True)), weight_mode
285
- )
281
+ sv, _ = _sigma_from_nodes((nd for _, nd in G.nodes(data=True)), weight_mode)
286
282
  return sv
287
283
 
288
284
 
@@ -292,6 +288,8 @@ def sigma_vector_from_graph(
292
288
 
293
289
 
294
290
  def push_sigma_snapshot(G: TNFRGraph, t: float | None = None) -> None:
291
+ """Record a global σ snapshot (and optional per-node traces) for ``G``."""
292
+
295
293
  cfg = _sigma_cfg(G)
296
294
  if not cfg.get("enabled", True):
297
295
  return
@@ -334,6 +332,8 @@ def push_sigma_snapshot(G: TNFRGraph, t: float | None = None) -> None:
334
332
 
335
333
 
336
334
  def register_sigma_callback(G: TNFRGraph) -> None:
335
+ """Attach :func:`push_sigma_snapshot` to the ``AFTER_STEP`` callback bus."""
336
+
337
337
  callback_manager.register_callback(
338
338
  G,
339
339
  event=CallbackEvent.AFTER_STEP.value,
@@ -352,9 +352,7 @@ def sigma_rose(G: TNFRGraph, steps: int | None = None) -> dict[str, int]:
352
352
  steps = int(steps)
353
353
  if steps < 0:
354
354
  raise ValueError("steps must be non-negative")
355
- rows = (
356
- counts if steps >= len(counts) else counts[-steps:]
357
- ) # noqa: E203
355
+ rows = counts if steps >= len(counts) else counts[-steps:] # noqa: E203
358
356
  else:
359
357
  rows = counts
360
358
  counter = Counter()
tnfr/sense.pyi CHANGED
@@ -10,21 +10,14 @@ __all__: tuple[str, ...]
10
10
  GLYPH_UNITS: dict[str, complex]
11
11
 
12
12
  def glyph_angle(g: str) -> float: ...
13
-
14
13
  def glyph_unit(g: str) -> complex: ...
15
-
16
14
  def push_sigma_snapshot(G: TNFRGraph, t: Optional[float] = None) -> None: ...
17
-
18
15
  def register_sigma_callback(G: TNFRGraph) -> None: ...
19
-
20
16
  def sigma_rose(G: TNFRGraph, steps: Optional[int] = None) -> dict[str, int]: ...
21
-
22
17
  def sigma_vector(dist: Mapping[str, float]) -> SigmaVector: ...
23
-
24
18
  def sigma_vector_from_graph(
25
19
  G: TNFRGraph, weight_mode: Optional[str] = None
26
20
  ) -> SigmaVector: ...
27
-
28
21
  def sigma_vector_node(
29
22
  G: TNFRGraph, n: NodeId, weight_mode: Optional[str] = None
30
23
  ) -> Optional[SigmaVector]: ...