tnfr 6.0.0__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 +50 -5
- tnfr/__init__.pyi +0 -7
- tnfr/_compat.py +0 -1
- tnfr/_generated_version.py +34 -0
- tnfr/_version.py +44 -2
- tnfr/alias.py +14 -13
- tnfr/alias.pyi +5 -37
- tnfr/cache.py +9 -729
- tnfr/cache.pyi +8 -224
- tnfr/callback_utils.py +16 -31
- tnfr/callback_utils.pyi +3 -29
- tnfr/cli/__init__.py +17 -11
- tnfr/cli/__init__.pyi +0 -21
- tnfr/cli/arguments.py +175 -14
- tnfr/cli/arguments.pyi +5 -11
- tnfr/cli/execution.py +434 -48
- tnfr/cli/execution.pyi +14 -24
- tnfr/cli/utils.py +20 -3
- tnfr/cli/utils.pyi +5 -5
- tnfr/config/__init__.py +2 -1
- tnfr/config/__init__.pyi +2 -0
- tnfr/config/feature_flags.py +83 -0
- tnfr/config/init.py +1 -1
- tnfr/config/operator_names.py +1 -14
- tnfr/config/presets.py +6 -26
- tnfr/constants/__init__.py +10 -13
- tnfr/constants/__init__.pyi +10 -22
- tnfr/constants/aliases.py +31 -0
- tnfr/constants/core.py +4 -3
- tnfr/constants/init.py +1 -1
- tnfr/constants/metric.py +3 -3
- tnfr/dynamics/__init__.py +64 -10
- tnfr/dynamics/__init__.pyi +3 -4
- tnfr/dynamics/adaptation.py +79 -13
- tnfr/dynamics/aliases.py +10 -9
- tnfr/dynamics/coordination.py +77 -35
- tnfr/dynamics/dnfr.py +575 -274
- tnfr/dynamics/dnfr.pyi +1 -10
- tnfr/dynamics/integrators.py +47 -33
- tnfr/dynamics/integrators.pyi +0 -1
- tnfr/dynamics/runtime.py +489 -129
- tnfr/dynamics/sampling.py +2 -0
- tnfr/dynamics/selectors.py +101 -62
- tnfr/execution.py +15 -8
- tnfr/execution.pyi +5 -25
- tnfr/flatten.py +7 -3
- tnfr/flatten.pyi +1 -8
- tnfr/gamma.py +22 -26
- tnfr/gamma.pyi +0 -6
- tnfr/glyph_history.py +37 -26
- tnfr/glyph_history.pyi +1 -19
- tnfr/glyph_runtime.py +16 -0
- tnfr/glyph_runtime.pyi +9 -0
- tnfr/immutable.py +20 -15
- tnfr/immutable.pyi +4 -7
- tnfr/initialization.py +5 -7
- tnfr/initialization.pyi +1 -9
- tnfr/io.py +6 -305
- tnfr/io.pyi +13 -8
- 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/coherence.py +123 -94
- tnfr/metrics/common.py +22 -13
- tnfr/metrics/common.pyi +42 -11
- tnfr/metrics/core.py +72 -14
- tnfr/metrics/diagnosis.py +48 -57
- tnfr/metrics/diagnosis.pyi +3 -7
- tnfr/metrics/export.py +3 -5
- tnfr/metrics/glyph_timing.py +41 -31
- tnfr/metrics/reporting.py +13 -6
- tnfr/metrics/sense_index.py +884 -114
- tnfr/metrics/trig.py +167 -11
- tnfr/metrics/trig.pyi +1 -0
- tnfr/metrics/trig_cache.py +112 -15
- tnfr/node.py +400 -17
- tnfr/node.pyi +55 -38
- tnfr/observers.py +111 -8
- tnfr/observers.pyi +0 -15
- tnfr/ontosim.py +9 -6
- tnfr/ontosim.pyi +0 -5
- tnfr/operators/__init__.py +529 -42
- tnfr/operators/__init__.pyi +14 -0
- tnfr/operators/definitions.py +350 -18
- tnfr/operators/definitions.pyi +0 -14
- tnfr/operators/grammar.py +760 -0
- tnfr/operators/jitter.py +28 -22
- tnfr/operators/registry.py +7 -12
- tnfr/operators/registry.pyi +0 -2
- tnfr/operators/remesh.py +38 -61
- tnfr/rng.py +17 -300
- tnfr/schemas/__init__.py +8 -0
- tnfr/schemas/grammar.json +94 -0
- tnfr/selector.py +3 -4
- tnfr/selector.pyi +1 -1
- tnfr/sense.py +22 -24
- tnfr/sense.pyi +0 -7
- tnfr/structural.py +504 -21
- tnfr/structural.pyi +41 -18
- tnfr/telemetry/__init__.py +23 -1
- tnfr/telemetry/cache_metrics.py +226 -0
- tnfr/telemetry/nu_f.py +423 -0
- tnfr/telemetry/nu_f.pyi +123 -0
- tnfr/tokens.py +1 -4
- tnfr/tokens.pyi +1 -6
- tnfr/trace.py +20 -53
- tnfr/trace.pyi +9 -37
- tnfr/types.py +244 -15
- tnfr/types.pyi +200 -14
- tnfr/units.py +69 -0
- tnfr/units.pyi +16 -0
- tnfr/utils/__init__.py +107 -48
- tnfr/utils/__init__.pyi +80 -11
- tnfr/utils/cache.py +1705 -65
- tnfr/utils/cache.pyi +370 -58
- tnfr/utils/chunks.py +104 -0
- tnfr/utils/chunks.pyi +21 -0
- tnfr/utils/data.py +95 -5
- tnfr/utils/data.pyi +8 -17
- tnfr/utils/graph.py +2 -4
- tnfr/utils/init.py +31 -7
- tnfr/utils/init.pyi +4 -11
- tnfr/utils/io.py +313 -14
- tnfr/{helpers → utils}/numeric.py +50 -24
- tnfr/utils/numeric.pyi +21 -0
- tnfr/validation/__init__.py +92 -4
- tnfr/validation/__init__.pyi +77 -17
- tnfr/validation/compatibility.py +79 -43
- tnfr/validation/compatibility.pyi +4 -6
- tnfr/validation/grammar.py +55 -133
- tnfr/validation/grammar.pyi +37 -8
- tnfr/validation/graph.py +138 -0
- tnfr/validation/graph.pyi +17 -0
- tnfr/validation/rules.py +161 -74
- tnfr/validation/rules.pyi +55 -18
- 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 +28 -139
- tnfr/validation/syntax.pyi +7 -4
- 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-6.0.0.dist-info → tnfr-7.0.0.dist-info}/METADATA +63 -19
- tnfr-7.0.0.dist-info/RECORD +185 -0
- {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
- tnfr/constants_glyphs.py +0 -16
- tnfr/constants_glyphs.pyi +0 -12
- tnfr/grammar.py +0 -25
- tnfr/grammar.pyi +0 -13
- tnfr/helpers/__init__.py +0 -151
- tnfr/helpers/__init__.pyi +0 -66
- tnfr/helpers/numeric.pyi +0 -12
- tnfr/presets.py +0 -15
- tnfr/presets.pyi +0 -7
- tnfr/utils/io.pyi +0 -10
- tnfr/utils/validators.py +0 -130
- tnfr/utils/validators.pyi +0 -19
- tnfr-6.0.0.dist-info/RECORD +0 -157
- {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
- {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
tnfr/validation/rules.py
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
"""Validation helpers grouped by rule type.
|
|
2
2
|
|
|
3
3
|
These utilities implement the canonical checks required by
|
|
4
|
-
:mod:`tnfr.
|
|
4
|
+
:mod:`tnfr.operators.grammar`. They are organised here to make it
|
|
5
5
|
explicit which pieces enforce repetition control, transition
|
|
6
6
|
compatibility or stabilisation thresholds.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
from
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Mapping
|
|
12
13
|
|
|
13
14
|
from ..alias import get_attr
|
|
14
|
-
from ..constants import
|
|
15
|
-
from ..
|
|
16
|
-
|
|
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
|
|
17
26
|
from ..metrics.common import normalize_dnfr
|
|
18
27
|
from ..types import Glyph
|
|
19
|
-
from .compatibility import CANON_COMPAT, CANON_FALLBACK
|
|
20
28
|
|
|
21
29
|
if TYPE_CHECKING: # pragma: no cover - only for typing
|
|
22
|
-
from .grammar import GrammarContext
|
|
23
|
-
|
|
24
|
-
ALIAS_SI = get_aliases("SI")
|
|
25
|
-
ALIAS_D2EPI = get_aliases("D2EPI")
|
|
30
|
+
from ..operators.grammar import GrammarContext
|
|
26
31
|
|
|
27
32
|
__all__ = [
|
|
28
33
|
"coerce_glyph",
|
|
@@ -31,9 +36,6 @@ __all__ = [
|
|
|
31
36
|
"normalized_dnfr",
|
|
32
37
|
"_norm_attr",
|
|
33
38
|
"_si",
|
|
34
|
-
"_accel_norm",
|
|
35
|
-
"_check_repeats",
|
|
36
|
-
"_maybe_force",
|
|
37
39
|
"_check_oz_to_zhir",
|
|
38
40
|
"_check_thol_closure",
|
|
39
41
|
"_check_compatibility",
|
|
@@ -46,6 +48,13 @@ def coerce_glyph(val: Any) -> Glyph | Any:
|
|
|
46
48
|
try:
|
|
47
49
|
return Glyph(val)
|
|
48
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
|
|
49
58
|
return val
|
|
50
59
|
|
|
51
60
|
|
|
@@ -53,19 +62,27 @@ def glyph_fallback(cand_key: str, fallbacks: Mapping[str, Any]) -> Glyph | str:
|
|
|
53
62
|
"""Determine fallback glyph for ``cand_key`` considering canon tables."""
|
|
54
63
|
|
|
55
64
|
glyph_key = coerce_glyph(cand_key)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
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)
|
|
63
79
|
|
|
64
80
|
|
|
65
81
|
# -------------------------
|
|
66
82
|
# Normalisation helpers
|
|
67
83
|
# -------------------------
|
|
68
84
|
|
|
85
|
+
|
|
69
86
|
def get_norm(ctx: "GrammarContext", key: str) -> float:
|
|
70
87
|
"""Retrieve a global normalisation value from ``ctx.norms``."""
|
|
71
88
|
|
|
@@ -85,12 +102,6 @@ def _si(nd) -> float:
|
|
|
85
102
|
return clamp01(get_attr(nd, ALIAS_SI, 0.5))
|
|
86
103
|
|
|
87
104
|
|
|
88
|
-
def _accel_norm(ctx: "GrammarContext", nd) -> float:
|
|
89
|
-
"""Normalise acceleration using the global maximum."""
|
|
90
|
-
|
|
91
|
-
return _norm_attr(ctx, nd, ALIAS_D2EPI, "accel_max")
|
|
92
|
-
|
|
93
|
-
|
|
94
105
|
def normalized_dnfr(ctx: "GrammarContext", nd) -> float:
|
|
95
106
|
"""Normalise |ΔNFR| using the configured global maximum."""
|
|
96
107
|
|
|
@@ -98,55 +109,66 @@ def normalized_dnfr(ctx: "GrammarContext", nd) -> float:
|
|
|
98
109
|
|
|
99
110
|
|
|
100
111
|
# -------------------------
|
|
101
|
-
#
|
|
112
|
+
# Translation helpers
|
|
102
113
|
# -------------------------
|
|
103
114
|
|
|
104
|
-
def _check_repeats(ctx: "GrammarContext", n, cand: Glyph | str) -> Glyph | str:
|
|
105
|
-
"""Avoid recent repetitions according to ``ctx.cfg_soft``."""
|
|
106
|
-
|
|
107
|
-
nd = ctx.G.nodes[n]
|
|
108
|
-
cfg = ctx.cfg_soft
|
|
109
|
-
gwin = int(cfg.get("window", 0))
|
|
110
|
-
avoid = set(cfg.get("avoid_repeats", []))
|
|
111
|
-
fallbacks = cfg.get("fallbacks", {})
|
|
112
|
-
cand_key = cand.value if isinstance(cand, Glyph) else str(cand)
|
|
113
|
-
if gwin > 0 and cand_key in avoid and recent_glyph(nd, cand_key, gwin):
|
|
114
|
-
return glyph_fallback(cand_key, fallbacks)
|
|
115
|
-
return cand
|
|
116
115
|
|
|
116
|
+
def _structural_label(value: object) -> str:
|
|
117
|
+
"""Return the canonical structural name for ``value`` when possible."""
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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))
|
|
127
131
|
|
|
128
|
-
if cand == original:
|
|
129
|
-
return cand
|
|
130
|
-
force_th = float(ctx.cfg_soft.get(key, 0.60))
|
|
131
|
-
if accessor(ctx, ctx.G.nodes[n]) >= force_th:
|
|
132
|
-
return original
|
|
133
|
-
return cand
|
|
134
132
|
|
|
133
|
+
# -------------------------
|
|
134
|
+
# Validation rules
|
|
135
|
+
# -------------------------
|
|
135
136
|
|
|
136
137
|
def _check_oz_to_zhir(ctx: "GrammarContext", n, cand: Glyph | str) -> Glyph | str:
|
|
137
138
|
"""Enforce OZ precedents before allowing ZHIR mutations."""
|
|
138
139
|
|
|
140
|
+
from ..glyph_history import recent_glyph
|
|
139
141
|
nd = ctx.G.nodes[n]
|
|
140
142
|
cand_glyph = coerce_glyph(cand)
|
|
141
|
-
|
|
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:
|
|
142
146
|
cfg = ctx.cfg_canon
|
|
143
147
|
win = int(cfg.get("zhir_requires_oz_window", 3))
|
|
144
148
|
dn_min = float(cfg.get("zhir_dnfr_min", 0.05))
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
149
|
-
|
|
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
|
+
)
|
|
150
172
|
return cand
|
|
151
173
|
|
|
152
174
|
|
|
@@ -162,14 +184,48 @@ def _check_thol_closure(
|
|
|
162
184
|
minlen = int(cfg.get("thol_min_len", 2))
|
|
163
185
|
maxlen = int(cfg.get("thol_max_len", 6))
|
|
164
186
|
close_dn = float(cfg.get("thol_close_dnfr", 0.15))
|
|
165
|
-
|
|
166
|
-
st["thol_len"] >= minlen
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
},
|
|
173
229
|
)
|
|
174
230
|
return cand
|
|
175
231
|
|
|
@@ -183,12 +239,43 @@ def _check_compatibility(ctx: "GrammarContext", n, cand: Glyph | str) -> Glyph |
|
|
|
183
239
|
prev_glyph = coerce_glyph(prev)
|
|
184
240
|
cand_glyph = coerce_glyph(cand)
|
|
185
241
|
if isinstance(prev_glyph, Glyph):
|
|
186
|
-
|
|
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)
|
|
187
248
|
if allowed is None:
|
|
188
249
|
return cand
|
|
189
|
-
if isinstance(cand_glyph, Glyph)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
+
)
|
|
194
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
|
tnfr/validation/rules.pyi
CHANGED
|
@@ -1,18 +1,55 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
+
|