tnfr 4.5.0__py3-none-any.whl → 4.5.2__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 (78) hide show
  1. tnfr/__init__.py +91 -89
  2. tnfr/alias.py +546 -0
  3. tnfr/cache.py +578 -0
  4. tnfr/callback_utils.py +388 -0
  5. tnfr/cli/__init__.py +75 -0
  6. tnfr/cli/arguments.py +177 -0
  7. tnfr/cli/execution.py +288 -0
  8. tnfr/cli/utils.py +36 -0
  9. tnfr/collections_utils.py +300 -0
  10. tnfr/config.py +19 -28
  11. tnfr/constants/__init__.py +174 -0
  12. tnfr/constants/core.py +159 -0
  13. tnfr/constants/init.py +31 -0
  14. tnfr/constants/metric.py +110 -0
  15. tnfr/constants_glyphs.py +98 -0
  16. tnfr/dynamics/__init__.py +658 -0
  17. tnfr/dynamics/dnfr.py +733 -0
  18. tnfr/dynamics/integrators.py +267 -0
  19. tnfr/dynamics/sampling.py +31 -0
  20. tnfr/execution.py +201 -0
  21. tnfr/flatten.py +283 -0
  22. tnfr/gamma.py +302 -88
  23. tnfr/glyph_history.py +290 -0
  24. tnfr/grammar.py +285 -96
  25. tnfr/graph_utils.py +84 -0
  26. tnfr/helpers/__init__.py +71 -0
  27. tnfr/helpers/numeric.py +87 -0
  28. tnfr/immutable.py +178 -0
  29. tnfr/import_utils.py +228 -0
  30. tnfr/initialization.py +197 -0
  31. tnfr/io.py +246 -0
  32. tnfr/json_utils.py +162 -0
  33. tnfr/locking.py +37 -0
  34. tnfr/logging_utils.py +116 -0
  35. tnfr/metrics/__init__.py +41 -0
  36. tnfr/metrics/coherence.py +829 -0
  37. tnfr/metrics/common.py +151 -0
  38. tnfr/metrics/core.py +101 -0
  39. tnfr/metrics/diagnosis.py +234 -0
  40. tnfr/metrics/export.py +137 -0
  41. tnfr/metrics/glyph_timing.py +189 -0
  42. tnfr/metrics/reporting.py +148 -0
  43. tnfr/metrics/sense_index.py +120 -0
  44. tnfr/metrics/trig.py +181 -0
  45. tnfr/metrics/trig_cache.py +109 -0
  46. tnfr/node.py +214 -159
  47. tnfr/observers.py +126 -128
  48. tnfr/ontosim.py +134 -134
  49. tnfr/operators/__init__.py +420 -0
  50. tnfr/operators/jitter.py +203 -0
  51. tnfr/operators/remesh.py +485 -0
  52. tnfr/presets.py +46 -14
  53. tnfr/rng.py +254 -0
  54. tnfr/selector.py +210 -0
  55. tnfr/sense.py +284 -131
  56. tnfr/structural.py +207 -79
  57. tnfr/tokens.py +60 -0
  58. tnfr/trace.py +329 -94
  59. tnfr/types.py +43 -17
  60. tnfr/validators.py +70 -24
  61. tnfr/value_utils.py +59 -0
  62. tnfr-4.5.2.dist-info/METADATA +379 -0
  63. tnfr-4.5.2.dist-info/RECORD +67 -0
  64. tnfr/cli.py +0 -322
  65. tnfr/constants.py +0 -277
  66. tnfr/dynamics.py +0 -814
  67. tnfr/helpers.py +0 -264
  68. tnfr/main.py +0 -47
  69. tnfr/metrics.py +0 -597
  70. tnfr/operators.py +0 -525
  71. tnfr/program.py +0 -176
  72. tnfr/scenarios.py +0 -34
  73. tnfr-4.5.0.dist-info/METADATA +0 -109
  74. tnfr-4.5.0.dist-info/RECORD +0 -28
  75. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/WHEEL +0 -0
  76. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/entry_points.txt +0 -0
  77. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/licenses/LICENSE.md +0 -0
  78. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/top_level.txt +0 -0
