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.
- tnfr/__init__.py +275 -51
- tnfr/__init__.pyi +33 -0
- tnfr/_compat.py +10 -0
- tnfr/_generated_version.py +34 -0
- tnfr/_version.py +49 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +117 -31
- tnfr/alias.pyi +108 -0
- tnfr/cache.py +6 -572
- tnfr/cache.pyi +16 -0
- tnfr/callback_utils.py +16 -38
- tnfr/callback_utils.pyi +79 -0
- tnfr/cli/__init__.py +34 -14
- tnfr/cli/__init__.pyi +26 -0
- tnfr/cli/arguments.py +211 -28
- tnfr/cli/arguments.pyi +27 -0
- tnfr/cli/execution.py +470 -50
- tnfr/cli/execution.pyi +70 -0
- tnfr/cli/utils.py +18 -3
- tnfr/cli/utils.pyi +8 -0
- tnfr/config/__init__.py +13 -0
- tnfr/config/__init__.pyi +10 -0
- tnfr/{constants_glyphs.py → config/constants.py} +26 -20
- tnfr/config/constants.pyi +12 -0
- tnfr/config/feature_flags.py +83 -0
- tnfr/{config.py → config/init.py} +11 -7
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +93 -0
- tnfr/config/operator_names.pyi +28 -0
- tnfr/config/presets.py +84 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/constants/__init__.py +80 -29
- tnfr/constants/__init__.pyi +92 -0
- tnfr/constants/aliases.py +31 -0
- tnfr/constants/core.py +4 -4
- tnfr/constants/core.pyi +17 -0
- tnfr/constants/init.py +1 -1
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +7 -15
- tnfr/constants/metric.pyi +19 -0
- tnfr/dynamics/__init__.py +165 -633
- tnfr/dynamics/__init__.pyi +82 -0
- tnfr/dynamics/adaptation.py +267 -0
- tnfr/dynamics/aliases.py +23 -0
- tnfr/dynamics/coordination.py +385 -0
- tnfr/dynamics/dnfr.py +2283 -400
- tnfr/dynamics/dnfr.pyi +24 -0
- tnfr/dynamics/integrators.py +406 -98
- tnfr/dynamics/integrators.pyi +34 -0
- tnfr/dynamics/runtime.py +881 -0
- tnfr/dynamics/sampling.py +10 -5
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +719 -0
- tnfr/execution.py +70 -48
- tnfr/execution.pyi +45 -0
- tnfr/flatten.py +13 -9
- tnfr/flatten.pyi +21 -0
- tnfr/gamma.py +66 -53
- tnfr/gamma.pyi +34 -0
- tnfr/glyph_history.py +110 -52
- tnfr/glyph_history.pyi +35 -0
- tnfr/glyph_runtime.py +16 -0
- tnfr/glyph_runtime.pyi +9 -0
- tnfr/immutable.py +69 -28
- tnfr/immutable.pyi +34 -0
- tnfr/initialization.py +16 -16
- tnfr/initialization.pyi +65 -0
- tnfr/io.py +6 -240
- tnfr/io.pyi +16 -0
- tnfr/locking.pyi +7 -0
- tnfr/mathematics/__init__.py +81 -0
- tnfr/mathematics/backend.py +426 -0
- tnfr/mathematics/dynamics.py +398 -0
- tnfr/mathematics/epi.py +254 -0
- tnfr/mathematics/generators.py +222 -0
- tnfr/mathematics/metrics.py +119 -0
- tnfr/mathematics/operators.py +233 -0
- tnfr/mathematics/operators_factory.py +71 -0
- tnfr/mathematics/projection.py +78 -0
- tnfr/mathematics/runtime.py +173 -0
- tnfr/mathematics/spaces.py +247 -0
- tnfr/mathematics/transforms.py +292 -0
- tnfr/metrics/__init__.py +10 -10
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/coherence.py +993 -324
- tnfr/metrics/common.py +23 -16
- tnfr/metrics/common.pyi +46 -0
- tnfr/metrics/core.py +251 -35
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +708 -111
- tnfr/metrics/diagnosis.pyi +85 -0
- tnfr/metrics/export.py +27 -15
- tnfr/metrics/glyph_timing.py +232 -42
- tnfr/metrics/reporting.py +33 -22
- tnfr/metrics/reporting.pyi +12 -0
- tnfr/metrics/sense_index.py +987 -43
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +214 -23
- tnfr/metrics/trig.pyi +13 -0
- tnfr/metrics/trig_cache.py +115 -22
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/node.py +542 -136
- tnfr/node.pyi +178 -0
- tnfr/observers.py +152 -35
- tnfr/observers.pyi +31 -0
- tnfr/ontosim.py +23 -19
- tnfr/ontosim.pyi +28 -0
- tnfr/operators/__init__.py +601 -82
- tnfr/operators/__init__.pyi +45 -0
- tnfr/operators/definitions.py +513 -0
- tnfr/operators/definitions.pyi +78 -0
- tnfr/operators/grammar.py +760 -0
- tnfr/operators/jitter.py +107 -38
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/registry.py +75 -0
- tnfr/operators/registry.pyi +13 -0
- tnfr/operators/remesh.py +149 -88
- tnfr/py.typed +0 -0
- tnfr/rng.py +46 -143
- tnfr/rng.pyi +14 -0
- tnfr/schemas/__init__.py +8 -0
- tnfr/schemas/grammar.json +94 -0
- tnfr/selector.py +25 -19
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +72 -62
- tnfr/sense.pyi +23 -0
- tnfr/structural.py +522 -262
- tnfr/structural.pyi +69 -0
- tnfr/telemetry/__init__.py +35 -0
- tnfr/telemetry/cache_metrics.py +226 -0
- tnfr/telemetry/nu_f.py +423 -0
- tnfr/telemetry/nu_f.pyi +123 -0
- tnfr/telemetry/verbosity.py +37 -0
- tnfr/tokens.py +1 -3
- tnfr/tokens.pyi +36 -0
- tnfr/trace.py +270 -113
- tnfr/trace.pyi +40 -0
- tnfr/types.py +574 -6
- tnfr/types.pyi +331 -0
- tnfr/units.py +69 -0
- tnfr/units.pyi +16 -0
- tnfr/utils/__init__.py +217 -0
- tnfr/utils/__init__.pyi +202 -0
- tnfr/utils/cache.py +2395 -0
- tnfr/utils/cache.pyi +468 -0
- tnfr/utils/chunks.py +104 -0
- tnfr/utils/chunks.pyi +21 -0
- tnfr/{collections_utils.py → utils/data.py} +147 -90
- tnfr/utils/data.pyi +64 -0
- tnfr/utils/graph.py +85 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +770 -0
- tnfr/utils/init.pyi +78 -0
- tnfr/utils/io.py +456 -0
- tnfr/{helpers → utils}/numeric.py +51 -24
- tnfr/utils/numeric.pyi +21 -0
- tnfr/validation/__init__.py +113 -0
- tnfr/validation/__init__.pyi +77 -0
- tnfr/validation/compatibility.py +95 -0
- tnfr/validation/compatibility.pyi +6 -0
- tnfr/validation/grammar.py +71 -0
- tnfr/validation/grammar.pyi +40 -0
- tnfr/validation/graph.py +138 -0
- tnfr/validation/graph.pyi +17 -0
- tnfr/validation/rules.py +281 -0
- tnfr/validation/rules.pyi +55 -0
- tnfr/validation/runtime.py +263 -0
- tnfr/validation/runtime.pyi +31 -0
- tnfr/validation/soft_filters.py +170 -0
- tnfr/validation/soft_filters.pyi +37 -0
- tnfr/validation/spectral.py +159 -0
- tnfr/validation/spectral.pyi +46 -0
- tnfr/validation/syntax.py +40 -0
- tnfr/validation/syntax.pyi +10 -0
- tnfr/validation/window.py +39 -0
- tnfr/validation/window.pyi +1 -0
- tnfr/viz/__init__.py +9 -0
- tnfr/viz/matplotlib.py +246 -0
- tnfr-7.0.0.dist-info/METADATA +179 -0
- tnfr-7.0.0.dist-info/RECORD +185 -0
- {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
- tnfr/grammar.py +0 -344
- tnfr/graph_utils.py +0 -84
- tnfr/helpers/__init__.py +0 -71
- tnfr/import_utils.py +0 -228
- tnfr/json_utils.py +0 -162
- tnfr/logging_utils.py +0 -116
- tnfr/presets.py +0 -60
- tnfr/validators.py +0 -84
- tnfr/value_utils.py +0 -59
- tnfr-4.5.2.dist-info/METADATA +0 -379
- tnfr-4.5.2.dist-info/RECORD +0 -67
- {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
tnfr/glyph_history.py
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from collections import
|
|
5
|
+
from collections import Counter, deque
|
|
6
|
+
from collections.abc import Iterable, Mapping, MutableMapping
|
|
7
7
|
from itertools import islice
|
|
8
|
-
from
|
|
9
|
-
from functools import lru_cache
|
|
8
|
+
from typing import Any, cast
|
|
10
9
|
|
|
11
|
-
from .constants import get_param
|
|
12
|
-
from .
|
|
13
|
-
from .
|
|
10
|
+
from .constants import get_param, normalise_state_token
|
|
11
|
+
from .glyph_runtime import last_glyph
|
|
12
|
+
from .types import TNFRGraph
|
|
13
|
+
from .utils import ensure_collection, get_logger
|
|
14
14
|
|
|
15
15
|
logger = get_logger(__name__)
|
|
16
16
|
|
|
@@ -21,28 +21,28 @@ __all__ = (
|
|
|
21
21
|
"ensure_history",
|
|
22
22
|
"current_step_idx",
|
|
23
23
|
"append_metric",
|
|
24
|
-
"last_glyph",
|
|
25
24
|
"count_glyphs",
|
|
26
25
|
)
|
|
27
26
|
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return _resolve_validate_window()(window, positive=positive)
|
|
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
|
+
)
|
|
38
36
|
|
|
39
37
|
|
|
40
38
|
def _ensure_history(
|
|
41
|
-
nd:
|
|
42
|
-
) -> tuple[int, deque | None]:
|
|
39
|
+
nd: MutableMapping[str, Any], window: int, *, create_zero: bool = False
|
|
40
|
+
) -> tuple[int, deque[str] | None]:
|
|
43
41
|
"""Validate ``window`` and ensure ``nd['glyph_history']`` deque."""
|
|
44
42
|
|
|
45
|
-
|
|
43
|
+
from tnfr.validation.window import validate_window
|
|
44
|
+
|
|
45
|
+
v_window = validate_window(window)
|
|
46
46
|
if v_window == 0 and not create_zero:
|
|
47
47
|
return v_window, None
|
|
48
48
|
hist = nd.setdefault("glyph_history", deque(maxlen=v_window))
|
|
@@ -54,16 +54,14 @@ def _ensure_history(
|
|
|
54
54
|
try:
|
|
55
55
|
items = ensure_collection(hist, max_materialize=None)
|
|
56
56
|
except TypeError:
|
|
57
|
-
logger.debug(
|
|
58
|
-
"Discarding non-iterable glyph history value %r", hist
|
|
59
|
-
)
|
|
57
|
+
logger.debug("Discarding non-iterable glyph history value %r", hist)
|
|
60
58
|
items = ()
|
|
61
|
-
hist = deque(items, maxlen=v_window)
|
|
59
|
+
hist = deque((str(item) for item in items), maxlen=v_window)
|
|
62
60
|
nd["glyph_history"] = hist
|
|
63
61
|
return v_window, hist
|
|
64
62
|
|
|
65
63
|
|
|
66
|
-
def push_glyph(nd:
|
|
64
|
+
def push_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> None:
|
|
67
65
|
"""Add ``glyph`` to node history with maximum size ``window``.
|
|
68
66
|
|
|
69
67
|
``window`` validation and deque creation are handled by
|
|
@@ -74,7 +72,7 @@ def push_glyph(nd: dict[str, Any], glyph: str, window: int) -> None:
|
|
|
74
72
|
hist.append(str(glyph))
|
|
75
73
|
|
|
76
74
|
|
|
77
|
-
def recent_glyph(nd:
|
|
75
|
+
def recent_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> bool:
|
|
78
76
|
"""Return ``True`` if ``glyph`` appeared in last ``window`` emissions.
|
|
79
77
|
|
|
80
78
|
``window`` validation and deque creation are handled by
|
|
@@ -89,7 +87,7 @@ def recent_glyph(nd: dict[str, Any], glyph: str, window: int) -> bool:
|
|
|
89
87
|
return gl in hist
|
|
90
88
|
|
|
91
89
|
|
|
92
|
-
class HistoryDict(dict):
|
|
90
|
+
class HistoryDict(dict[str, Any]):
|
|
93
91
|
"""Dict specialized for bounded history series and usage counts.
|
|
94
92
|
|
|
95
93
|
Usage counts are tracked explicitly via :meth:`get_increment`. Accessing
|
|
@@ -108,7 +106,7 @@ class HistoryDict(dict):
|
|
|
108
106
|
|
|
109
107
|
def __init__(
|
|
110
108
|
self,
|
|
111
|
-
data:
|
|
109
|
+
data: Mapping[str, Any] | None = None,
|
|
112
110
|
*,
|
|
113
111
|
maxlen: int = 0,
|
|
114
112
|
) -> None:
|
|
@@ -129,7 +127,7 @@ class HistoryDict(dict):
|
|
|
129
127
|
"""Increase usage count for ``key``."""
|
|
130
128
|
self._counts[key] += 1
|
|
131
129
|
|
|
132
|
-
def _to_deque(self, val: Any) -> deque:
|
|
130
|
+
def _to_deque(self, val: Any) -> deque[Any]:
|
|
133
131
|
"""Coerce ``val`` to a deque respecting ``self._maxlen``.
|
|
134
132
|
|
|
135
133
|
``Iterable`` inputs (excluding ``str`` and ``bytes``) are expanded into
|
|
@@ -155,26 +153,36 @@ class HistoryDict(dict):
|
|
|
155
153
|
return val
|
|
156
154
|
|
|
157
155
|
def get_increment(self, key: str, default: Any = None) -> Any:
|
|
156
|
+
"""Return value for ``key`` and increment its usage counter."""
|
|
157
|
+
|
|
158
158
|
insert = key not in self
|
|
159
159
|
val = self._resolve_value(key, default, insert=insert)
|
|
160
160
|
self._increment(key)
|
|
161
161
|
return val
|
|
162
162
|
|
|
163
|
-
def __getitem__(self, key): # type: ignore[override]
|
|
163
|
+
def __getitem__(self, key: str) -> Any: # type: ignore[override]
|
|
164
|
+
"""Return the tracked value for ``key`` ensuring deque normalisation."""
|
|
165
|
+
|
|
164
166
|
return self._resolve_value(key, None, insert=False)
|
|
165
167
|
|
|
166
|
-
def get(self, key, default=None): # type: ignore[override]
|
|
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
|
+
|
|
167
171
|
try:
|
|
168
172
|
return self._resolve_value(key, None, insert=False)
|
|
169
173
|
except KeyError:
|
|
170
174
|
return default
|
|
171
175
|
|
|
172
|
-
def __setitem__(self, key, value): # type: ignore[override]
|
|
176
|
+
def __setitem__(self, key: str, value: Any) -> None: # type: ignore[override]
|
|
177
|
+
"""Store ``value`` for ``key`` while initialising usage tracking."""
|
|
178
|
+
|
|
173
179
|
super().__setitem__(key, value)
|
|
174
180
|
if key not in self._counts:
|
|
175
181
|
self._counts[key] = 0
|
|
176
182
|
|
|
177
|
-
def setdefault(self, key, default=None): # type: ignore[override]
|
|
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
|
+
|
|
178
186
|
insert = key not in self
|
|
179
187
|
val = self._resolve_value(key, default, insert=insert)
|
|
180
188
|
if insert:
|
|
@@ -191,6 +199,8 @@ class HistoryDict(dict):
|
|
|
191
199
|
raise KeyError("HistoryDict is empty; cannot pop least used")
|
|
192
200
|
|
|
193
201
|
def pop_least_used_batch(self, k: int) -> None:
|
|
202
|
+
"""Remove up to ``k`` least-used entries from the history."""
|
|
203
|
+
|
|
194
204
|
for _ in range(max(0, int(k))):
|
|
195
205
|
try:
|
|
196
206
|
self.pop_least_used()
|
|
@@ -198,7 +208,7 @@ class HistoryDict(dict):
|
|
|
198
208
|
break
|
|
199
209
|
|
|
200
210
|
|
|
201
|
-
def ensure_history(G) -> dict[str, Any]:
|
|
211
|
+
def ensure_history(G: TNFRGraph) -> HistoryDict | dict[str, Any]:
|
|
202
212
|
"""Ensure ``G.graph['history']`` exists and return it.
|
|
203
213
|
|
|
204
214
|
``HISTORY_MAXLEN`` must be non-negative; otherwise a
|
|
@@ -220,11 +230,10 @@ def ensure_history(G) -> dict[str, Any]:
|
|
|
220
230
|
replaced = True
|
|
221
231
|
if replaced:
|
|
222
232
|
G.graph.pop(sentinel_key, None)
|
|
233
|
+
if isinstance(hist, MutableMapping):
|
|
234
|
+
_normalise_state_streams(hist)
|
|
223
235
|
return hist
|
|
224
|
-
if (
|
|
225
|
-
not isinstance(hist, HistoryDict)
|
|
226
|
-
or hist._maxlen != maxlen
|
|
227
|
-
):
|
|
236
|
+
if not isinstance(hist, HistoryDict) or hist._maxlen != maxlen:
|
|
228
237
|
hist = HistoryDict(hist, maxlen=maxlen)
|
|
229
238
|
G.graph["history"] = hist
|
|
230
239
|
replaced = True
|
|
@@ -233,31 +242,44 @@ def ensure_history(G) -> dict[str, Any]:
|
|
|
233
242
|
hist.pop_least_used_batch(excess)
|
|
234
243
|
if replaced:
|
|
235
244
|
G.graph.pop(sentinel_key, None)
|
|
245
|
+
_normalise_state_streams(cast(MutableMapping[str, Any], hist))
|
|
246
|
+
for key in _NU_F_HISTORY_KEYS:
|
|
247
|
+
hist.setdefault(key, [])
|
|
236
248
|
return hist
|
|
237
249
|
|
|
238
250
|
|
|
239
|
-
def current_step_idx(G) -> int:
|
|
251
|
+
def current_step_idx(G: TNFRGraph | Mapping[str, Any]) -> int:
|
|
240
252
|
"""Return the current step index from ``G`` history."""
|
|
241
253
|
|
|
242
254
|
graph = getattr(G, "graph", G)
|
|
243
255
|
return len(graph.get("history", {}).get("C_steps", []))
|
|
244
256
|
|
|
245
|
-
|
|
246
257
|
|
|
247
|
-
def append_metric(hist:
|
|
258
|
+
def append_metric(hist: MutableMapping[str, list[Any]], key: str, value: Any) -> None:
|
|
248
259
|
"""Append ``value`` to ``hist[key]`` list, creating it if missing."""
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
260
|
+
if key == "phase_state" and isinstance(value, str):
|
|
261
|
+
value = normalise_state_token(value)
|
|
262
|
+
elif key == "nodal_diag" and isinstance(value, Mapping):
|
|
263
|
+
snapshot: dict[Any, Any] = {}
|
|
264
|
+
for node, payload in value.items():
|
|
265
|
+
if isinstance(payload, Mapping):
|
|
266
|
+
state_value = payload.get("state")
|
|
267
|
+
if isinstance(payload, MutableMapping):
|
|
268
|
+
updated = payload
|
|
269
|
+
else:
|
|
270
|
+
updated = dict(payload)
|
|
271
|
+
if isinstance(state_value, str):
|
|
272
|
+
updated["state"] = normalise_state_token(state_value)
|
|
273
|
+
snapshot[node] = updated
|
|
274
|
+
else:
|
|
275
|
+
snapshot[node] = payload
|
|
276
|
+
hist.setdefault(key, []).append(snapshot)
|
|
277
|
+
return
|
|
257
278
|
|
|
279
|
+
hist.setdefault(key, []).append(value)
|
|
258
280
|
def count_glyphs(
|
|
259
|
-
G, window: int | None = None, *, last_only: bool = False
|
|
260
|
-
) -> Counter:
|
|
281
|
+
G: TNFRGraph, window: int | None = None, *, last_only: bool = False
|
|
282
|
+
) -> Counter[str]:
|
|
261
283
|
"""Count recent glyphs in the network.
|
|
262
284
|
|
|
263
285
|
If ``window`` is ``None``, the full history for each node is used. A
|
|
@@ -266,7 +288,9 @@ def count_glyphs(
|
|
|
266
288
|
"""
|
|
267
289
|
|
|
268
290
|
if window is not None:
|
|
269
|
-
window
|
|
291
|
+
from tnfr.validation.window import validate_window
|
|
292
|
+
|
|
293
|
+
window = validate_window(window)
|
|
270
294
|
if window == 0:
|
|
271
295
|
return Counter()
|
|
272
296
|
|
|
@@ -288,3 +312,37 @@ def count_glyphs(
|
|
|
288
312
|
counts.update(seq)
|
|
289
313
|
|
|
290
314
|
return counts
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _normalise_state_streams(hist: MutableMapping[str, Any]) -> None:
|
|
318
|
+
"""Normalise legacy state tokens stored in telemetry history."""
|
|
319
|
+
|
|
320
|
+
phase_state = hist.get("phase_state")
|
|
321
|
+
if isinstance(phase_state, deque):
|
|
322
|
+
canonical = [normalise_state_token(str(item)) for item in phase_state]
|
|
323
|
+
if canonical != list(phase_state):
|
|
324
|
+
phase_state.clear()
|
|
325
|
+
phase_state.extend(canonical)
|
|
326
|
+
elif isinstance(phase_state, list):
|
|
327
|
+
canonical = [normalise_state_token(str(item)) for item in phase_state]
|
|
328
|
+
if canonical != phase_state:
|
|
329
|
+
hist["phase_state"] = canonical
|
|
330
|
+
|
|
331
|
+
diag_history = hist.get("nodal_diag")
|
|
332
|
+
if isinstance(diag_history, list):
|
|
333
|
+
for snapshot in diag_history:
|
|
334
|
+
if not isinstance(snapshot, Mapping):
|
|
335
|
+
continue
|
|
336
|
+
for node, payload in snapshot.items():
|
|
337
|
+
if not isinstance(payload, Mapping):
|
|
338
|
+
continue
|
|
339
|
+
state_value = payload.get("state")
|
|
340
|
+
if not isinstance(state_value, str):
|
|
341
|
+
continue
|
|
342
|
+
canonical = normalise_state_token(state_value)
|
|
343
|
+
if canonical == state_value:
|
|
344
|
+
continue
|
|
345
|
+
if isinstance(payload, MutableMapping):
|
|
346
|
+
payload["state"] = canonical
|
|
347
|
+
else:
|
|
348
|
+
snapshot[node] = {**payload, "state": canonical}
|
tnfr/glyph_history.pyi
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from collections.abc import Mapping, MutableMapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .types import TNFRGraph
|
|
8
|
+
|
|
9
|
+
__all__: tuple[str, ...]
|
|
10
|
+
|
|
11
|
+
class HistoryDict(dict[str, Any]):
|
|
12
|
+
_maxlen: int
|
|
13
|
+
_counts: Counter[str]
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self, data: Mapping[str, Any] | None = ..., *, maxlen: int = ...
|
|
17
|
+
) -> None: ...
|
|
18
|
+
def get_increment(self, key: str, default: Any = ...) -> Any: ...
|
|
19
|
+
def __getitem__(self, key: str) -> Any: ...
|
|
20
|
+
def get(self, key: str, default: Any | None = ...) -> Any: ...
|
|
21
|
+
def __setitem__(self, key: str, value: Any) -> None: ...
|
|
22
|
+
def setdefault(self, key: str, default: Any | None = ...) -> Any: ...
|
|
23
|
+
def pop_least_used(self) -> Any: ...
|
|
24
|
+
def pop_least_used_batch(self, k: int) -> None: ...
|
|
25
|
+
|
|
26
|
+
def push_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> None: ...
|
|
27
|
+
def recent_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> bool: ...
|
|
28
|
+
def ensure_history(G: TNFRGraph) -> HistoryDict | dict[str, Any]: ...
|
|
29
|
+
def current_step_idx(G: TNFRGraph | Mapping[str, Any]) -> int: ...
|
|
30
|
+
def append_metric(
|
|
31
|
+
hist: MutableMapping[str, list[Any]], key: str, value: Any
|
|
32
|
+
) -> None: ...
|
|
33
|
+
def count_glyphs(
|
|
34
|
+
G: TNFRGraph, window: int | None = ..., *, last_only: bool = ...
|
|
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
tnfr/immutable.py
CHANGED
|
@@ -7,23 +7,47 @@ 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
|
-
from functools import lru_cache, singledispatch, wraps
|
|
13
|
-
from typing import Any, Callable
|
|
14
|
-
from collections.abc import Mapping
|
|
15
|
+
from functools import lru_cache, partial, singledispatch, wraps
|
|
15
16
|
from types import MappingProxyType
|
|
16
|
-
import
|
|
17
|
-
|
|
17
|
+
from typing import Any, Callable, Iterable, Iterator, cast
|
|
18
|
+
|
|
19
|
+
from ._compat import TypeAlias
|
|
18
20
|
|
|
19
21
|
# Types considered immutable without further inspection
|
|
20
|
-
IMMUTABLE_SIMPLE = frozenset(
|
|
21
|
-
|
|
22
|
+
IMMUTABLE_SIMPLE = frozenset({int, float, complex, str, bool, bytes, type(None)})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
FrozenPrimitive: TypeAlias = int | float | complex | str | bool | bytes | None
|
|
26
|
+
"""Primitive immutable values handled directly by :func:`_freeze`."""
|
|
27
|
+
|
|
28
|
+
FrozenCollectionItems: TypeAlias = tuple["FrozenSnapshot", ...]
|
|
29
|
+
"""Frozen representation for generic iterables."""
|
|
30
|
+
|
|
31
|
+
FrozenMappingItems: TypeAlias = tuple[tuple[Any, "FrozenSnapshot"], ...]
|
|
32
|
+
"""Frozen representation for mapping ``items()`` snapshots."""
|
|
33
|
+
|
|
34
|
+
FrozenTaggedCollection: TypeAlias = tuple[str, FrozenCollectionItems]
|
|
35
|
+
"""Tagged iterable snapshot identifying the original container type."""
|
|
36
|
+
|
|
37
|
+
FrozenTaggedMapping: TypeAlias = tuple[str, FrozenMappingItems]
|
|
38
|
+
"""Tagged mapping snapshot identifying the original mapping flavour."""
|
|
39
|
+
|
|
40
|
+
FrozenSnapshot: TypeAlias = (
|
|
41
|
+
FrozenPrimitive
|
|
42
|
+
| FrozenCollectionItems
|
|
43
|
+
| FrozenTaggedCollection
|
|
44
|
+
| FrozenTaggedMapping
|
|
22
45
|
)
|
|
46
|
+
"""Union describing the immutable snapshot returned by :func:`_freeze`."""
|
|
23
47
|
|
|
24
48
|
|
|
25
49
|
@contextmanager
|
|
26
|
-
def _cycle_guard(value: Any, seen: set[int] | None = None):
|
|
50
|
+
def _cycle_guard(value: Any, seen: set[int] | None = None) -> Iterator[set[int]]:
|
|
27
51
|
"""Context manager that detects reference cycles during freezing."""
|
|
28
52
|
if seen is None:
|
|
29
53
|
seen = set()
|
|
@@ -37,18 +61,20 @@ def _cycle_guard(value: Any, seen: set[int] | None = None):
|
|
|
37
61
|
seen.remove(obj_id)
|
|
38
62
|
|
|
39
63
|
|
|
40
|
-
def _check_cycle(
|
|
41
|
-
|
|
64
|
+
def _check_cycle(
|
|
65
|
+
func: Callable[[Any, set[int] | None], FrozenSnapshot],
|
|
66
|
+
) -> Callable[[Any, set[int] | None], FrozenSnapshot]:
|
|
67
|
+
"""Apply :func:`_cycle_guard` to ``func``."""
|
|
42
68
|
|
|
43
69
|
@wraps(func)
|
|
44
|
-
def wrapper(value: Any, seen: set[int] | None = None):
|
|
45
|
-
with _cycle_guard(value, seen) as
|
|
46
|
-
return func(value,
|
|
70
|
+
def wrapper(value: Any, seen: set[int] | None = None) -> FrozenSnapshot:
|
|
71
|
+
with _cycle_guard(value, seen) as guard_seen:
|
|
72
|
+
return func(value, guard_seen)
|
|
47
73
|
|
|
48
74
|
return wrapper
|
|
49
75
|
|
|
50
76
|
|
|
51
|
-
def _freeze_dataclass(value: Any, seen: set[int]):
|
|
77
|
+
def _freeze_dataclass(value: Any, seen: set[int]) -> FrozenTaggedMapping:
|
|
52
78
|
params = getattr(type(value), "__dataclass_params__", None)
|
|
53
79
|
frozen = bool(params and params.frozen)
|
|
54
80
|
data = asdict(value)
|
|
@@ -58,9 +84,10 @@ def _freeze_dataclass(value: Any, seen: set[int]):
|
|
|
58
84
|
|
|
59
85
|
@singledispatch
|
|
60
86
|
@_check_cycle
|
|
61
|
-
def _freeze(value: Any, seen: set[int] | None = None):
|
|
87
|
+
def _freeze(value: Any, seen: set[int] | None = None) -> FrozenSnapshot:
|
|
62
88
|
"""Recursively convert ``value`` into an immutable representation."""
|
|
63
89
|
if is_dataclass(value) and not isinstance(value, type):
|
|
90
|
+
assert seen is not None
|
|
64
91
|
return _freeze_dataclass(value, seen)
|
|
65
92
|
if type(value) in IMMUTABLE_SIMPLE:
|
|
66
93
|
return value
|
|
@@ -69,22 +96,31 @@ def _freeze(value: Any, seen: set[int] | None = None):
|
|
|
69
96
|
|
|
70
97
|
@_freeze.register(tuple)
|
|
71
98
|
@_check_cycle
|
|
72
|
-
def _freeze_tuple(
|
|
99
|
+
def _freeze_tuple(
|
|
100
|
+
value: tuple[Any, ...], seen: set[int] | None = None
|
|
101
|
+
) -> FrozenCollectionItems: # noqa: F401
|
|
102
|
+
assert seen is not None
|
|
73
103
|
return tuple(_freeze(v, seen) for v in value)
|
|
74
104
|
|
|
75
105
|
|
|
76
|
-
def _freeze_iterable(
|
|
106
|
+
def _freeze_iterable(
|
|
107
|
+
container: Iterable[Any], tag: str, seen: set[int]
|
|
108
|
+
) -> FrozenTaggedCollection:
|
|
77
109
|
return (tag, tuple(_freeze(v, seen) for v in container))
|
|
78
110
|
|
|
79
111
|
|
|
80
112
|
def _freeze_iterable_with_tag(
|
|
81
|
-
value: Any, seen: set[int] | None = None, *, tag: str
|
|
82
|
-
) ->
|
|
113
|
+
value: Iterable[Any], seen: set[int] | None = None, *, tag: str
|
|
114
|
+
) -> FrozenTaggedCollection:
|
|
115
|
+
assert seen is not None
|
|
83
116
|
return _freeze_iterable(value, tag, seen)
|
|
84
117
|
|
|
85
118
|
|
|
86
119
|
def _register_iterable(cls: type, tag: str) -> None:
|
|
87
|
-
|
|
120
|
+
handler = _check_cycle(partial(_freeze_iterable_with_tag, tag=tag))
|
|
121
|
+
_freeze.register(cls)(
|
|
122
|
+
cast(Callable[[Any, set[int] | None], FrozenSnapshot], handler)
|
|
123
|
+
)
|
|
88
124
|
|
|
89
125
|
|
|
90
126
|
for _cls, _tag in (
|
|
@@ -98,17 +134,22 @@ for _cls, _tag in (
|
|
|
98
134
|
|
|
99
135
|
@_freeze.register(Mapping)
|
|
100
136
|
@_check_cycle
|
|
101
|
-
def _freeze_mapping(
|
|
137
|
+
def _freeze_mapping(
|
|
138
|
+
value: Mapping[Any, Any], seen: set[int] | None = None
|
|
139
|
+
) -> FrozenTaggedMapping: # noqa: F401
|
|
140
|
+
assert seen is not None
|
|
102
141
|
tag = "dict" if hasattr(value, "__setitem__") else "mapping"
|
|
103
142
|
return (tag, tuple((k, _freeze(v, seen)) for k, v in value.items()))
|
|
104
143
|
|
|
105
144
|
|
|
106
|
-
def _all_immutable(iterable) -> bool:
|
|
145
|
+
def _all_immutable(iterable: Iterable[Any]) -> bool:
|
|
107
146
|
return all(_is_immutable_inner(v) for v in iterable)
|
|
108
147
|
|
|
109
148
|
|
|
110
149
|
# Dispatch table kept immutable to avoid accidental mutation.
|
|
111
|
-
|
|
150
|
+
ImmutableTagHandler: TypeAlias = Callable[[tuple[Any, ...]], bool]
|
|
151
|
+
|
|
152
|
+
_IMMUTABLE_TAG_DISPATCH: Mapping[str, ImmutableTagHandler] = MappingProxyType(
|
|
112
153
|
{
|
|
113
154
|
"mapping": lambda v: _all_immutable(v[1]),
|
|
114
155
|
"frozenset": lambda v: _all_immutable(v[1]),
|
|
@@ -123,11 +164,13 @@ _IMMUTABLE_TAG_DISPATCH: Mapping[str, Callable[[tuple], bool]] = MappingProxyTyp
|
|
|
123
164
|
@lru_cache(maxsize=1024)
|
|
124
165
|
@singledispatch
|
|
125
166
|
def _is_immutable_inner(value: Any) -> bool:
|
|
167
|
+
"""Return ``True`` when ``value`` belongs to the canonical immutable set."""
|
|
168
|
+
|
|
126
169
|
return type(value) in IMMUTABLE_SIMPLE
|
|
127
170
|
|
|
128
171
|
|
|
129
172
|
@_is_immutable_inner.register(tuple)
|
|
130
|
-
def _is_immutable_inner_tuple(value: tuple) -> bool: # noqa: F401
|
|
173
|
+
def _is_immutable_inner_tuple(value: tuple[Any, ...]) -> bool: # noqa: F401
|
|
131
174
|
if value and isinstance(value[0], str):
|
|
132
175
|
handler = _IMMUTABLE_TAG_DISPATCH.get(value[0])
|
|
133
176
|
if handler is not None:
|
|
@@ -136,13 +179,11 @@ def _is_immutable_inner_tuple(value: tuple) -> bool: # noqa: F401
|
|
|
136
179
|
|
|
137
180
|
|
|
138
181
|
@_is_immutable_inner.register(frozenset)
|
|
139
|
-
def _is_immutable_inner_frozenset(value: frozenset) -> bool: # noqa: F401
|
|
182
|
+
def _is_immutable_inner_frozenset(value: frozenset[Any]) -> bool: # noqa: F401
|
|
140
183
|
return _all_immutable(value)
|
|
141
184
|
|
|
142
185
|
|
|
143
|
-
_IMMUTABLE_CACHE: weakref.WeakKeyDictionary[Any, bool] = (
|
|
144
|
-
weakref.WeakKeyDictionary()
|
|
145
|
-
)
|
|
186
|
+
_IMMUTABLE_CACHE: weakref.WeakKeyDictionary[Any, bool] = weakref.WeakKeyDictionary()
|
|
146
187
|
_IMMUTABLE_CACHE_LOCK = threading.Lock()
|
|
147
188
|
|
|
148
189
|
|
tnfr/immutable.pyi
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Any, Callable, Iterator, Mapping
|
|
2
|
+
|
|
3
|
+
from ._compat import TypeAlias
|
|
4
|
+
|
|
5
|
+
FrozenPrimitive: TypeAlias = int | float | complex | str | bool | bytes | None
|
|
6
|
+
FrozenCollectionItems: TypeAlias = tuple["FrozenSnapshot", ...]
|
|
7
|
+
FrozenMappingItems: TypeAlias = tuple[tuple[Any, "FrozenSnapshot"], ...]
|
|
8
|
+
FrozenTaggedCollection: TypeAlias = tuple[str, FrozenCollectionItems]
|
|
9
|
+
FrozenTaggedMapping: TypeAlias = tuple[str, FrozenMappingItems]
|
|
10
|
+
FrozenSnapshot: TypeAlias = (
|
|
11
|
+
FrozenPrimitive
|
|
12
|
+
| FrozenCollectionItems
|
|
13
|
+
| FrozenTaggedCollection
|
|
14
|
+
| FrozenTaggedMapping
|
|
15
|
+
)
|
|
16
|
+
ImmutableTagHandler: TypeAlias = Callable[[tuple[Any, ...]], bool]
|
|
17
|
+
|
|
18
|
+
__all__: tuple[str, ...]
|
|
19
|
+
|
|
20
|
+
def __getattr__(name: str) -> Any: ...
|
|
21
|
+
def _cycle_guard(value: Any, seen: set[int] | None = ...) -> Iterator[set[int]]: ...
|
|
22
|
+
def _check_cycle(
|
|
23
|
+
func: Callable[[Any, set[int] | None], FrozenSnapshot],
|
|
24
|
+
) -> Callable[[Any, set[int] | None], FrozenSnapshot]: ...
|
|
25
|
+
def _freeze(value: Any, seen: set[int] | None = ...) -> FrozenSnapshot: ...
|
|
26
|
+
def _freeze_mapping(
|
|
27
|
+
value: Mapping[Any, Any],
|
|
28
|
+
seen: set[int] | None = ...,
|
|
29
|
+
) -> FrozenTaggedMapping: ...
|
|
30
|
+
def _is_immutable(value: Any) -> bool: ...
|
|
31
|
+
def _is_immutable_inner(value: Any) -> bool: ...
|
|
32
|
+
|
|
33
|
+
_IMMUTABLE_CACHE: Any
|
|
34
|
+
_IMMUTABLE_TAG_DISPATCH: Mapping[str, ImmutableTagHandler]
|