tnfr 4.5.1__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.
- tnfr/__init__.py +91 -90
- tnfr/alias.py +546 -0
- tnfr/cache.py +578 -0
- tnfr/callback_utils.py +388 -0
- tnfr/cli/__init__.py +75 -0
- tnfr/cli/arguments.py +177 -0
- tnfr/cli/execution.py +288 -0
- tnfr/cli/utils.py +36 -0
- tnfr/collections_utils.py +300 -0
- tnfr/config.py +19 -28
- tnfr/constants/__init__.py +174 -0
- tnfr/constants/core.py +159 -0
- tnfr/constants/init.py +31 -0
- tnfr/constants/metric.py +110 -0
- tnfr/constants_glyphs.py +98 -0
- tnfr/dynamics/__init__.py +658 -0
- tnfr/dynamics/dnfr.py +733 -0
- tnfr/dynamics/integrators.py +267 -0
- tnfr/dynamics/sampling.py +31 -0
- tnfr/execution.py +201 -0
- tnfr/flatten.py +283 -0
- tnfr/gamma.py +302 -88
- tnfr/glyph_history.py +290 -0
- tnfr/grammar.py +285 -96
- tnfr/graph_utils.py +84 -0
- tnfr/helpers/__init__.py +71 -0
- tnfr/helpers/numeric.py +87 -0
- tnfr/immutable.py +178 -0
- tnfr/import_utils.py +228 -0
- tnfr/initialization.py +197 -0
- tnfr/io.py +246 -0
- tnfr/json_utils.py +162 -0
- tnfr/locking.py +37 -0
- tnfr/logging_utils.py +116 -0
- tnfr/metrics/__init__.py +41 -0
- tnfr/metrics/coherence.py +829 -0
- tnfr/metrics/common.py +151 -0
- tnfr/metrics/core.py +101 -0
- tnfr/metrics/diagnosis.py +234 -0
- tnfr/metrics/export.py +137 -0
- tnfr/metrics/glyph_timing.py +189 -0
- tnfr/metrics/reporting.py +148 -0
- tnfr/metrics/sense_index.py +120 -0
- tnfr/metrics/trig.py +181 -0
- tnfr/metrics/trig_cache.py +109 -0
- tnfr/node.py +214 -159
- tnfr/observers.py +126 -136
- tnfr/ontosim.py +134 -134
- tnfr/operators/__init__.py +420 -0
- tnfr/operators/jitter.py +203 -0
- tnfr/operators/remesh.py +485 -0
- tnfr/presets.py +46 -14
- tnfr/rng.py +254 -0
- tnfr/selector.py +210 -0
- tnfr/sense.py +284 -131
- tnfr/structural.py +207 -79
- tnfr/tokens.py +60 -0
- tnfr/trace.py +329 -94
- tnfr/types.py +43 -17
- tnfr/validators.py +70 -24
- tnfr/value_utils.py +59 -0
- tnfr-4.5.2.dist-info/METADATA +379 -0
- tnfr-4.5.2.dist-info/RECORD +67 -0
- tnfr/cli.py +0 -322
- tnfr/constants.py +0 -277
- tnfr/dynamics.py +0 -814
- tnfr/helpers.py +0 -264
- tnfr/main.py +0 -47
- tnfr/metrics.py +0 -597
- tnfr/operators.py +0 -525
- tnfr/program.py +0 -176
- tnfr/scenarios.py +0 -34
- tnfr-4.5.1.dist-info/METADATA +0 -221
- tnfr-4.5.1.dist-info/RECORD +0 -28
- {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/WHEEL +0 -0
- {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.5.1.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
|