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/validation/rules.py
ADDED
|
@@ -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
|
+
|