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.

Files changed (195) hide show
  1. tnfr/__init__.py +275 -51
  2. tnfr/__init__.pyi +33 -0
  3. tnfr/_compat.py +10 -0
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +49 -0
  6. tnfr/_version.pyi +7 -0
  7. tnfr/alias.py +117 -31
  8. tnfr/alias.pyi +108 -0
  9. tnfr/cache.py +6 -572
  10. tnfr/cache.pyi +16 -0
  11. tnfr/callback_utils.py +16 -38
  12. tnfr/callback_utils.pyi +79 -0
  13. tnfr/cli/__init__.py +34 -14
  14. tnfr/cli/__init__.pyi +26 -0
  15. tnfr/cli/arguments.py +211 -28
  16. tnfr/cli/arguments.pyi +27 -0
  17. tnfr/cli/execution.py +470 -50
  18. tnfr/cli/execution.pyi +70 -0
  19. tnfr/cli/utils.py +18 -3
  20. tnfr/cli/utils.pyi +8 -0
  21. tnfr/config/__init__.py +13 -0
  22. tnfr/config/__init__.pyi +10 -0
  23. tnfr/{constants_glyphs.py → config/constants.py} +26 -20
  24. tnfr/config/constants.pyi +12 -0
  25. tnfr/config/feature_flags.py +83 -0
  26. tnfr/{config.py → config/init.py} +11 -7
  27. tnfr/config/init.pyi +8 -0
  28. tnfr/config/operator_names.py +93 -0
  29. tnfr/config/operator_names.pyi +28 -0
  30. tnfr/config/presets.py +84 -0
  31. tnfr/config/presets.pyi +7 -0
  32. tnfr/constants/__init__.py +80 -29
  33. tnfr/constants/__init__.pyi +92 -0
  34. tnfr/constants/aliases.py +31 -0
  35. tnfr/constants/core.py +4 -4
  36. tnfr/constants/core.pyi +17 -0
  37. tnfr/constants/init.py +1 -1
  38. tnfr/constants/init.pyi +12 -0
  39. tnfr/constants/metric.py +7 -15
  40. tnfr/constants/metric.pyi +19 -0
  41. tnfr/dynamics/__init__.py +165 -633
  42. tnfr/dynamics/__init__.pyi +82 -0
  43. tnfr/dynamics/adaptation.py +267 -0
  44. tnfr/dynamics/aliases.py +23 -0
  45. tnfr/dynamics/coordination.py +385 -0
  46. tnfr/dynamics/dnfr.py +2283 -400
  47. tnfr/dynamics/dnfr.pyi +24 -0
  48. tnfr/dynamics/integrators.py +406 -98
  49. tnfr/dynamics/integrators.pyi +34 -0
  50. tnfr/dynamics/runtime.py +881 -0
  51. tnfr/dynamics/sampling.py +10 -5
  52. tnfr/dynamics/sampling.pyi +7 -0
  53. tnfr/dynamics/selectors.py +719 -0
  54. tnfr/execution.py +70 -48
  55. tnfr/execution.pyi +45 -0
  56. tnfr/flatten.py +13 -9
  57. tnfr/flatten.pyi +21 -0
  58. tnfr/gamma.py +66 -53
  59. tnfr/gamma.pyi +34 -0
  60. tnfr/glyph_history.py +110 -52
  61. tnfr/glyph_history.pyi +35 -0
  62. tnfr/glyph_runtime.py +16 -0
  63. tnfr/glyph_runtime.pyi +9 -0
  64. tnfr/immutable.py +69 -28
  65. tnfr/immutable.pyi +34 -0
  66. tnfr/initialization.py +16 -16
  67. tnfr/initialization.pyi +65 -0
  68. tnfr/io.py +6 -240
  69. tnfr/io.pyi +16 -0
  70. tnfr/locking.pyi +7 -0
  71. tnfr/mathematics/__init__.py +81 -0
  72. tnfr/mathematics/backend.py +426 -0
  73. tnfr/mathematics/dynamics.py +398 -0
  74. tnfr/mathematics/epi.py +254 -0
  75. tnfr/mathematics/generators.py +222 -0
  76. tnfr/mathematics/metrics.py +119 -0
  77. tnfr/mathematics/operators.py +233 -0
  78. tnfr/mathematics/operators_factory.py +71 -0
  79. tnfr/mathematics/projection.py +78 -0
  80. tnfr/mathematics/runtime.py +173 -0
  81. tnfr/mathematics/spaces.py +247 -0
  82. tnfr/mathematics/transforms.py +292 -0
  83. tnfr/metrics/__init__.py +10 -10
  84. tnfr/metrics/__init__.pyi +20 -0
  85. tnfr/metrics/coherence.py +993 -324
  86. tnfr/metrics/common.py +23 -16
  87. tnfr/metrics/common.pyi +46 -0
  88. tnfr/metrics/core.py +251 -35
  89. tnfr/metrics/core.pyi +13 -0
  90. tnfr/metrics/diagnosis.py +708 -111
  91. tnfr/metrics/diagnosis.pyi +85 -0
  92. tnfr/metrics/export.py +27 -15
  93. tnfr/metrics/glyph_timing.py +232 -42
  94. tnfr/metrics/reporting.py +33 -22
  95. tnfr/metrics/reporting.pyi +12 -0
  96. tnfr/metrics/sense_index.py +987 -43
  97. tnfr/metrics/sense_index.pyi +9 -0
  98. tnfr/metrics/trig.py +214 -23
  99. tnfr/metrics/trig.pyi +13 -0
  100. tnfr/metrics/trig_cache.py +115 -22
  101. tnfr/metrics/trig_cache.pyi +10 -0
  102. tnfr/node.py +542 -136
  103. tnfr/node.pyi +178 -0
  104. tnfr/observers.py +152 -35
  105. tnfr/observers.pyi +31 -0
  106. tnfr/ontosim.py +23 -19
  107. tnfr/ontosim.pyi +28 -0
  108. tnfr/operators/__init__.py +601 -82
  109. tnfr/operators/__init__.pyi +45 -0
  110. tnfr/operators/definitions.py +513 -0
  111. tnfr/operators/definitions.pyi +78 -0
  112. tnfr/operators/grammar.py +760 -0
  113. tnfr/operators/jitter.py +107 -38
  114. tnfr/operators/jitter.pyi +11 -0
  115. tnfr/operators/registry.py +75 -0
  116. tnfr/operators/registry.pyi +13 -0
  117. tnfr/operators/remesh.py +149 -88
  118. tnfr/py.typed +0 -0
  119. tnfr/rng.py +46 -143
  120. tnfr/rng.pyi +14 -0
  121. tnfr/schemas/__init__.py +8 -0
  122. tnfr/schemas/grammar.json +94 -0
  123. tnfr/selector.py +25 -19
  124. tnfr/selector.pyi +19 -0
  125. tnfr/sense.py +72 -62
  126. tnfr/sense.pyi +23 -0
  127. tnfr/structural.py +522 -262
  128. tnfr/structural.pyi +69 -0
  129. tnfr/telemetry/__init__.py +35 -0
  130. tnfr/telemetry/cache_metrics.py +226 -0
  131. tnfr/telemetry/nu_f.py +423 -0
  132. tnfr/telemetry/nu_f.pyi +123 -0
  133. tnfr/telemetry/verbosity.py +37 -0
  134. tnfr/tokens.py +1 -3
  135. tnfr/tokens.pyi +36 -0
  136. tnfr/trace.py +270 -113
  137. tnfr/trace.pyi +40 -0
  138. tnfr/types.py +574 -6
  139. tnfr/types.pyi +331 -0
  140. tnfr/units.py +69 -0
  141. tnfr/units.pyi +16 -0
  142. tnfr/utils/__init__.py +217 -0
  143. tnfr/utils/__init__.pyi +202 -0
  144. tnfr/utils/cache.py +2395 -0
  145. tnfr/utils/cache.pyi +468 -0
  146. tnfr/utils/chunks.py +104 -0
  147. tnfr/utils/chunks.pyi +21 -0
  148. tnfr/{collections_utils.py → utils/data.py} +147 -90
  149. tnfr/utils/data.pyi +64 -0
  150. tnfr/utils/graph.py +85 -0
  151. tnfr/utils/graph.pyi +10 -0
  152. tnfr/utils/init.py +770 -0
  153. tnfr/utils/init.pyi +78 -0
  154. tnfr/utils/io.py +456 -0
  155. tnfr/{helpers → utils}/numeric.py +51 -24
  156. tnfr/utils/numeric.pyi +21 -0
  157. tnfr/validation/__init__.py +113 -0
  158. tnfr/validation/__init__.pyi +77 -0
  159. tnfr/validation/compatibility.py +95 -0
  160. tnfr/validation/compatibility.pyi +6 -0
  161. tnfr/validation/grammar.py +71 -0
  162. tnfr/validation/grammar.pyi +40 -0
  163. tnfr/validation/graph.py +138 -0
  164. tnfr/validation/graph.pyi +17 -0
  165. tnfr/validation/rules.py +281 -0
  166. tnfr/validation/rules.pyi +55 -0
  167. tnfr/validation/runtime.py +263 -0
  168. tnfr/validation/runtime.pyi +31 -0
  169. tnfr/validation/soft_filters.py +170 -0
  170. tnfr/validation/soft_filters.pyi +37 -0
  171. tnfr/validation/spectral.py +159 -0
  172. tnfr/validation/spectral.pyi +46 -0
  173. tnfr/validation/syntax.py +40 -0
  174. tnfr/validation/syntax.pyi +10 -0
  175. tnfr/validation/window.py +39 -0
  176. tnfr/validation/window.pyi +1 -0
  177. tnfr/viz/__init__.py +9 -0
  178. tnfr/viz/matplotlib.py +246 -0
  179. tnfr-7.0.0.dist-info/METADATA +179 -0
  180. tnfr-7.0.0.dist-info/RECORD +185 -0
  181. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
  182. tnfr/grammar.py +0 -344
  183. tnfr/graph_utils.py +0 -84
  184. tnfr/helpers/__init__.py +0 -71
  185. tnfr/import_utils.py +0 -228
  186. tnfr/json_utils.py +0 -162
  187. tnfr/logging_utils.py +0 -116
  188. tnfr/presets.py +0 -60
  189. tnfr/validators.py +0 -84
  190. tnfr/value_utils.py +0 -59
  191. tnfr-4.5.2.dist-info/METADATA +0 -379
  192. tnfr-4.5.2.dist-info/RECORD +0 -67
  193. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
  194. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
  195. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,281 @@
