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.
Files changed (170) hide show
  1. tnfr/__init__.py +270 -90
  2. tnfr/__init__.pyi +40 -0
  3. tnfr/_compat.py +11 -0
  4. tnfr/_version.py +7 -0
  5. tnfr/_version.pyi +7 -0
  6. tnfr/alias.py +631 -0
  7. tnfr/alias.pyi +140 -0
  8. tnfr/cache.py +732 -0
  9. tnfr/cache.pyi +232 -0
  10. tnfr/callback_utils.py +381 -0
  11. tnfr/callback_utils.pyi +105 -0
  12. tnfr/cli/__init__.py +89 -0
  13. tnfr/cli/__init__.pyi +47 -0
  14. tnfr/cli/arguments.py +199 -0
  15. tnfr/cli/arguments.pyi +33 -0
  16. tnfr/cli/execution.py +322 -0
  17. tnfr/cli/execution.pyi +80 -0
  18. tnfr/cli/utils.py +34 -0
  19. tnfr/cli/utils.pyi +8 -0
  20. tnfr/config/__init__.py +12 -0
  21. tnfr/config/__init__.pyi +8 -0
  22. tnfr/config/constants.py +104 -0
  23. tnfr/config/constants.pyi +12 -0
  24. tnfr/config/init.py +36 -0
  25. tnfr/config/init.pyi +8 -0
  26. tnfr/config/operator_names.py +106 -0
  27. tnfr/config/operator_names.pyi +28 -0
  28. tnfr/config/presets.py +104 -0
  29. tnfr/config/presets.pyi +7 -0
  30. tnfr/constants/__init__.py +228 -0
  31. tnfr/constants/__init__.pyi +104 -0
  32. tnfr/constants/core.py +158 -0
  33. tnfr/constants/core.pyi +17 -0
  34. tnfr/constants/init.py +31 -0
  35. tnfr/constants/init.pyi +12 -0
  36. tnfr/constants/metric.py +102 -0
  37. tnfr/constants/metric.pyi +19 -0
  38. tnfr/constants_glyphs.py +16 -0
  39. tnfr/constants_glyphs.pyi +12 -0
  40. tnfr/dynamics/__init__.py +136 -0
  41. tnfr/dynamics/__init__.pyi +83 -0
  42. tnfr/dynamics/adaptation.py +201 -0
  43. tnfr/dynamics/aliases.py +22 -0
  44. tnfr/dynamics/coordination.py +343 -0
  45. tnfr/dynamics/dnfr.py +2315 -0
  46. tnfr/dynamics/dnfr.pyi +33 -0
  47. tnfr/dynamics/integrators.py +561 -0
  48. tnfr/dynamics/integrators.pyi +35 -0
  49. tnfr/dynamics/runtime.py +521 -0
  50. tnfr/dynamics/sampling.py +34 -0
  51. tnfr/dynamics/sampling.pyi +7 -0
  52. tnfr/dynamics/selectors.py +680 -0
  53. tnfr/execution.py +216 -0
  54. tnfr/execution.pyi +65 -0
  55. tnfr/flatten.py +283 -0
  56. tnfr/flatten.pyi +28 -0
  57. tnfr/gamma.py +320 -89
  58. tnfr/gamma.pyi +40 -0
  59. tnfr/glyph_history.py +337 -0
  60. tnfr/glyph_history.pyi +53 -0
  61. tnfr/grammar.py +23 -153
  62. tnfr/grammar.pyi +13 -0
  63. tnfr/helpers/__init__.py +151 -0
  64. tnfr/helpers/__init__.pyi +66 -0
  65. tnfr/helpers/numeric.py +88 -0
  66. tnfr/helpers/numeric.pyi +12 -0
  67. tnfr/immutable.py +214 -0
  68. tnfr/immutable.pyi +37 -0
  69. tnfr/initialization.py +199 -0
  70. tnfr/initialization.pyi +73 -0
  71. tnfr/io.py +311 -0
  72. tnfr/io.pyi +11 -0
  73. tnfr/locking.py +37 -0
  74. tnfr/locking.pyi +7 -0
  75. tnfr/metrics/__init__.py +41 -0
  76. tnfr/metrics/__init__.pyi +20 -0
  77. tnfr/metrics/coherence.py +1469 -0
  78. tnfr/metrics/common.py +149 -0
  79. tnfr/metrics/common.pyi +15 -0
  80. tnfr/metrics/core.py +259 -0
  81. tnfr/metrics/core.pyi +13 -0
  82. tnfr/metrics/diagnosis.py +840 -0
  83. tnfr/metrics/diagnosis.pyi +89 -0
  84. tnfr/metrics/export.py +151 -0
  85. tnfr/metrics/glyph_timing.py +369 -0
  86. tnfr/metrics/reporting.py +152 -0
  87. tnfr/metrics/reporting.pyi +12 -0
  88. tnfr/metrics/sense_index.py +294 -0
  89. tnfr/metrics/sense_index.pyi +9 -0
  90. tnfr/metrics/trig.py +216 -0
  91. tnfr/metrics/trig.pyi +12 -0
  92. tnfr/metrics/trig_cache.py +105 -0
  93. tnfr/metrics/trig_cache.pyi +10 -0
  94. tnfr/node.py +255 -177
  95. tnfr/node.pyi +161 -0
  96. tnfr/observers.py +154 -150
  97. tnfr/observers.pyi +46 -0
  98. tnfr/ontosim.py +135 -134
  99. tnfr/ontosim.pyi +33 -0
  100. tnfr/operators/__init__.py +452 -0
  101. tnfr/operators/__init__.pyi +31 -0
  102. tnfr/operators/definitions.py +181 -0
  103. tnfr/operators/definitions.pyi +92 -0
  104. tnfr/operators/jitter.py +266 -0
  105. tnfr/operators/jitter.pyi +11 -0
  106. tnfr/operators/registry.py +80 -0
  107. tnfr/operators/registry.pyi +15 -0
  108. tnfr/operators/remesh.py +569 -0
  109. tnfr/presets.py +10 -23
  110. tnfr/presets.pyi +7 -0
  111. tnfr/py.typed +0 -0
  112. tnfr/rng.py +440 -0
  113. tnfr/rng.pyi +14 -0
  114. tnfr/selector.py +217 -0
  115. tnfr/selector.pyi +19 -0
  116. tnfr/sense.py +307 -142
  117. tnfr/sense.pyi +30 -0
  118. tnfr/structural.py +69 -164
  119. tnfr/structural.pyi +46 -0
  120. tnfr/telemetry/__init__.py +13 -0
  121. tnfr/telemetry/verbosity.py +37 -0
  122. tnfr/tokens.py +61 -0
  123. tnfr/tokens.pyi +41 -0
  124. tnfr/trace.py +520 -95
  125. tnfr/trace.pyi +68 -0
  126. tnfr/types.py +382 -17
  127. tnfr/types.pyi +145 -0
  128. tnfr/utils/__init__.py +158 -0
  129. tnfr/utils/__init__.pyi +133 -0
  130. tnfr/utils/cache.py +755 -0
  131. tnfr/utils/cache.pyi +156 -0
  132. tnfr/utils/data.py +267 -0
  133. tnfr/utils/data.pyi +73 -0
  134. tnfr/utils/graph.py +87 -0
  135. tnfr/utils/graph.pyi +10 -0
  136. tnfr/utils/init.py +746 -0
  137. tnfr/utils/init.pyi +85 -0
  138. tnfr/utils/io.py +157 -0
  139. tnfr/utils/io.pyi +10 -0
  140. tnfr/utils/validators.py +130 -0
  141. tnfr/utils/validators.pyi +19 -0
  142. tnfr/validation/__init__.py +25 -0
  143. tnfr/validation/__init__.pyi +17 -0
  144. tnfr/validation/compatibility.py +59 -0
  145. tnfr/validation/compatibility.pyi +8 -0
  146. tnfr/validation/grammar.py +149 -0
  147. tnfr/validation/grammar.pyi +11 -0
  148. tnfr/validation/rules.py +194 -0
  149. tnfr/validation/rules.pyi +18 -0
  150. tnfr/validation/syntax.py +151 -0
  151. tnfr/validation/syntax.pyi +7 -0
  152. tnfr-6.0.0.dist-info/METADATA +135 -0
  153. tnfr-6.0.0.dist-info/RECORD +157 -0
  154. tnfr/cli.py +0 -322
  155. tnfr/config.py +0 -41
  156. tnfr/constants.py +0 -277
  157. tnfr/dynamics.py +0 -814
  158. tnfr/helpers.py +0 -264
  159. tnfr/main.py +0 -47
  160. tnfr/metrics.py +0 -597
  161. tnfr/operators.py +0 -525
  162. tnfr/program.py +0 -176
  163. tnfr/scenarios.py +0 -34
  164. tnfr/validators.py +0 -38
  165. tnfr-4.5.1.dist-info/METADATA +0 -221
  166. tnfr-4.5.1.dist-info/RECORD +0 -28
  167. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
  168. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
  169. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
  170. {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
- from __future__ import annotations
2
- from typing import Dict, Any, Set
3
-
4
- from .constants import (
5
- DEFAULTS,
6
- ALIAS_SI, ALIAS_DNFR, ALIAS_EPI,
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
- El selector puede incluir una gramática **suave** (pre–filtro) como
148
- `parametric_glyph_selector`; la presente función garantiza que la
149
- gramática canónica tenga precedencia final.
150
- """
151
- from .operators import aplicar_glifo
152
- cand = selector(G, n)
153
- cand = enforce_canonical_grammar(G, n, cand)
154
- aplicar_glifo(G, n, cand, window=window)
155
- on_applied_glifo(G, n, cand)
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