tnfr 4.5.1__py3-none-any.whl → 6.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.
- tnfr/__init__.py +270 -90
- tnfr/__init__.pyi +40 -0
- tnfr/_compat.py +11 -0
- tnfr/_version.py +7 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +631 -0
- tnfr/alias.pyi +140 -0
- tnfr/cache.py +732 -0
- tnfr/cache.pyi +232 -0
- tnfr/callback_utils.py +381 -0
- tnfr/callback_utils.pyi +105 -0
- tnfr/cli/__init__.py +89 -0
- tnfr/cli/__init__.pyi +47 -0
- tnfr/cli/arguments.py +199 -0
- tnfr/cli/arguments.pyi +33 -0
- tnfr/cli/execution.py +322 -0
- tnfr/cli/execution.pyi +80 -0
- tnfr/cli/utils.py +34 -0
- tnfr/cli/utils.pyi +8 -0
- tnfr/config/__init__.py +12 -0
- tnfr/config/__init__.pyi +8 -0
- tnfr/config/constants.py +104 -0
- tnfr/config/constants.pyi +12 -0
- tnfr/config/init.py +36 -0
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +106 -0
- tnfr/config/operator_names.pyi +28 -0
- tnfr/config/presets.py +104 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/constants/__init__.py +228 -0
- tnfr/constants/__init__.pyi +104 -0
- tnfr/constants/core.py +158 -0
- tnfr/constants/core.pyi +17 -0
- tnfr/constants/init.py +31 -0
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +102 -0
- tnfr/constants/metric.pyi +19 -0
- tnfr/constants_glyphs.py +16 -0
- tnfr/constants_glyphs.pyi +12 -0
- tnfr/dynamics/__init__.py +136 -0
- tnfr/dynamics/__init__.pyi +83 -0
- tnfr/dynamics/adaptation.py +201 -0
- tnfr/dynamics/aliases.py +22 -0
- tnfr/dynamics/coordination.py +343 -0
- tnfr/dynamics/dnfr.py +2315 -0
- tnfr/dynamics/dnfr.pyi +33 -0
- tnfr/dynamics/integrators.py +561 -0
- tnfr/dynamics/integrators.pyi +35 -0
- tnfr/dynamics/runtime.py +521 -0
- tnfr/dynamics/sampling.py +34 -0
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +680 -0
- tnfr/execution.py +216 -0
- tnfr/execution.pyi +65 -0
- tnfr/flatten.py +283 -0
- tnfr/flatten.pyi +28 -0
- tnfr/gamma.py +320 -89
- tnfr/gamma.pyi +40 -0
- tnfr/glyph_history.py +337 -0
- tnfr/glyph_history.pyi +53 -0
- tnfr/grammar.py +23 -153
- tnfr/grammar.pyi +13 -0
- tnfr/helpers/__init__.py +151 -0
- tnfr/helpers/__init__.pyi +66 -0
- tnfr/helpers/numeric.py +88 -0
- tnfr/helpers/numeric.pyi +12 -0
- tnfr/immutable.py +214 -0
- tnfr/immutable.pyi +37 -0
- tnfr/initialization.py +199 -0
- tnfr/initialization.pyi +73 -0
- tnfr/io.py +311 -0
- tnfr/io.pyi +11 -0
- tnfr/locking.py +37 -0
- tnfr/locking.pyi +7 -0
- tnfr/metrics/__init__.py +41 -0
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/coherence.py +1469 -0
- tnfr/metrics/common.py +149 -0
- tnfr/metrics/common.pyi +15 -0
- tnfr/metrics/core.py +259 -0
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +840 -0
- tnfr/metrics/diagnosis.pyi +89 -0
- tnfr/metrics/export.py +151 -0
- tnfr/metrics/glyph_timing.py +369 -0
- tnfr/metrics/reporting.py +152 -0
- tnfr/metrics/reporting.pyi +12 -0
- tnfr/metrics/sense_index.py +294 -0
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +216 -0
- tnfr/metrics/trig.pyi +12 -0
- tnfr/metrics/trig_cache.py +105 -0
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/node.py +255 -177
- tnfr/node.pyi +161 -0
- tnfr/observers.py +154 -150
- tnfr/observers.pyi +46 -0
- tnfr/ontosim.py +135 -134
- tnfr/ontosim.pyi +33 -0
- tnfr/operators/__init__.py +452 -0
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/definitions.py +181 -0
- tnfr/operators/definitions.pyi +92 -0
- tnfr/operators/jitter.py +266 -0
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/registry.py +80 -0
- tnfr/operators/registry.pyi +15 -0
- tnfr/operators/remesh.py +569 -0
- tnfr/presets.py +10 -23
- tnfr/presets.pyi +7 -0
- tnfr/py.typed +0 -0
- tnfr/rng.py +440 -0
- tnfr/rng.pyi +14 -0
- tnfr/selector.py +217 -0
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +307 -142
- tnfr/sense.pyi +30 -0
- tnfr/structural.py +69 -164
- tnfr/structural.pyi +46 -0
- tnfr/telemetry/__init__.py +13 -0
- tnfr/telemetry/verbosity.py +37 -0
- tnfr/tokens.py +61 -0
- tnfr/tokens.pyi +41 -0
- tnfr/trace.py +520 -95
- tnfr/trace.pyi +68 -0
- tnfr/types.py +382 -17
- tnfr/types.pyi +145 -0
- tnfr/utils/__init__.py +158 -0
- tnfr/utils/__init__.pyi +133 -0
- tnfr/utils/cache.py +755 -0
- tnfr/utils/cache.pyi +156 -0
- tnfr/utils/data.py +267 -0
- tnfr/utils/data.pyi +73 -0
- tnfr/utils/graph.py +87 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +746 -0
- tnfr/utils/init.pyi +85 -0
- tnfr/utils/io.py +157 -0
- tnfr/utils/io.pyi +10 -0
- tnfr/utils/validators.py +130 -0
- tnfr/utils/validators.pyi +19 -0
- tnfr/validation/__init__.py +25 -0
- tnfr/validation/__init__.pyi +17 -0
- tnfr/validation/compatibility.py +59 -0
- tnfr/validation/compatibility.pyi +8 -0
- tnfr/validation/grammar.py +149 -0
- tnfr/validation/grammar.pyi +11 -0
- tnfr/validation/rules.py +194 -0
- tnfr/validation/rules.pyi +18 -0
- tnfr/validation/syntax.py +151 -0
- tnfr/validation/syntax.pyi +7 -0
- tnfr-6.0.0.dist-info/METADATA +135 -0
- tnfr-6.0.0.dist-info/RECORD +157 -0
- tnfr/cli.py +0 -322
- tnfr/config.py +0 -41
- 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/validators.py +0 -38
- 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-6.0.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/glyph_history.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Utilities for tracking glyph emission history and related metrics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
from collections import deque, Counter
|
|
7
|
+
from itertools import islice
|
|
8
|
+
from collections.abc import Iterable, Mapping, MutableMapping
|
|
9
|
+
|
|
10
|
+
from .constants import get_param, normalise_state_token
|
|
11
|
+
from .utils import ensure_collection, get_logger, validate_window
|
|
12
|
+
from .types import TNFRGraph
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
__all__ = (
|
|
17
|
+
"HistoryDict",
|
|
18
|
+
"push_glyph",
|
|
19
|
+
"recent_glyph",
|
|
20
|
+
"ensure_history",
|
|
21
|
+
"current_step_idx",
|
|
22
|
+
"append_metric",
|
|
23
|
+
"last_glyph",
|
|
24
|
+
"count_glyphs",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _ensure_history(
|
|
29
|
+
nd: MutableMapping[str, Any], window: int, *, create_zero: bool = False
|
|
30
|
+
) -> tuple[int, deque[str] | None]:
|
|
31
|
+
"""Validate ``window`` and ensure ``nd['glyph_history']`` deque."""
|
|
32
|
+
|
|
33
|
+
v_window = validate_window(window)
|
|
34
|
+
if v_window == 0 and not create_zero:
|
|
35
|
+
return v_window, None
|
|
36
|
+
hist = nd.setdefault("glyph_history", deque(maxlen=v_window))
|
|
37
|
+
if not isinstance(hist, deque) or hist.maxlen != v_window:
|
|
38
|
+
# Rebuild deque from any iterable, ignoring raw strings/bytes and scalars
|
|
39
|
+
if isinstance(hist, (str, bytes, bytearray)):
|
|
40
|
+
items: Iterable[Any] = ()
|
|
41
|
+
else:
|
|
42
|
+
try:
|
|
43
|
+
items = ensure_collection(hist, max_materialize=None)
|
|
44
|
+
except TypeError:
|
|
45
|
+
logger.debug(
|
|
46
|
+
"Discarding non-iterable glyph history value %r", hist
|
|
47
|
+
)
|
|
48
|
+
items = ()
|
|
49
|
+
hist = deque((str(item) for item in items), maxlen=v_window)
|
|
50
|
+
nd["glyph_history"] = hist
|
|
51
|
+
return v_window, hist
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def push_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> None:
|
|
55
|
+
"""Add ``glyph`` to node history with maximum size ``window``.
|
|
56
|
+
|
|
57
|
+
``window`` validation and deque creation are handled by
|
|
58
|
+
:func:`_ensure_history`.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
_, hist = _ensure_history(nd, window, create_zero=True)
|
|
62
|
+
hist.append(str(glyph))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def recent_glyph(
|
|
66
|
+
nd: MutableMapping[str, Any], glyph: str, window: int
|
|
67
|
+
) -> bool:
|
|
68
|
+
"""Return ``True`` if ``glyph`` appeared in last ``window`` emissions.
|
|
69
|
+
|
|
70
|
+
``window`` validation and deque creation are handled by
|
|
71
|
+
:func:`_ensure_history`. A ``window`` of zero returns ``False`` and
|
|
72
|
+
leaves ``nd`` unchanged. Negative values raise :class:`ValueError`.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
v_window, hist = _ensure_history(nd, window)
|
|
76
|
+
if v_window == 0:
|
|
77
|
+
return False
|
|
78
|
+
gl = str(glyph)
|
|
79
|
+
return gl in hist
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class HistoryDict(dict[str, Any]):
|
|
83
|
+
"""Dict specialized for bounded history series and usage counts.
|
|
84
|
+
|
|
85
|
+
Usage counts are tracked explicitly via :meth:`get_increment`. Accessing
|
|
86
|
+
keys through ``__getitem__`` or :meth:`get` does not affect the internal
|
|
87
|
+
counters, avoiding surprising evictions on mere reads. Counting is now
|
|
88
|
+
handled with :class:`collections.Counter` alone, relying on
|
|
89
|
+
:meth:`Counter.most_common` to locate least-used entries when required.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
data:
|
|
94
|
+
Initial mapping to populate the dictionary.
|
|
95
|
+
maxlen:
|
|
96
|
+
Maximum length for history lists stored as values.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
data: Mapping[str, Any] | None = None,
|
|
102
|
+
*,
|
|
103
|
+
maxlen: int = 0,
|
|
104
|
+
) -> None:
|
|
105
|
+
super().__init__(data or {})
|
|
106
|
+
self._maxlen = maxlen
|
|
107
|
+
self._counts: Counter[str] = Counter()
|
|
108
|
+
if self._maxlen > 0:
|
|
109
|
+
for k, v in list(self.items()):
|
|
110
|
+
if isinstance(v, list):
|
|
111
|
+
super().__setitem__(k, deque(v, maxlen=self._maxlen))
|
|
112
|
+
self._counts[k] = 0
|
|
113
|
+
else:
|
|
114
|
+
for k in self:
|
|
115
|
+
self._counts[k] = 0
|
|
116
|
+
# ``_heap`` is no longer required with ``Counter.most_common``.
|
|
117
|
+
|
|
118
|
+
def _increment(self, key: str) -> None:
|
|
119
|
+
"""Increase usage count for ``key``."""
|
|
120
|
+
self._counts[key] += 1
|
|
121
|
+
|
|
122
|
+
def _to_deque(self, val: Any) -> deque[Any]:
|
|
123
|
+
"""Coerce ``val`` to a deque respecting ``self._maxlen``.
|
|
124
|
+
|
|
125
|
+
``Iterable`` inputs (excluding ``str`` and ``bytes``) are expanded into
|
|
126
|
+
the deque, while single values are wrapped. Existing deques are
|
|
127
|
+
returned unchanged.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
if isinstance(val, deque):
|
|
131
|
+
return val
|
|
132
|
+
if isinstance(val, Iterable) and not isinstance(val, (str, bytes)):
|
|
133
|
+
return deque(val, maxlen=self._maxlen)
|
|
134
|
+
return deque([val], maxlen=self._maxlen)
|
|
135
|
+
|
|
136
|
+
def _resolve_value(self, key: str, default: Any, *, insert: bool) -> Any:
|
|
137
|
+
if insert:
|
|
138
|
+
val = super().setdefault(key, default)
|
|
139
|
+
else:
|
|
140
|
+
val = super().__getitem__(key)
|
|
141
|
+
if self._maxlen > 0:
|
|
142
|
+
if not isinstance(val, Mapping):
|
|
143
|
+
val = self._to_deque(val)
|
|
144
|
+
super().__setitem__(key, val)
|
|
145
|
+
return val
|
|
146
|
+
|
|
147
|
+
def get_increment(self, key: str, default: Any = None) -> Any:
|
|
148
|
+
insert = key not in self
|
|
149
|
+
val = self._resolve_value(key, default, insert=insert)
|
|
150
|
+
self._increment(key)
|
|
151
|
+
return val
|
|
152
|
+
|
|
153
|
+
def __getitem__(self, key: str) -> Any: # type: ignore[override]
|
|
154
|
+
return self._resolve_value(key, None, insert=False)
|
|
155
|
+
|
|
156
|
+
def get(self, key: str, default: Any | None = None) -> Any: # type: ignore[override]
|
|
157
|
+
try:
|
|
158
|
+
return self._resolve_value(key, None, insert=False)
|
|
159
|
+
except KeyError:
|
|
160
|
+
return default
|
|
161
|
+
|
|
162
|
+
def __setitem__(self, key: str, value: Any) -> None: # type: ignore[override]
|
|
163
|
+
super().__setitem__(key, value)
|
|
164
|
+
if key not in self._counts:
|
|
165
|
+
self._counts[key] = 0
|
|
166
|
+
|
|
167
|
+
def setdefault(self, key: str, default: Any | None = None) -> Any: # type: ignore[override]
|
|
168
|
+
insert = key not in self
|
|
169
|
+
val = self._resolve_value(key, default, insert=insert)
|
|
170
|
+
if insert:
|
|
171
|
+
self._counts[key] = 0
|
|
172
|
+
return val
|
|
173
|
+
|
|
174
|
+
def pop_least_used(self) -> Any:
|
|
175
|
+
"""Remove and return the value with the smallest usage count."""
|
|
176
|
+
while self._counts:
|
|
177
|
+
key = min(self._counts, key=self._counts.get)
|
|
178
|
+
self._counts.pop(key, None)
|
|
179
|
+
if key in self:
|
|
180
|
+
return super().pop(key)
|
|
181
|
+
raise KeyError("HistoryDict is empty; cannot pop least used")
|
|
182
|
+
|
|
183
|
+
def pop_least_used_batch(self, k: int) -> None:
|
|
184
|
+
for _ in range(max(0, int(k))):
|
|
185
|
+
try:
|
|
186
|
+
self.pop_least_used()
|
|
187
|
+
except KeyError:
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def ensure_history(G: TNFRGraph) -> HistoryDict | dict[str, Any]:
|
|
192
|
+
"""Ensure ``G.graph['history']`` exists and return it.
|
|
193
|
+
|
|
194
|
+
``HISTORY_MAXLEN`` must be non-negative; otherwise a
|
|
195
|
+
:class:`ValueError` is raised. When ``HISTORY_MAXLEN`` is zero, a regular
|
|
196
|
+
``dict`` is used.
|
|
197
|
+
"""
|
|
198
|
+
maxlen, _ = _ensure_history({}, int(get_param(G, "HISTORY_MAXLEN")))
|
|
199
|
+
hist = G.graph.get("history")
|
|
200
|
+
sentinel_key = "_metrics_history_id"
|
|
201
|
+
replaced = False
|
|
202
|
+
if maxlen == 0:
|
|
203
|
+
if isinstance(hist, HistoryDict):
|
|
204
|
+
hist = dict(hist)
|
|
205
|
+
G.graph["history"] = hist
|
|
206
|
+
replaced = True
|
|
207
|
+
elif hist is None:
|
|
208
|
+
hist = {}
|
|
209
|
+
G.graph["history"] = hist
|
|
210
|
+
replaced = True
|
|
211
|
+
if replaced:
|
|
212
|
+
G.graph.pop(sentinel_key, None)
|
|
213
|
+
if isinstance(hist, MutableMapping):
|
|
214
|
+
_normalise_state_streams(hist)
|
|
215
|
+
return hist
|
|
216
|
+
if (
|
|
217
|
+
not isinstance(hist, HistoryDict)
|
|
218
|
+
or hist._maxlen != maxlen
|
|
219
|
+
):
|
|
220
|
+
hist = HistoryDict(hist, maxlen=maxlen)
|
|
221
|
+
G.graph["history"] = hist
|
|
222
|
+
replaced = True
|
|
223
|
+
excess = len(hist) - maxlen
|
|
224
|
+
if excess > 0:
|
|
225
|
+
hist.pop_least_used_batch(excess)
|
|
226
|
+
if replaced:
|
|
227
|
+
G.graph.pop(sentinel_key, None)
|
|
228
|
+
_normalise_state_streams(cast(MutableMapping[str, Any], hist))
|
|
229
|
+
return hist
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def current_step_idx(G: TNFRGraph | Mapping[str, Any]) -> int:
|
|
233
|
+
"""Return the current step index from ``G`` history."""
|
|
234
|
+
|
|
235
|
+
graph = getattr(G, "graph", G)
|
|
236
|
+
return len(graph.get("history", {}).get("C_steps", []))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def append_metric(
|
|
240
|
+
hist: MutableMapping[str, list[Any]], key: str, value: Any
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Append ``value`` to ``hist[key]`` list, creating it if missing."""
|
|
243
|
+
if key == "phase_state" and isinstance(value, str):
|
|
244
|
+
value = normalise_state_token(value)
|
|
245
|
+
elif key == "nodal_diag" and isinstance(value, Mapping):
|
|
246
|
+
snapshot: dict[Any, Any] = {}
|
|
247
|
+
for node, payload in value.items():
|
|
248
|
+
if isinstance(payload, Mapping):
|
|
249
|
+
state_value = payload.get("state")
|
|
250
|
+
if isinstance(payload, MutableMapping):
|
|
251
|
+
updated = payload
|
|
252
|
+
else:
|
|
253
|
+
updated = dict(payload)
|
|
254
|
+
if isinstance(state_value, str):
|
|
255
|
+
updated["state"] = normalise_state_token(state_value)
|
|
256
|
+
snapshot[node] = updated
|
|
257
|
+
else:
|
|
258
|
+
snapshot[node] = payload
|
|
259
|
+
hist.setdefault(key, []).append(snapshot)
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
hist.setdefault(key, []).append(value)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def last_glyph(nd: Mapping[str, Any]) -> str | None:
|
|
266
|
+
"""Return the most recent glyph for node or ``None``."""
|
|
267
|
+
hist = nd.get("glyph_history")
|
|
268
|
+
return hist[-1] if hist else None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def count_glyphs(
|
|
272
|
+
G: TNFRGraph, window: int | None = None, *, last_only: bool = False
|
|
273
|
+
) -> Counter[str]:
|
|
274
|
+
"""Count recent glyphs in the network.
|
|
275
|
+
|
|
276
|
+
If ``window`` is ``None``, the full history for each node is used. A
|
|
277
|
+
``window`` of zero yields an empty :class:`Counter`. Negative values raise
|
|
278
|
+
:class:`ValueError`.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
if window is not None:
|
|
282
|
+
window = validate_window(window)
|
|
283
|
+
if window == 0:
|
|
284
|
+
return Counter()
|
|
285
|
+
|
|
286
|
+
counts: Counter[str] = Counter()
|
|
287
|
+
for _, nd in G.nodes(data=True):
|
|
288
|
+
if last_only:
|
|
289
|
+
g = last_glyph(nd)
|
|
290
|
+
if g:
|
|
291
|
+
counts[g] += 1
|
|
292
|
+
continue
|
|
293
|
+
hist = nd.get("glyph_history")
|
|
294
|
+
if not hist:
|
|
295
|
+
continue
|
|
296
|
+
if window is None:
|
|
297
|
+
seq = hist
|
|
298
|
+
else:
|
|
299
|
+
start = max(len(hist) - window, 0)
|
|
300
|
+
seq = islice(hist, start, None)
|
|
301
|
+
counts.update(seq)
|
|
302
|
+
|
|
303
|
+
return counts
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _normalise_state_streams(hist: MutableMapping[str, Any]) -> None:
|
|
307
|
+
"""Normalise legacy state tokens stored in telemetry history."""
|
|
308
|
+
|
|
309
|
+
phase_state = hist.get("phase_state")
|
|
310
|
+
if isinstance(phase_state, deque):
|
|
311
|
+
canonical = [normalise_state_token(str(item)) for item in phase_state]
|
|
312
|
+
if canonical != list(phase_state):
|
|
313
|
+
phase_state.clear()
|
|
314
|
+
phase_state.extend(canonical)
|
|
315
|
+
elif isinstance(phase_state, list):
|
|
316
|
+
canonical = [normalise_state_token(str(item)) for item in phase_state]
|
|
317
|
+
if canonical != phase_state:
|
|
318
|
+
hist["phase_state"] = canonical
|
|
319
|
+
|
|
320
|
+
diag_history = hist.get("nodal_diag")
|
|
321
|
+
if isinstance(diag_history, list):
|
|
322
|
+
for snapshot in diag_history:
|
|
323
|
+
if not isinstance(snapshot, Mapping):
|
|
324
|
+
continue
|
|
325
|
+
for node, payload in snapshot.items():
|
|
326
|
+
if not isinstance(payload, Mapping):
|
|
327
|
+
continue
|
|
328
|
+
state_value = payload.get("state")
|
|
329
|
+
if not isinstance(state_value, str):
|
|
330
|
+
continue
|
|
331
|
+
canonical = normalise_state_token(state_value)
|
|
332
|
+
if canonical == state_value:
|
|
333
|
+
continue
|
|
334
|
+
if isinstance(payload, MutableMapping):
|
|
335
|
+
payload["state"] = canonical
|
|
336
|
+
else:
|
|
337
|
+
snapshot[node] = {**payload, "state": canonical}
|
tnfr/glyph_history.pyi
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
|
|
12
|
+
class HistoryDict(dict[str, Any]):
|
|
13
|
+
_maxlen: int
|
|
14
|
+
_counts: Counter[str]
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self, data: Mapping[str, Any] | None = ..., *, maxlen: int = ...
|
|
18
|
+
) -> None: ...
|
|
19
|
+
|
|
20
|
+
def get_increment(self, key: str, default: Any = ...) -> Any: ...
|
|
21
|
+
def __getitem__(self, key: str) -> Any: ...
|
|
22
|
+
def get(self, key: str, default: Any | None = ...) -> Any: ...
|
|
23
|
+
def __setitem__(self, key: str, value: Any) -> None: ...
|
|
24
|
+
def setdefault(self, key: str, default: Any | None = ...) -> Any: ...
|
|
25
|
+
def pop_least_used(self) -> Any: ...
|
|
26
|
+
def pop_least_used_batch(self, k: int) -> None: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def push_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> None: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def recent_glyph(
|
|
33
|
+
nd: MutableMapping[str, Any], glyph: str, window: int
|
|
34
|
+
) -> bool: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ensure_history(G: TNFRGraph) -> HistoryDict | dict[str, Any]: ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def current_step_idx(G: TNFRGraph | Mapping[str, Any]) -> int: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def append_metric(
|
|
44
|
+
hist: MutableMapping[str, list[Any]], key: str, value: Any
|
|
45
|
+
) -> None: ...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def last_glyph(nd: Mapping[str, Any]) -> str | None: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def count_glyphs(
|
|
52
|
+
G: TNFRGraph, window: int | None = ..., *, last_only: bool = ...
|
|
53
|
+
) -> Counter[str]: ...
|
tnfr/grammar.py
CHANGED
|
@@ -1,155 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
"""Backwards compatibility layer for grammar helpers.
|
|
2
|
+
|
|
3
|
+
The canonical implementations now live in :mod:`tnfr.validation`. This
|
|
4
|
+
module only re-exports them to avoid breaking external callers until the
|
|
5
|
+
new import paths are fully adopted.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .validation.compatibility import CANON_COMPAT, CANON_FALLBACK
|
|
9
|
+
from .validation.grammar import (
|
|
10
|
+
GrammarContext,
|
|
11
|
+
apply_glyph_with_grammar,
|
|
12
|
+
enforce_canonical_grammar,
|
|
13
|
+
on_applied_glyph,
|
|
14
|
+
_gram_state,
|
|
7
15
|
)
|
|
8
|
-
from .helpers import _get_attr, clamp01, reciente_glifo
|
|
9
|
-
from collections import deque
|
|
10
|
-
|
|
11
|
-
# Glifos nominales (para evitar typos)
|
|
12
|
-
AL = "A’L"; EN = "E’N"; IL = "I’L"; OZ = "O’Z"; UM = "U’M"; RA = "R’A"; SHA = "SH’A"; VAL = "VA’L"; NUL = "NU’L"; THOL = "T’HOL"; ZHIR = "Z’HIR"; NAV = "NA’V"; REMESH = "RE’MESH"
|
|
13
|
-
|
|
14
|
-
# -------------------------
|
|
15
|
-
# Estado de gramática por nodo
|
|
16
|
-
# -------------------------
|
|
17
|
-
|
|
18
|
-
def _gram_state(nd: Dict[str, Any]) -> Dict[str, Any]:
|
|
19
|
-
"""Crea/retorna el estado de gramática nodal.
|
|
20
|
-
Campos:
|
|
21
|
-
- thol_open (bool)
|
|
22
|
-
- thol_len (int)
|
|
23
|
-
"""
|
|
24
|
-
st = nd.setdefault("_GRAM", {"thol_open": False, "thol_len": 0})
|
|
25
|
-
st.setdefault("thol_open", False)
|
|
26
|
-
st.setdefault("thol_len", 0)
|
|
27
|
-
return st
|
|
28
|
-
|
|
29
|
-
# -------------------------
|
|
30
|
-
# Compatibilidades canónicas (siguiente permitido)
|
|
31
|
-
# -------------------------
|
|
32
|
-
CANON_COMPAT: Dict[str, Set[str]] = {
|
|
33
|
-
# Inicio / apertura
|
|
34
|
-
AL: {EN, RA, NAV, VAL, UM},
|
|
35
|
-
EN: {IL, UM, RA, NAV},
|
|
36
|
-
# Estabilización / difusión / acople
|
|
37
|
-
IL: {RA, VAL, UM, SHA},
|
|
38
|
-
UM: {RA, IL, VAL, NAV},
|
|
39
|
-
RA: {IL, VAL, UM, NAV},
|
|
40
|
-
VAL: {UM, RA, IL, NAV},
|
|
41
|
-
# Disonancia → transición → mutación
|
|
42
|
-
OZ: {ZHIR, NAV},
|
|
43
|
-
ZHIR: {IL, NAV},
|
|
44
|
-
NAV: {OZ, ZHIR, RA, IL, UM},
|
|
45
|
-
# Cierres / latencias
|
|
46
|
-
SHA: {AL, EN},
|
|
47
|
-
NUL: {AL, IL},
|
|
48
|
-
# Bloques autoorganizativos
|
|
49
|
-
THOL: {OZ, ZHIR, NAV, RA, IL, UM, SHA, NUL},
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
# Fallbacks canónicos si una transición no está permitida
|
|
53
|
-
CANON_FALLBACK: Dict[str, str] = {
|
|
54
|
-
AL: EN, EN: IL, IL: RA, UM: RA, RA: IL, VAL: RA, OZ: ZHIR, ZHIR: IL, NAV: RA, SHA: AL, NUL: AL, THOL: NAV,
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
# -------------------------
|
|
58
|
-
# Cierres T’HOL y precondiciones Z’HIR
|
|
59
|
-
# -------------------------
|
|
60
|
-
|
|
61
|
-
def _dnfr_norm(G, nd) -> float:
|
|
62
|
-
# Normalizador robusto: usa historial de |ΔNFR| máx guardado por dynamics (si existe)
|
|
63
|
-
norms = G.graph.get("_sel_norms") or {}
|
|
64
|
-
dmax = float(norms.get("dnfr_max", 1.0)) or 1.0
|
|
65
|
-
return clamp01(abs(_get_attr(nd, ALIAS_DNFR, 0.0)) / dmax)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _si(G, nd) -> float:
|
|
69
|
-
return clamp01(_get_attr(nd, ALIAS_SI, 0.5))
|
|
70
|
-
|
|
71
|
-
# -------------------------
|
|
72
|
-
# Núcleo: forzar gramática sobre un candidato
|
|
73
|
-
# -------------------------
|
|
74
|
-
|
|
75
|
-
def enforce_canonical_grammar(G, n, cand: str) -> str:
|
|
76
|
-
"""Valida/ajusta el glifo candidato según la gramática canónica.
|
|
77
|
-
|
|
78
|
-
Reglas clave:
|
|
79
|
-
- Compatibilidades de transición glífica (recorrido TNFR).
|
|
80
|
-
- O’Z→Z’HIR: la mutación requiere disonancia reciente o |ΔNFR| alto.
|
|
81
|
-
- T’HOL[...]: obliga cierre con SH’A o NU’L cuando el campo se estabiliza
|
|
82
|
-
o se alcanza el largo del bloque; mantiene estado por nodo.
|
|
83
|
-
|
|
84
|
-
Devuelve el glifo efectivo a aplicar.
|
|
85
|
-
"""
|
|
86
|
-
nd = G.nodes[n]
|
|
87
|
-
st = _gram_state(nd)
|
|
88
|
-
cfg = G.graph.get("GRAMMAR_CANON", DEFAULTS.get("GRAMMAR_CANON", {}))
|
|
89
|
-
|
|
90
|
-
# 0) Si vienen glifos fuera del alfabeto, no tocamos
|
|
91
|
-
if cand not in CANON_COMPAT:
|
|
92
|
-
return cand
|
|
93
|
-
|
|
94
|
-
# 1) Precondición O’Z→Z’HIR: mutación requiere disonancia reciente o campo fuerte
|
|
95
|
-
if cand == ZHIR:
|
|
96
|
-
win = int(cfg.get("zhir_requires_oz_window", 3))
|
|
97
|
-
dn_min = float(cfg.get("zhir_dnfr_min", 0.05))
|
|
98
|
-
if not reciente_glifo(nd, OZ, win) and _dnfr_norm(G, nd) < dn_min:
|
|
99
|
-
cand = OZ # forzamos paso por O’Z
|
|
100
|
-
|
|
101
|
-
# 2) Si estamos dentro de T’HOL, control de cierre obligado
|
|
102
|
-
if st.get("thol_open", False):
|
|
103
|
-
st["thol_len"] = int(st.get("thol_len", 0))
|
|
104
|
-
st["thol_len"] += 1
|
|
105
|
-
minlen = int(cfg.get("thol_min_len", 2))
|
|
106
|
-
maxlen = int(cfg.get("thol_max_len", 6))
|
|
107
|
-
close_dn = float(cfg.get("thol_close_dnfr", 0.15))
|
|
108
|
-
if st["thol_len"] >= maxlen or (st["thol_len"] >= minlen and _dnfr_norm(G, nd) <= close_dn):
|
|
109
|
-
cand = NUL if _si(G, nd) >= float(cfg.get("si_high", 0.66)) else SHA
|
|
110
|
-
|
|
111
|
-
# 3) Compatibilidades: si el anterior restringe el siguiente
|
|
112
|
-
prev = None
|
|
113
|
-
hist = nd.get("hist_glifos")
|
|
114
|
-
if hist:
|
|
115
|
-
try:
|
|
116
|
-
prev = list(hist)[-1]
|
|
117
|
-
except Exception:
|
|
118
|
-
prev = None
|
|
119
|
-
if prev in CANON_COMPAT and cand not in CANON_COMPAT[prev]:
|
|
120
|
-
cand = CANON_FALLBACK.get(prev, cand)
|
|
121
|
-
|
|
122
|
-
return cand
|
|
123
|
-
|
|
124
|
-
# -------------------------
|
|
125
|
-
# Post-selección: actualizar estado de gramática
|
|
126
|
-
# -------------------------
|
|
127
|
-
|
|
128
|
-
def on_applied_glifo(G, n, applied: str) -> None:
|
|
129
|
-
nd = G.nodes[n]
|
|
130
|
-
st = _gram_state(nd)
|
|
131
|
-
if applied == THOL:
|
|
132
|
-
st["thol_open"] = True
|
|
133
|
-
st["thol_len"] = 0
|
|
134
|
-
elif applied in (SHA, NUL):
|
|
135
|
-
st["thol_open"] = False
|
|
136
|
-
st["thol_len"] = 0
|
|
137
|
-
else:
|
|
138
|
-
pass
|
|
139
|
-
|
|
140
|
-
# -------------------------
|
|
141
|
-
# Integración con dynamics.step: helper de selección+aplicación
|
|
142
|
-
# -------------------------
|
|
143
|
-
|
|
144
|
-
def select_and_apply_with_grammar(G, n, selector, window: int) -> None:
|
|
145
|
-
"""Aplica gramática canónica sobre la propuesta del selector.
|
|
146
16
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
""
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
17
|
+
__all__ = [
|
|
18
|
+
"GrammarContext",
|
|
19
|
+
"CANON_COMPAT",
|
|
20
|
+
"CANON_FALLBACK",
|
|
21
|
+
"enforce_canonical_grammar",
|
|
22
|
+
"on_applied_glyph",
|
|
23
|
+
"apply_glyph_with_grammar",
|
|
24
|
+
"_gram_state",
|
|
25
|
+
]
|
tnfr/grammar.pyi
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
__all__: Any
|
|
4
|
+
|
|
5
|
+
def __getattr__(name: str) -> Any: ...
|
|
6
|
+
|
|
7
|
+
CANON_COMPAT: Any
|
|
8
|
+
CANON_FALLBACK: Any
|
|
9
|
+
GrammarContext: Any
|
|
10
|
+
_gram_state: Any
|
|
11
|
+
apply_glyph_with_grammar: Any
|
|
12
|
+
enforce_canonical_grammar: Any
|
|
13
|
+
on_applied_glyph: Any
|