1
+ """Validation helpers grouped by rule type.
2
+
3
+ These utilities implement the canonical checks required by
4
+ :mod:`tnfr.operators.grammar`. They are organised here to make it
5
+ explicit which pieces enforce repetition control, transition
6
+ compatibility or stabilisation thresholds.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from functools import lru_cache
12
+ from typing import TYPE_CHECKING, Any, Mapping
13
+
14
+ from ..alias import get_attr
15
+ from ..constants.aliases import ALIAS_SI
16
+ from ..config.operator_names import (
17
+ CONTRACTION,
18
+ DISSONANCE,
19
+ MUTATION,
20
+ SELF_ORGANIZATION,
21
+ SILENCE,
22
+ canonical_operator_name,
23
+ operator_display_name,
24
+ )
25
+ from ..utils import clamp01
26
+ from ..metrics.common import normalize_dnfr
27
+ from ..types import Glyph
28
+
29
+ if TYPE_CHECKING: # pragma: no cover - only for typing
30
+ from ..operators.grammar import GrammarContext
31
+
32
+ __all__ = [
33
+ "coerce_glyph",
34
+ "glyph_fallback",
35
+ "get_norm",
36
+ "normalized_dnfr",
37
+ "_norm_attr",
38
+ "_si",
39
+ "_check_oz_to_zhir",
40
+ "_check_thol_closure",
41
+ "_check_compatibility",
42
+ ]
43
+
44
+
45
+ def coerce_glyph(val: Any) -> Glyph | Any:
46
+ """Return ``val`` coerced to :class:`Glyph` when possible."""
47
+
48
+ try:
49
+ return Glyph(val)
50
+ except (ValueError, TypeError):
51
+ if isinstance(val, str) and val.startswith("Glyph."):
52
+ _, _, candidate = val.partition(".")
53
+ if candidate:
54
+ try:
55
+ return Glyph(candidate)
56
+ except ValueError:
57
+ pass
58
+ return val
59
+
60
+
61
+ def glyph_fallback(cand_key: str, fallbacks: Mapping[str, Any]) -> Glyph | str:
62
+ """Determine fallback glyph for ``cand_key`` considering canon tables."""
63
+
64
+ glyph_key = coerce_glyph(cand_key)
65
+ fb_override = fallbacks.get(cand_key)
66
+ if fb_override is not None:
67
+ return coerce_glyph(fb_override)
68
+
69
+ glyph_to_name, name_to_glyph = _functional_translators()
70
+ _, fallback_table = _structural_tables()
71
+ cand_name = glyph_to_name(glyph_key if isinstance(glyph_key, Glyph) else cand_key)
72
+ if cand_name is None:
73
+ return coerce_glyph(cand_key)
74
+ fb_name = fallback_table.get(cand_name)
75
+ if fb_name is None:
76
+ return coerce_glyph(cand_key)
77
+ fb_glyph = name_to_glyph(fb_name)
78
+ return fb_glyph if fb_glyph is not None else coerce_glyph(cand_key)
79
+
80
+
81
+ # -------------------------
82
+ # Normalisation helpers
83
+ # -------------------------
84
+
85
+
86
+ def get_norm(ctx: "GrammarContext", key: str) -> float:
87
+ """Retrieve a global normalisation value from ``ctx.norms``."""
88
+
89
+ return float(ctx.norms.get(key, 1.0)) or 1.0
90
+
91
+
92
+ def _norm_attr(ctx: "GrammarContext", nd, attr_alias: str, norm_key: str) -> float:
93
+ """Normalise ``attr_alias`` using the global maximum ``norm_key``."""
94
+
95
+ max_val = get_norm(ctx, norm_key)
96
+ return clamp01(abs(get_attr(nd, attr_alias, 0.0)) / max_val)
97
+
98
+
99
+ def _si(nd) -> float:
100
+ """Return the structural sense index for ``nd`` clamped to ``[0, 1]``."""
101
+
102
+ return clamp01(get_attr(nd, ALIAS_SI, 0.5))
103
+
104
+
105
+ def normalized_dnfr(ctx: "GrammarContext", nd) -> float:
106
+ """Normalise |ΔNFR| using the configured global maximum."""
107
+
108
+ return normalize_dnfr(nd, get_norm(ctx, "dnfr_max"))
109
+
110
+
111
+ # -------------------------
112
+ # Translation helpers
113
+ # -------------------------
114
+
115
+
116
+ def _structural_label(value: object) -> str:
117
+ """Return the canonical structural name for ``value`` when possible."""
118
+
119
+ glyph_to_name = _functional_translators()[0]
120
+ coerced = coerce_glyph(value)
121
+ if isinstance(coerced, Glyph):
122
+ name = glyph_to_name(coerced)
123
+ if name is not None:
124
+ return name
125
+ name = glyph_to_name(value if isinstance(value, Glyph) else value)
126
+ if name is not None:
127
+ return name
128
+ if value is None:
129
+ return "unknown"
130
+ return canonical_operator_name(str(value))
131
+
132
+
133
+ # -------------------------
134
+ # Validation rules
135
+ # -------------------------
136
+
137
+ def _check_oz_to_zhir(ctx: "GrammarContext", n, cand: Glyph | str) -> Glyph | str:
138
+ """Enforce OZ precedents before allowing ZHIR mutations."""
139
+
140
+ from ..glyph_history import recent_glyph
141
+ nd = ctx.G.nodes[n]
142
+ cand_glyph = coerce_glyph(cand)
143
+ glyph_to_name, name_to_glyph = _functional_translators()
144
+ cand_name = glyph_to_name(cand_glyph if isinstance(cand_glyph, Glyph) else cand)
145
+ if cand_name == MUTATION:
146
+ cfg = ctx.cfg_canon
147
+ win = int(cfg.get("zhir_requires_oz_window", 3))
148
+ dn_min = float(cfg.get("zhir_dnfr_min", 0.05))
149
+ dissonance_glyph = name_to_glyph(DISSONANCE)
150
+ if dissonance_glyph is None:
151
+ return cand
152
+ norm_dn = normalized_dnfr(ctx, nd)
153
+ recent_glyph(nd, dissonance_glyph.value, win)
154
+ history = tuple(_structural_label(item) for item in nd.get("glyph_history", ()))
155
+ has_recent_dissonance = any(
156
+ entry == DISSONANCE for entry in history[-win:]
157
+ )
158
+ if not has_recent_dissonance and norm_dn < dn_min:
159
+ cand_label = cand_name if cand_name is not None else _structural_label(cand)
160
+ order = (*history[-win:], cand_label)
161
+ from ..operators import grammar as _grammar
162
+
163
+ raise _grammar.MutationPreconditionError(
164
+ rule="oz-before-zhir",
165
+ candidate=cand_label,
166
+ message=f"mutation {cand_label} requires {DISSONANCE} within window {win}",
167
+ window=win,
168
+ threshold=dn_min,
169
+ order=order,
170
+ context={"normalized_dnfr": norm_dn},
171
+ )
172
+ return cand
173
+
174
+
175
+ def _check_thol_closure(
176
+ ctx: "GrammarContext", n, cand: Glyph | str, st: dict[str, Any]
177
+ ) -> Glyph | str:
178
+ """Close THOL blocks with canonical glyphs once stabilised."""
179
+
180
+ nd = ctx.G.nodes[n]
181
+ if st.get("thol_open", False):
182
+ st["thol_len"] = int(st.get("thol_len", 0)) + 1
183
+ cfg = ctx.cfg_canon
184
+ minlen = int(cfg.get("thol_min_len", 2))
185
+ maxlen = int(cfg.get("thol_max_len", 6))
186
+ close_dn = float(cfg.get("thol_close_dnfr", 0.15))
187
+ requires_close = st["thol_len"] >= maxlen or (
188
+ st["thol_len"] >= minlen and normalized_dnfr(ctx, nd) <= close_dn
189
+ )
190
+ if requires_close:
191
+ glyph_to_name, name_to_glyph = _functional_translators()
192
+ cand_glyph = coerce_glyph(cand)
193
+ cand_name = glyph_to_name(cand_glyph if isinstance(cand_glyph, Glyph) else cand)
194
+
195
+ si_high = float(cfg.get("si_high", 0.66))
196
+ si = _si(nd)
197
+ target_name = SILENCE if si >= si_high else CONTRACTION
198
+ target_glyph = name_to_glyph(target_name)
199
+
200
+ if cand_name == target_name and isinstance(cand_glyph, Glyph):
201
+ return cand_glyph
202
+
203
+ if target_glyph is not None and cand_name in {CONTRACTION, SILENCE}:
204
+ return target_glyph
205
+
206
+ history = tuple(
207
+ _structural_label(item) for item in nd.get("glyph_history", ())
208
+ )
209
+ cand_label = cand_name if cand_name is not None else _structural_label(cand)
210
+ order = (*history[-st["thol_len"]:], cand_label)
211
+ from ..operators import grammar as _grammar
212
+
213
+ raise _grammar.TholClosureError(
214
+ rule="thol-closure",
215
+ candidate=cand_label,
216
+ message=(
217
+ f"{operator_display_name(SELF_ORGANIZATION)} block requires {operator_display_name(target_name)} closure"
218
+ ),
219
+ window=st["thol_len"],
220
+ threshold=close_dn,
221
+ order=order,
222
+ context={
223
+ "thol_min_len": minlen,
224
+ "thol_max_len": maxlen,
225
+ "si": si,
226
+ "si_high": si_high,
227
+ "required_closure": target_name,
228
+ },
229
+ )
230
+ return cand
231
+
232
+
233
+ def _check_compatibility(ctx: "GrammarContext", n, cand: Glyph | str) -> Glyph | str:
234
+ """Verify canonical transition compatibility for ``cand``."""
235
+
236
+ nd = ctx.G.nodes[n]
237
+ hist = nd.get("glyph_history")
238
+ prev = hist[-1] if hist else None
239
+ prev_glyph = coerce_glyph(prev)
240
+ cand_glyph = coerce_glyph(cand)
241
+ if isinstance(prev_glyph, Glyph):
242
+ glyph_to_name, name_to_glyph = _functional_translators()
243
+ compat, fallback = _structural_tables()
244
+ prev_name = glyph_to_name(prev_glyph)
245
+ if prev_name is None:
246
+ return cand
247
+ allowed = compat.get(prev_name)
248
+ if allowed is None:
249
+ return cand
250
+ cand_name = glyph_to_name(cand_glyph if isinstance(cand_glyph, Glyph) else cand)
251
+ if cand_name in allowed:
252
+ return cand
253
+ fb_name = fallback.get(prev_name)
254
+ if fb_name is not None:
255
+ fb_glyph = name_to_glyph(fb_name)
256
+ if fb_glyph is not None:
257
+ return fb_glyph
258
+ order = (prev_name, cand_name or str(cand))
259
+ from ..operators import grammar as _grammar
260
+
261
+ raise _grammar.TransitionCompatibilityError(
262
+ rule="transition-compatibility",
263
+ candidate=cand_name or str(cand),
264
+ message=f"{cand_name or cand} incompatible after {prev_name}",
265
+ order=order,
266
+ )
267
+ return cand
268
+
269
+
270
+ @lru_cache(maxsize=1)
271
+ def _functional_translators():
272
+ from ..operators import grammar as _grammar
273
+
274
+ return _grammar.glyph_function_name, _grammar.function_name_to_glyph
275
+
276
+
277
+ @lru_cache(maxsize=1)
278
+ def _structural_tables():
279
+ from . import compatibility as _compat
280
+
281
+ return _compat._STRUCTURAL_COMPAT_TABLE, _compat._STRUCTURAL_FALLBACK_TABLE
@@ -0,0 +1,55 @@
1
+ from collections.abc import Callable, Mapping
2
+ from typing import Any, TypeVar
3
+
4
+ from typing import Any, Mapping, TypeVar
5
+
6
+ from ..types import Glyph, NodeId
7
+ from .grammar import GrammarContext
8
+
9
+ __all__ = (
10
+ "coerce_glyph",
11
+ "glyph_fallback",
12
+ "get_norm",
13
+ "normalized_dnfr",
14
+ "_norm_attr",
15
+ "_si",
16
+ "_check_oz_to_zhir",
17
+ "_check_thol_closure",
18
+ "_check_compatibility",
19
+ )
20
+
21
+ _T = TypeVar("_T")
22
+
23
+
24
+ def coerce_glyph(val: _T) -> Glyph | _T: ...
25
+
26
+
27
+ def glyph_fallback(cand_key: str, fallbacks: Mapping[str, Any]) -> Glyph | str: ...
28
+
29
+
30
+ def get_norm(ctx: GrammarContext, key: str) -> float: ...
31
+
32
+
33
+ def _norm_attr(
34
+ ctx: GrammarContext, nd: Mapping[str, Any], attr_alias: str, norm_key: str
35
+ ) -> float: ...
36
+
37
+
38
+ def _si(nd: Mapping[str, Any]) -> float: ...
39
+
40
+
41
+ def normalized_dnfr(ctx: GrammarContext, nd: Mapping[str, Any]) -> float: ...
42
+
43
+
44
+ def _check_oz_to_zhir(ctx: GrammarContext, n: NodeId, cand: Glyph | str) -> Glyph | str: ...
45
+
46
+
47
+ def _check_thol_closure(
48
+ ctx: GrammarContext,
49
+ n: NodeId,
50
+ cand: Glyph | str,
51
+ st: dict[str, Any],
52
+ ) -> Glyph | str: ...
53
+
54
+
55
+ def _check_compatibility(ctx: GrammarContext, n: NodeId, cand: Glyph | str) -> Glyph | str: ...
@@ -0,0 +1,263 @@
1
+ """Runtime validation helpers exposing canonical graph contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Mapping, MutableMapping, MutableSequence
7
+ from typing import Any, cast
8
+
9
+ import numpy as np
10
+
11
+ from ..alias import (
12
+ get_attr,
13
+ get_theta_attr,
14
+ multi_recompute_abs_max,
15
+ set_attr,
16
+ set_attr_generic,
17
+ set_theta,
18
+ set_theta_attr,
19
+ set_vf,
20
+ )
21
+ from ..constants import DEFAULTS
22
+ from ..types import (
23
+ NodeId,
24
+ TNFRGraph,
25
+ ZERO_BEPI_STORAGE,
26
+ ensure_bepi,
27
+ serialize_bepi,
28
+ )
29
+ from ..utils import clamp, ensure_collection
30
+ from . import ValidationOutcome, Validator
31
+ from .graph import run_validators
32
+
33
+ HistoryLog = MutableSequence[MutableMapping[str, object]]
34
+ """Mutable history buffer storing clamp alerts for strict graphs."""
35
+
36
+ __all__ = (
37
+ "GraphCanonicalValidator",
38
+ "apply_canonical_clamps",
39
+ "validate_canon",
40
+ )
41
+
42
+
43
+ def _max_bepi_magnitude(value: Any) -> float:
44
+ element = ensure_bepi(value)
45
+ mags = [
46
+ float(np.max(np.abs(element.f_continuous))) if element.f_continuous.size else 0.0,
47
+ float(np.max(np.abs(element.a_discrete))) if element.a_discrete.size else 0.0,
48
+ ]
49
+ return float(max(mags)) if mags else 0.0
50
+
51
+
52
+ def _clamp_component(values: Any, lower: float, upper: float) -> np.ndarray:
53
+ array = np.asarray(values, dtype=np.complex128)
54
+ if array.size == 0:
55
+ return array
56
+ magnitudes = np.abs(array)
57
+ result = array.copy()
58
+ above = magnitudes > upper
59
+ if np.any(above):
60
+ result[above] = array[above] * (upper / magnitudes[above])
61
+ if lower > 0.0:
62
+ below = (magnitudes < lower) & (magnitudes > 0.0)
63
+ if np.any(below):
64
+ result[below] = array[below] * (lower / magnitudes[below])
65
+ return result
66
+
67
+
68
+ def _clamp_bepi(value: Any, lower: float, upper: float) -> Any:
69
+ element = ensure_bepi(value)
70
+ clamped_cont = _clamp_component(element.f_continuous, lower, upper)
71
+ clamped_disc = _clamp_component(element.a_discrete, lower, upper)
72
+ return ensure_bepi(
73
+ {"continuous": clamped_cont, "discrete": clamped_disc, "grid": element.x_grid}
74
+ )
75
+
76
+
77
+ def _log_clamp(
78
+ hist: HistoryLog,
79
+ node: NodeId | None,
80
+ attr: str,
81
+ value: float,
82
+ lo: float,
83
+ hi: float,
84
+ ) -> None:
85
+ if value < lo or value > hi:
86
+ hist.append({"node": node, "attr": attr, "value": float(value)})
87
+
88
+
89
+ def apply_canonical_clamps(
90
+ nd: MutableMapping[str, Any],
91
+ G: TNFRGraph | None = None,
92
+ node: NodeId | None = None,
93
+ ) -> None:
94
+ """Clamp nodal EPI, νf and θ according to canonical bounds."""
95
+
96
+ from ..dynamics.aliases import ALIAS_EPI, ALIAS_VF
97
+
98
+ if G is not None:
99
+ graph_dict = cast(MutableMapping[str, Any], G.graph)
100
+ graph_data: Mapping[str, Any] = graph_dict
101
+ else:
102
+ graph_dict = None
103
+ graph_data = DEFAULTS
104
+ eps_min = float(graph_data.get("EPI_MIN", DEFAULTS["EPI_MIN"]))
105
+ eps_max = float(graph_data.get("EPI_MAX", DEFAULTS["EPI_MAX"]))
106
+ vf_min = float(graph_data.get("VF_MIN", DEFAULTS["VF_MIN"]))
107
+ vf_max = float(graph_data.get("VF_MAX", DEFAULTS["VF_MAX"]))
108
+ theta_wrap = bool(graph_data.get("THETA_WRAP", DEFAULTS["THETA_WRAP"]))
109
+
110
+ raw_epi = get_attr(nd, ALIAS_EPI, ZERO_BEPI_STORAGE, conv=lambda obj: obj)
111
+ epi = ensure_bepi(raw_epi)
112
+ vf = get_attr(nd, ALIAS_VF, 0.0)
113
+ th_val = get_theta_attr(nd, 0.0)
114
+ th = 0.0 if th_val is None else float(th_val)
115
+
116
+ strict = bool(
117
+ graph_data.get("VALIDATORS_STRICT", DEFAULTS.get("VALIDATORS_STRICT", False))
118
+ )
119
+ if strict and graph_dict is not None:
120
+ history = cast(MutableMapping[str, Any], graph_dict.setdefault("history", {}))
121
+ alerts = history.get("clamp_alerts")
122
+ if alerts is None:
123
+ hist = cast(HistoryLog, history.setdefault("clamp_alerts", []))
124
+ elif isinstance(alerts, MutableSequence):
125
+ hist = cast(HistoryLog, alerts)
126
+ else:
127
+ materialized = ensure_collection(alerts, max_materialize=None)
128
+ hist = cast(HistoryLog, list(materialized))
129
+ history["clamp_alerts"] = hist
130
+ epi_mag = _max_bepi_magnitude(epi)
131
+ _log_clamp(hist, node, "EPI", epi_mag, eps_min, eps_max)
132
+ _log_clamp(hist, node, "VF", float(vf), vf_min, vf_max)
133
+
134
+ clamped_epi = _clamp_bepi(epi, eps_min, eps_max)
135
+ set_attr_generic(nd, ALIAS_EPI, serialize_bepi(clamped_epi), conv=lambda obj: obj)
136
+
137
+ vf_val = float(clamp(vf, vf_min, vf_max))
138
+ if G is not None and node is not None:
139
+ set_vf(G, node, vf_val, update_max=False)
140
+ else:
141
+ set_attr(nd, ALIAS_VF, vf_val)
142
+
143
+ if theta_wrap:
144
+ new_th = (th + math.pi) % (2 * math.pi) - math.pi
145
+ if G is not None and node is not None:
146
+ set_theta(G, node, new_th)
147
+ else:
148
+ set_theta_attr(nd, new_th)
149
+
150
+
151
+ class GraphCanonicalValidator(Validator[TNFRGraph]):
152
+ """Validator enforcing canonical runtime contracts on TNFR graphs."""
153
+
154
+ recompute_frequency_maxima: bool
155
+ enforce_graph_validators: bool
156
+
157
+ def __init__(
158
+ self,
159
+ *,
160
+ recompute_frequency_maxima: bool = True,
161
+ enforce_graph_validators: bool = True,
162
+ ) -> None:
163
+ self.recompute_frequency_maxima = bool(recompute_frequency_maxima)
164
+ self.enforce_graph_validators = bool(enforce_graph_validators)
165
+
166
+ def _diff_after_clamp(
167
+ self,
168
+ before: Mapping[str, float],
169
+ after: Mapping[str, float],
170
+ ) -> Mapping[str, Mapping[str, float]]:
171
+ changes: dict[str, Mapping[str, float]] = {}
172
+ for key, before_val in before.items():
173
+ after_val = after.get(key)
174
+ if after_val is None:
175
+ continue
176
+ if math.isclose(before_val, after_val, rel_tol=0.0, abs_tol=1e-12):
177
+ continue
178
+ changes[key] = {"before": before_val, "after": after_val}
179
+ return changes
180
+
181
+ def validate(self, subject: TNFRGraph, /, **_: Any) -> ValidationOutcome[TNFRGraph]:
182
+ """Clamp nodal attributes, refresh νf maxima and run graph validators."""
183
+
184
+ from ..dynamics.aliases import ALIAS_EPI, ALIAS_VF
185
+
186
+ clamped_nodes: list[dict[str, Any]] = []
187
+ for node, data in subject.nodes(data=True):
188
+ mapping = cast(MutableMapping[str, Any], data)
189
+ before = {
190
+ "EPI": _max_bepi_magnitude(
191
+ get_attr(mapping, ALIAS_EPI, ZERO_BEPI_STORAGE, conv=lambda obj: obj)
192
+ ),
193
+ "VF": float(get_attr(mapping, ALIAS_VF, 0.0)),
194
+ "THETA": float(get_theta_attr(mapping, 0.0) or 0.0),
195
+ }
196
+ apply_canonical_clamps(mapping, subject, cast(NodeId, node))
197
+ after = {
198
+ "EPI": _max_bepi_magnitude(
199
+ get_attr(mapping, ALIAS_EPI, ZERO_BEPI_STORAGE, conv=lambda obj: obj)
200
+ ),
201
+ "VF": float(get_attr(mapping, ALIAS_VF, 0.0)),
202
+ "THETA": float(get_theta_attr(mapping, 0.0) or 0.0),
203
+ }
204
+ changes = self._diff_after_clamp(before, after)
205
+ if changes:
206
+ clamped_nodes.append({"node": node, "attributes": changes})
207
+
208
+ maxima: Mapping[str, float] = {}
209
+ if self.recompute_frequency_maxima:
210
+ maxima = multi_recompute_abs_max(subject, {"_vfmax": ALIAS_VF})
211
+ subject.graph.update(maxima)
212
+
213
+ errors: list[str] = []
214
+ if self.enforce_graph_validators:
215
+ try:
216
+ run_validators(subject)
217
+ except Exception as exc: # pragma: no cover - defensive
218
+ errors.append(str(exc))
219
+
220
+ summary: dict[str, Any] = {
221
+ "clamped": tuple(clamped_nodes),
222
+ "maxima": dict(maxima),
223
+ }
224
+ if errors:
225
+ summary["errors"] = tuple(errors)
226
+
227
+ passed = not errors
228
+ artifacts: dict[str, Any] = {}
229
+ if clamped_nodes:
230
+ artifacts["clamped_nodes"] = clamped_nodes
231
+ if maxima:
232
+ artifacts["maxima"] = dict(maxima)
233
+
234
+ return ValidationOutcome(
235
+ subject=subject,
236
+ passed=passed,
237
+ summary=summary,
238
+ artifacts=artifacts or None,
239
+ )
240
+
241
+ def report(self, outcome: ValidationOutcome[TNFRGraph]) -> str:
242
+ """Return a concise textual summary of ``outcome``."""
243
+
244
+ if outcome.passed:
245
+ clamped = outcome.summary.get("clamped")
246
+ if clamped:
247
+ return "Graph canonical validation applied clamps successfully."
248
+ return "Graph canonical validation passed without adjustments."
249
+
250
+ errors = outcome.summary.get("errors")
251
+ if not errors:
252
+ return "Graph canonical validation failed without diagnostic details."
253
+ if isinstance(errors, (list, tuple)):
254
+ return "Graph canonical validation errors: " + ", ".join(map(str, errors))
255
+ return f"Graph canonical validation error: {errors}"
256
+
257
+
258
+ def validate_canon(G: TNFRGraph) -> ValidationOutcome[TNFRGraph]:
259
+ """Validate ``G`` using :class:`GraphCanonicalValidator`."""
260
+
261
+ validator = GraphCanonicalValidator()
262
+ return validator.validate(G)
263
+
@@ -0,0 +1,31 @@
1
+ from collections.abc import MutableMapping
2
+ from typing import Any
3
+
4
+ from ..types import NodeId, TNFRGraph
5
+ from . import ValidationOutcome, Validator
6
+
7
+ class GraphCanonicalValidator(Validator[TNFRGraph]):
8
+ def __init__(
9
+ self,
10
+ *,
11
+ recompute_frequency_maxima: bool = ...,
12
+ enforce_graph_validators: bool = ...,
13
+ ) -> None: ...
14
+
15
+ def validate(self, subject: TNFRGraph, /, **kwargs: Any) -> ValidationOutcome[TNFRGraph]: ...
16
+
17
+ def report(self, outcome: ValidationOutcome[TNFRGraph]) -> str: ...
18
+
19
+
20
+ def apply_canonical_clamps(
21
+ nd: MutableMapping[str, Any],
22
+ G: TNFRGraph | None = ...,
23
+ node: NodeId | None = ...,
24
+ ) -> None: ...
25
+
26
+
27
+ def validate_canon(G: TNFRGraph) -> ValidationOutcome[TNFRGraph]: ...
28
+
29
+
30
+ __all__: tuple[str, ...]
31
+