tnfr/glyph_history.py ADDED
@@ -0,0 +1,290 @@
1
+ """Utilities for tracking glyph emission history and related metrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from collections import deque, Counter
7
+ from itertools import islice
8
+ from collections.abc import Iterable, Mapping
9
+ from functools import lru_cache
10
+
11
+ from .constants import get_param
12
+ from .collections_utils import ensure_collection
13
+ from .logging_utils import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ __all__ = (
18
+ "HistoryDict",
19
+ "push_glyph",
20
+ "recent_glyph",
21
+ "ensure_history",
22
+ "current_step_idx",
23
+ "append_metric",
24
+ "last_glyph",
25
+ "count_glyphs",
26
+ )
27
+
28
+
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)
38
+
39
+
40
+ def _ensure_history(
41
+ nd: dict[str, Any], window: int, *, create_zero: bool = False
42
+ ) -> tuple[int, deque | None]:
43
+ """Validate ``window`` and ensure ``nd['glyph_history']`` deque."""
44
+
45
+ v_window = _validate_window(window)
46
+ if v_window == 0 and not create_zero:
47
+ return v_window, None
48
+ hist = nd.setdefault("glyph_history", deque(maxlen=v_window))
49
+ if not isinstance(hist, deque) or hist.maxlen != v_window:
50
+ # Rebuild deque from any iterable, ignoring raw strings/bytes and scalars
51
+ if isinstance(hist, (str, bytes, bytearray)):
52
+ items: Iterable[Any] = ()
53
+ else:
54
+ try:
55
+ items = ensure_collection(hist, max_materialize=None)
56
+ except TypeError:
57
+ logger.debug(
58
+ "Discarding non-iterable glyph history value %r", hist
59
+ )
60
+ items = ()
61
+ hist = deque(items, maxlen=v_window)
62
+ nd["glyph_history"] = hist
63
+ return v_window, hist
64
+
65
+
66
+ def push_glyph(nd: dict[str, Any], glyph: str, window: int) -> None:
67
+ """Add ``glyph`` to node history with maximum size ``window``.
68
+
69
+ ``window`` validation and deque creation are handled by
70
+ :func:`_ensure_history`.
71
+ """
72
+
73
+ _, hist = _ensure_history(nd, window, create_zero=True)
74
+ hist.append(str(glyph))
75
+
76
+
77
+ def recent_glyph(nd: dict[str, Any], glyph: str, window: int) -> bool:
78
+ """Return ``True`` if ``glyph`` appeared in last ``window`` emissions.
79
+
80
+ ``window`` validation and deque creation are handled by
81
+ :func:`_ensure_history`. A ``window`` of zero returns ``False`` and
82
+ leaves ``nd`` unchanged. Negative values raise :class:`ValueError`.
83
+ """
84
+
85
+ v_window, hist = _ensure_history(nd, window)
86
+ if v_window == 0:
87
+ return False
88
+ gl = str(glyph)
89
+ return gl in hist
90
+
91
+
92
+ class HistoryDict(dict):
93
+ """Dict specialized for bounded history series and usage counts.
94
+
95
+ Usage counts are tracked explicitly via :meth:`get_increment`. Accessing
96
+ keys through ``__getitem__`` or :meth:`get` does not affect the internal
97
+ counters, avoiding surprising evictions on mere reads. Counting is now
98
+ handled with :class:`collections.Counter` alone, relying on
99
+ :meth:`Counter.most_common` to locate least-used entries when required.
100
+
101
+ Parameters
102
+ ----------
103
+ data:
104
+ Initial mapping to populate the dictionary.
105
+ maxlen:
106
+ Maximum length for history lists stored as values.
107
+ """
108
+
109
+ def __init__(
110
+ self,
111
+ data: dict[str, Any] | None = None,
112
+ *,
113
+ maxlen: int = 0,
114
+ ) -> None:
115
+ super().__init__(data or {})
116
+ self._maxlen = maxlen
117
+ self._counts: Counter[str] = Counter()
118
+ if self._maxlen > 0:
119
+ for k, v in list(self.items()):
120
+ if isinstance(v, list):
121
+ super().__setitem__(k, deque(v, maxlen=self._maxlen))
122
+ self._counts[k] = 0
123
+ else:
124
+ for k in self:
125
+ self._counts[k] = 0
126
+ # ``_heap`` is no longer required with ``Counter.most_common``.
127
+
128
+ def _increment(self, key: str) -> None:
129
+ """Increase usage count for ``key``."""
130
+ self._counts[key] += 1
131
+
132
+ def _to_deque(self, val: Any) -> deque:
133
+ """Coerce ``val`` to a deque respecting ``self._maxlen``.
134
+
135
+ ``Iterable`` inputs (excluding ``str`` and ``bytes``) are expanded into
136
+ the deque, while single values are wrapped. Existing deques are
137
+ returned unchanged.
138
+ """
139
+
140
+ if isinstance(val, deque):
141
+ return val
142
+ if isinstance(val, Iterable) and not isinstance(val, (str, bytes)):
143
+ return deque(val, maxlen=self._maxlen)
144
+ return deque([val], maxlen=self._maxlen)
145
+
146
+ def _resolve_value(self, key: str, default: Any, *, insert: bool) -> Any:
147
+ if insert:
148
+ val = super().setdefault(key, default)
149
+ else:
150
+ val = super().__getitem__(key)
151
+ if self._maxlen > 0:
152
+ if not isinstance(val, Mapping):
153
+ val = self._to_deque(val)
154
+ super().__setitem__(key, val)
155
+ return val
156
+
157
+ def get_increment(self, key: str, default: Any = None) -> Any:
158
+ insert = key not in self
159
+ val = self._resolve_value(key, default, insert=insert)
160
+ self._increment(key)
161
+ return val
162
+
163
+ def __getitem__(self, key): # type: ignore[override]
164
+ return self._resolve_value(key, None, insert=False)
165
+
166
+ def get(self, key, default=None): # type: ignore[override]
167
+ try:
168
+ return self._resolve_value(key, None, insert=False)
169
+ except KeyError:
170
+ return default
171
+
172
+ def __setitem__(self, key, value): # type: ignore[override]
173
+ super().__setitem__(key, value)
174
+ if key not in self._counts:
175
+ self._counts[key] = 0
176
+
177
+ def setdefault(self, key, default=None): # type: ignore[override]
178
+ insert = key not in self
179
+ val = self._resolve_value(key, default, insert=insert)
180
+ if insert:
181
+ self._counts[key] = 0
182
+ return val
183
+
184
+ def pop_least_used(self) -> Any:
185
+ """Remove and return the value with the smallest usage count."""
186
+ while self._counts:
187
+ key = min(self._counts, key=self._counts.get)
188
+ self._counts.pop(key, None)
189
+ if key in self:
190
+ return super().pop(key)
191
+ raise KeyError("HistoryDict is empty; cannot pop least used")
192
+
193
+ def pop_least_used_batch(self, k: int) -> None:
194
+ for _ in range(max(0, int(k))):
195
+ try:
196
+ self.pop_least_used()
197
+ except KeyError:
198
+ break
199
+
200
+
201
+ def ensure_history(G) -> dict[str, Any]:
202
+ """Ensure ``G.graph['history']`` exists and return it.
203
+
204
+ ``HISTORY_MAXLEN`` must be non-negative; otherwise a
205
+ :class:`ValueError` is raised. When ``HISTORY_MAXLEN`` is zero, a regular
206
+ ``dict`` is used.
207
+ """
208
+ maxlen, _ = _ensure_history({}, int(get_param(G, "HISTORY_MAXLEN")))
209
+ hist = G.graph.get("history")
210
+ sentinel_key = "_metrics_history_id"
211
+ replaced = False
212
+ if maxlen == 0:
213
+ if isinstance(hist, HistoryDict):
214
+ hist = dict(hist)
215
+ G.graph["history"] = hist
216
+ replaced = True
217
+ elif hist is None:
218
+ hist = {}
219
+ G.graph["history"] = hist
220
+ replaced = True
221
+ if replaced:
222
+ G.graph.pop(sentinel_key, None)
223
+ return hist
224
+ if (
225
+ not isinstance(hist, HistoryDict)
226
+ or hist._maxlen != maxlen
227
+ ):
228
+ hist = HistoryDict(hist, maxlen=maxlen)
229
+ G.graph["history"] = hist
230
+ replaced = True
231
+ excess = len(hist) - maxlen
232
+ if excess > 0:
233
+ hist.pop_least_used_batch(excess)
234
+ if replaced:
235
+ G.graph.pop(sentinel_key, None)
236
+ return hist
237
+
238
+
239
+ def current_step_idx(G) -> int:
240
+ """Return the current step index from ``G`` history."""
241
+
242
+ graph = getattr(G, "graph", G)
243
+ return len(graph.get("history", {}).get("C_steps", []))
244
+
245
+
246
+
247
+ def append_metric(hist: dict[str, Any], key: str, value: Any) -> None:
248
+ """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
+
257
+
258
+ def count_glyphs(
259
+ G, window: int | None = None, *, last_only: bool = False
260
+ ) -> Counter:
261
+ """Count recent glyphs in the network.
262
+
263
+ If ``window`` is ``None``, the full history for each node is used. A
264
+ ``window`` of zero yields an empty :class:`Counter`. Negative values raise
265
+ :class:`ValueError`.
266
+ """
267
+
268
+ if window is not None:
269
+ window = _validate_window(window)
270
+ if window == 0:
271
+ return Counter()
272
+
273
+ counts: Counter[str] = Counter()
274
+ for _, nd in G.nodes(data=True):
275
+ if last_only:
276
+ g = last_glyph(nd)
277
+ if g:
278
+ counts[g] += 1
279
+ continue
280
+ hist = nd.get("glyph_history")
281
+ if not hist:
282
+ continue
283
+ if window is None:
284
+ seq = hist
285
+ else:
286
+ start = max(len(hist) - window, 0)
287
+ seq = islice(hist, start, None)
288
+ counts.update(seq)
289
+
290
+ return counts