tnfr 3.0.3__py3-none-any.whl → 8.5.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 +375 -56
- 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 +723 -0
- tnfr/alias.pyi +108 -0
- tnfr/backends/__init__.py +354 -0
- tnfr/backends/jax_backend.py +173 -0
- tnfr/backends/numpy_backend.py +238 -0
- tnfr/backends/optimized_numpy.py +420 -0
- tnfr/backends/torch_backend.py +408 -0
- tnfr/cache.py +171 -0
- tnfr/cache.pyi +13 -0
- tnfr/cli/__init__.py +110 -0
- tnfr/cli/__init__.pyi +26 -0
- tnfr/cli/arguments.py +489 -0
- tnfr/cli/arguments.pyi +29 -0
- tnfr/cli/execution.py +914 -0
- tnfr/cli/execution.pyi +70 -0
- tnfr/cli/interactive_validator.py +614 -0
- tnfr/cli/utils.py +51 -0
- tnfr/cli/utils.pyi +7 -0
- tnfr/cli/validate.py +236 -0
- tnfr/compat/__init__.py +85 -0
- tnfr/compat/dataclass.py +136 -0
- tnfr/compat/jsonschema_stub.py +61 -0
- tnfr/compat/matplotlib_stub.py +73 -0
- tnfr/compat/numpy_stub.py +155 -0
- tnfr/config/__init__.py +224 -0
- tnfr/config/__init__.pyi +10 -0
- tnfr/config/constants.py +104 -0
- tnfr/config/constants.pyi +12 -0
- tnfr/config/defaults.py +54 -0
- tnfr/config/defaults_core.py +212 -0
- tnfr/config/defaults_init.py +33 -0
- tnfr/config/defaults_metric.py +104 -0
- tnfr/config/feature_flags.py +81 -0
- tnfr/config/feature_flags.pyi +16 -0
- tnfr/config/glyph_constants.py +31 -0
- tnfr/config/init.py +77 -0
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +254 -0
- tnfr/config/operator_names.pyi +36 -0
- tnfr/config/physics_derivation.py +354 -0
- tnfr/config/presets.py +83 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/config/security.py +927 -0
- tnfr/config/thresholds.py +114 -0
- tnfr/config/tnfr_config.py +498 -0
- tnfr/constants/__init__.py +92 -0
- tnfr/constants/__init__.pyi +92 -0
- tnfr/constants/aliases.py +33 -0
- tnfr/constants/aliases.pyi +27 -0
- tnfr/constants/init.py +33 -0
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +104 -0
- tnfr/constants/metric.pyi +19 -0
- tnfr/core/__init__.py +33 -0
- tnfr/core/container.py +226 -0
- tnfr/core/default_implementations.py +329 -0
- tnfr/core/interfaces.py +279 -0
- tnfr/dynamics/__init__.py +238 -0
- tnfr/dynamics/__init__.pyi +83 -0
- tnfr/dynamics/adaptation.py +267 -0
- tnfr/dynamics/adaptation.pyi +7 -0
- tnfr/dynamics/adaptive_sequences.py +189 -0
- tnfr/dynamics/adaptive_sequences.pyi +14 -0
- tnfr/dynamics/aliases.py +23 -0
- tnfr/dynamics/aliases.pyi +19 -0
- tnfr/dynamics/bifurcation.py +232 -0
- tnfr/dynamics/canonical.py +229 -0
- tnfr/dynamics/canonical.pyi +48 -0
- tnfr/dynamics/coordination.py +385 -0
- tnfr/dynamics/coordination.pyi +25 -0
- tnfr/dynamics/dnfr.py +3034 -0
- tnfr/dynamics/dnfr.pyi +26 -0
- tnfr/dynamics/dynamic_limits.py +225 -0
- tnfr/dynamics/feedback.py +252 -0
- tnfr/dynamics/feedback.pyi +24 -0
- tnfr/dynamics/fused_dnfr.py +454 -0
- tnfr/dynamics/homeostasis.py +157 -0
- tnfr/dynamics/homeostasis.pyi +14 -0
- tnfr/dynamics/integrators.py +661 -0
- tnfr/dynamics/integrators.pyi +36 -0
- tnfr/dynamics/learning.py +310 -0
- tnfr/dynamics/learning.pyi +33 -0
- tnfr/dynamics/metabolism.py +254 -0
- tnfr/dynamics/nbody.py +796 -0
- tnfr/dynamics/nbody_tnfr.py +783 -0
- tnfr/dynamics/propagation.py +326 -0
- tnfr/dynamics/runtime.py +908 -0
- tnfr/dynamics/runtime.pyi +77 -0
- tnfr/dynamics/sampling.py +36 -0
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +711 -0
- tnfr/dynamics/selectors.pyi +85 -0
- tnfr/dynamics/structural_clip.py +207 -0
- tnfr/errors/__init__.py +37 -0
- tnfr/errors/contextual.py +492 -0
- tnfr/execution.py +223 -0
- tnfr/execution.pyi +45 -0
- tnfr/extensions/__init__.py +205 -0
- tnfr/extensions/__init__.pyi +18 -0
- tnfr/extensions/base.py +173 -0
- tnfr/extensions/base.pyi +35 -0
- tnfr/extensions/business/__init__.py +71 -0
- tnfr/extensions/business/__init__.pyi +11 -0
- tnfr/extensions/business/cookbook.py +88 -0
- tnfr/extensions/business/cookbook.pyi +8 -0
- tnfr/extensions/business/health_analyzers.py +202 -0
- tnfr/extensions/business/health_analyzers.pyi +9 -0
- tnfr/extensions/business/patterns.py +183 -0
- tnfr/extensions/business/patterns.pyi +8 -0
- tnfr/extensions/medical/__init__.py +73 -0
- tnfr/extensions/medical/__init__.pyi +11 -0
- tnfr/extensions/medical/cookbook.py +88 -0
- tnfr/extensions/medical/cookbook.pyi +8 -0
- tnfr/extensions/medical/health_analyzers.py +181 -0
- tnfr/extensions/medical/health_analyzers.pyi +9 -0
- tnfr/extensions/medical/patterns.py +163 -0
- tnfr/extensions/medical/patterns.pyi +8 -0
- tnfr/flatten.py +262 -0
- tnfr/flatten.pyi +21 -0
- tnfr/gamma.py +354 -0
- tnfr/gamma.pyi +36 -0
- tnfr/glyph_history.py +377 -0
- tnfr/glyph_history.pyi +35 -0
- tnfr/glyph_runtime.py +19 -0
- tnfr/glyph_runtime.pyi +8 -0
- tnfr/immutable.py +218 -0
- tnfr/immutable.pyi +36 -0
- tnfr/initialization.py +203 -0
- tnfr/initialization.pyi +65 -0
- tnfr/io.py +10 -0
- tnfr/io.pyi +13 -0
- tnfr/locking.py +37 -0
- tnfr/locking.pyi +7 -0
- tnfr/mathematics/__init__.py +79 -0
- tnfr/mathematics/backend.py +453 -0
- tnfr/mathematics/backend.pyi +99 -0
- tnfr/mathematics/dynamics.py +408 -0
- tnfr/mathematics/dynamics.pyi +90 -0
- tnfr/mathematics/epi.py +391 -0
- tnfr/mathematics/epi.pyi +65 -0
- tnfr/mathematics/generators.py +242 -0
- tnfr/mathematics/generators.pyi +29 -0
- tnfr/mathematics/metrics.py +119 -0
- tnfr/mathematics/metrics.pyi +16 -0
- tnfr/mathematics/operators.py +239 -0
- tnfr/mathematics/operators.pyi +59 -0
- tnfr/mathematics/operators_factory.py +124 -0
- tnfr/mathematics/operators_factory.pyi +11 -0
- tnfr/mathematics/projection.py +87 -0
- tnfr/mathematics/projection.pyi +33 -0
- tnfr/mathematics/runtime.py +182 -0
- tnfr/mathematics/runtime.pyi +64 -0
- tnfr/mathematics/spaces.py +256 -0
- tnfr/mathematics/spaces.pyi +83 -0
- tnfr/mathematics/transforms.py +305 -0
- tnfr/mathematics/transforms.pyi +62 -0
- tnfr/metrics/__init__.py +79 -0
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/buffer_cache.py +163 -0
- tnfr/metrics/buffer_cache.pyi +24 -0
- tnfr/metrics/cache_utils.py +214 -0
- tnfr/metrics/coherence.py +2009 -0
- tnfr/metrics/coherence.pyi +129 -0
- tnfr/metrics/common.py +158 -0
- tnfr/metrics/common.pyi +35 -0
- tnfr/metrics/core.py +316 -0
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +833 -0
- tnfr/metrics/diagnosis.pyi +86 -0
- tnfr/metrics/emergence.py +245 -0
- tnfr/metrics/export.py +179 -0
- tnfr/metrics/export.pyi +7 -0
- tnfr/metrics/glyph_timing.py +379 -0
- tnfr/metrics/glyph_timing.pyi +81 -0
- tnfr/metrics/learning_metrics.py +280 -0
- tnfr/metrics/learning_metrics.pyi +21 -0
- tnfr/metrics/phase_coherence.py +351 -0
- tnfr/metrics/phase_compatibility.py +349 -0
- tnfr/metrics/reporting.py +183 -0
- tnfr/metrics/reporting.pyi +25 -0
- tnfr/metrics/sense_index.py +1203 -0
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +373 -0
- tnfr/metrics/trig.pyi +13 -0
- tnfr/metrics/trig_cache.py +233 -0
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/multiscale/__init__.py +32 -0
- tnfr/multiscale/hierarchical.py +517 -0
- tnfr/node.py +763 -0
- tnfr/node.pyi +139 -0
- tnfr/observers.py +255 -130
- tnfr/observers.pyi +31 -0
- tnfr/ontosim.py +144 -137
- tnfr/ontosim.pyi +28 -0
- tnfr/operators/__init__.py +1672 -0
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/algebra.py +277 -0
- tnfr/operators/canonical_patterns.py +420 -0
- tnfr/operators/cascade.py +267 -0
- tnfr/operators/cycle_detection.py +358 -0
- tnfr/operators/definitions.py +4108 -0
- tnfr/operators/definitions.pyi +78 -0
- tnfr/operators/grammar.py +1164 -0
- tnfr/operators/grammar.pyi +140 -0
- tnfr/operators/hamiltonian.py +710 -0
- tnfr/operators/health_analyzer.py +809 -0
- tnfr/operators/jitter.py +272 -0
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/lifecycle.py +314 -0
- tnfr/operators/metabolism.py +618 -0
- tnfr/operators/metrics.py +2138 -0
- tnfr/operators/network_analysis/__init__.py +27 -0
- tnfr/operators/network_analysis/source_detection.py +186 -0
- tnfr/operators/nodal_equation.py +395 -0
- tnfr/operators/pattern_detection.py +660 -0
- tnfr/operators/patterns.py +669 -0
- tnfr/operators/postconditions/__init__.py +38 -0
- tnfr/operators/postconditions/mutation.py +236 -0
- tnfr/operators/preconditions/__init__.py +1226 -0
- tnfr/operators/preconditions/coherence.py +305 -0
- tnfr/operators/preconditions/dissonance.py +236 -0
- tnfr/operators/preconditions/emission.py +128 -0
- tnfr/operators/preconditions/mutation.py +580 -0
- tnfr/operators/preconditions/reception.py +125 -0
- tnfr/operators/preconditions/resonance.py +364 -0
- tnfr/operators/registry.py +74 -0
- tnfr/operators/registry.pyi +9 -0
- tnfr/operators/remesh.py +1809 -0
- tnfr/operators/remesh.pyi +26 -0
- tnfr/operators/structural_units.py +268 -0
- tnfr/operators/unified_grammar.py +105 -0
- tnfr/parallel/__init__.py +54 -0
- tnfr/parallel/auto_scaler.py +234 -0
- tnfr/parallel/distributed.py +384 -0
- tnfr/parallel/engine.py +238 -0
- tnfr/parallel/gpu_engine.py +420 -0
- tnfr/parallel/monitoring.py +248 -0
- tnfr/parallel/partitioner.py +459 -0
- tnfr/py.typed +0 -0
- tnfr/recipes/__init__.py +22 -0
- tnfr/recipes/cookbook.py +743 -0
- tnfr/rng.py +178 -0
- tnfr/rng.pyi +26 -0
- tnfr/schemas/__init__.py +8 -0
- tnfr/schemas/grammar.json +94 -0
- tnfr/sdk/__init__.py +107 -0
- tnfr/sdk/__init__.pyi +19 -0
- tnfr/sdk/adaptive_system.py +173 -0
- tnfr/sdk/adaptive_system.pyi +21 -0
- tnfr/sdk/builders.py +370 -0
- tnfr/sdk/builders.pyi +51 -0
- tnfr/sdk/fluent.py +1121 -0
- tnfr/sdk/fluent.pyi +74 -0
- tnfr/sdk/templates.py +342 -0
- tnfr/sdk/templates.pyi +41 -0
- tnfr/sdk/utils.py +341 -0
- tnfr/secure_config.py +46 -0
- tnfr/security/__init__.py +70 -0
- tnfr/security/database.py +514 -0
- tnfr/security/subprocess.py +503 -0
- tnfr/security/validation.py +290 -0
- tnfr/selector.py +247 -0
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +378 -0
- tnfr/sense.pyi +23 -0
- tnfr/services/__init__.py +17 -0
- tnfr/services/orchestrator.py +325 -0
- tnfr/sparse/__init__.py +39 -0
- tnfr/sparse/representations.py +492 -0
- tnfr/structural.py +705 -0
- tnfr/structural.pyi +83 -0
- tnfr/telemetry/__init__.py +35 -0
- tnfr/telemetry/cache_metrics.py +226 -0
- tnfr/telemetry/cache_metrics.pyi +64 -0
- tnfr/telemetry/nu_f.py +422 -0
- tnfr/telemetry/nu_f.pyi +108 -0
- tnfr/telemetry/verbosity.py +36 -0
- tnfr/telemetry/verbosity.pyi +15 -0
- tnfr/tokens.py +58 -0
- tnfr/tokens.pyi +36 -0
- tnfr/tools/__init__.py +20 -0
- tnfr/tools/domain_templates.py +478 -0
- tnfr/tools/sequence_generator.py +846 -0
- tnfr/topology/__init__.py +13 -0
- tnfr/topology/asymmetry.py +151 -0
- tnfr/trace.py +543 -0
- tnfr/trace.pyi +42 -0
- tnfr/tutorials/__init__.py +38 -0
- tnfr/tutorials/autonomous_evolution.py +285 -0
- tnfr/tutorials/interactive.py +1576 -0
- tnfr/tutorials/structural_metabolism.py +238 -0
- tnfr/types.py +775 -0
- tnfr/types.pyi +357 -0
- tnfr/units.py +68 -0
- tnfr/units.pyi +13 -0
- tnfr/utils/__init__.py +282 -0
- tnfr/utils/__init__.pyi +215 -0
- tnfr/utils/cache.py +4223 -0
- tnfr/utils/cache.pyi +470 -0
- tnfr/utils/callbacks.py +375 -0
- tnfr/utils/callbacks.pyi +49 -0
- tnfr/utils/chunks.py +108 -0
- tnfr/utils/chunks.pyi +22 -0
- tnfr/utils/data.py +428 -0
- tnfr/utils/data.pyi +74 -0
- tnfr/utils/graph.py +85 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +821 -0
- tnfr/utils/init.pyi +80 -0
- tnfr/utils/io.py +559 -0
- tnfr/utils/io.pyi +66 -0
- tnfr/utils/numeric.py +114 -0
- tnfr/utils/numeric.pyi +21 -0
- tnfr/validation/__init__.py +257 -0
- tnfr/validation/__init__.pyi +85 -0
- tnfr/validation/compatibility.py +460 -0
- tnfr/validation/compatibility.pyi +6 -0
- tnfr/validation/config.py +73 -0
- tnfr/validation/graph.py +139 -0
- tnfr/validation/graph.pyi +18 -0
- tnfr/validation/input_validation.py +755 -0
- tnfr/validation/invariants.py +712 -0
- tnfr/validation/rules.py +253 -0
- tnfr/validation/rules.pyi +44 -0
- tnfr/validation/runtime.py +279 -0
- tnfr/validation/runtime.pyi +28 -0
- tnfr/validation/sequence_validator.py +162 -0
- tnfr/validation/soft_filters.py +170 -0
- tnfr/validation/soft_filters.pyi +32 -0
- tnfr/validation/spectral.py +164 -0
- tnfr/validation/spectral.pyi +42 -0
- tnfr/validation/validator.py +1266 -0
- tnfr/validation/window.py +39 -0
- tnfr/validation/window.pyi +1 -0
- tnfr/visualization/__init__.py +98 -0
- tnfr/visualization/cascade_viz.py +256 -0
- tnfr/visualization/hierarchy.py +284 -0
- tnfr/visualization/sequence_plotter.py +784 -0
- tnfr/viz/__init__.py +60 -0
- tnfr/viz/matplotlib.py +278 -0
- tnfr/viz/matplotlib.pyi +35 -0
- tnfr-8.5.0.dist-info/METADATA +573 -0
- tnfr-8.5.0.dist-info/RECORD +353 -0
- tnfr-8.5.0.dist-info/entry_points.txt +3 -0
- tnfr-3.0.3.dist-info/licenses/LICENSE.txt → tnfr-8.5.0.dist-info/licenses/LICENSE.md +1 -1
- tnfr/constants.py +0 -183
- tnfr/dynamics.py +0 -543
- tnfr/helpers.py +0 -198
- tnfr/main.py +0 -37
- tnfr/operators.py +0 -296
- tnfr-3.0.3.dist-info/METADATA +0 -35
- tnfr-3.0.3.dist-info/RECORD +0 -13
- {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
- {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
tnfr/validation/rules.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Validation helpers grouped by rule type.
|
|
2
|
+
|
|
3
|
+
These utilities implement the canonical checks required by
|
|
4
|
+
:mod:`tnfr.validation`. 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 # Invalid glyph candidate, return as-is
|
|
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``.
|
|
63
|
+
|
|
64
|
+
Note: Compatibility table fallbacks have been deprecated.
|
|
65
|
+
Only explicit fallback overrides are now supported.
|
|
66
|
+
Grammar rules emerge naturally from TNFR structural dynamics.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
glyph_key = coerce_glyph(cand_key)
|
|
70
|
+
fb_override = fallbacks.get(cand_key)
|
|
71
|
+
if fb_override is not None:
|
|
72
|
+
return coerce_glyph(fb_override)
|
|
73
|
+
|
|
74
|
+
# No automatic fallback - let frequency validation handle compatibility
|
|
75
|
+
return coerce_glyph(cand_key)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# -------------------------
|
|
79
|
+
# Normalisation helpers
|
|
80
|
+
# -------------------------
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_norm(ctx: "GrammarContext", key: str) -> float:
|
|
84
|
+
"""Retrieve a global normalisation value from ``ctx.norms``."""
|
|
85
|
+
|
|
86
|
+
return float(ctx.norms.get(key, 1.0)) or 1.0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _norm_attr(ctx: "GrammarContext", nd, attr_alias: str, norm_key: str) -> float:
|
|
90
|
+
"""Normalise ``attr_alias`` using the global maximum ``norm_key``."""
|
|
91
|
+
|
|
92
|
+
max_val = get_norm(ctx, norm_key)
|
|
93
|
+
return clamp01(abs(get_attr(nd, attr_alias, 0.0)) / max_val)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _si(nd) -> float:
|
|
97
|
+
"""Return the structural sense index for ``nd`` clamped to ``[0, 1]``."""
|
|
98
|
+
|
|
99
|
+
return clamp01(get_attr(nd, ALIAS_SI, 0.5))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def normalized_dnfr(ctx: "GrammarContext", nd) -> float:
|
|
103
|
+
"""Normalise |ΔNFR| using the configured global maximum."""
|
|
104
|
+
|
|
105
|
+
return normalize_dnfr(nd, get_norm(ctx, "dnfr_max"))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# -------------------------
|
|
109
|
+
# Translation helpers
|
|
110
|
+
# -------------------------
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _structural_label(value: object) -> str:
|
|
114
|
+
"""Return the canonical structural name for ``value`` when possible."""
|
|
115
|
+
|
|
116
|
+
glyph_to_name = _functional_translators()[0]
|
|
117
|
+
coerced = coerce_glyph(value)
|
|
118
|
+
if isinstance(coerced, Glyph):
|
|
119
|
+
name = glyph_to_name(coerced)
|
|
120
|
+
if name is not None:
|
|
121
|
+
return name
|
|
122
|
+
name = glyph_to_name(value if isinstance(value, Glyph) else value)
|
|
123
|
+
if name is not None:
|
|
124
|
+
return name
|
|
125
|
+
if value is None:
|
|
126
|
+
return "unknown"
|
|
127
|
+
return canonical_operator_name(str(value))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# -------------------------
|
|
131
|
+
# Validation rules
|
|
132
|
+
# -------------------------
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _check_oz_to_zhir(ctx: "GrammarContext", n, cand: Glyph | str) -> Glyph | str:
|
|
136
|
+
"""Enforce OZ precedents before allowing ZHIR mutations.
|
|
137
|
+
|
|
138
|
+
When mutation is attempted without recent dissonance and low ΔNFR,
|
|
139
|
+
returns DISSONANCE as a fallback glyph (structural requirement).
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
from ..glyph_history import recent_glyph
|
|
143
|
+
|
|
144
|
+
nd = ctx.G.nodes[n]
|
|
145
|
+
cand_glyph = coerce_glyph(cand)
|
|
146
|
+
glyph_to_name, name_to_glyph = _functional_translators()
|
|
147
|
+
cand_name = glyph_to_name(cand_glyph if isinstance(cand_glyph, Glyph) else cand)
|
|
148
|
+
if cand_name == MUTATION:
|
|
149
|
+
cfg = ctx.cfg_canon
|
|
150
|
+
win = int(cfg.get("zhir_requires_oz_window", 3))
|
|
151
|
+
dn_min = float(cfg.get("zhir_dnfr_min", 0.05))
|
|
152
|
+
dissonance_glyph = name_to_glyph(DISSONANCE)
|
|
153
|
+
if dissonance_glyph is None:
|
|
154
|
+
return cand
|
|
155
|
+
norm_dn = normalized_dnfr(ctx, nd)
|
|
156
|
+
recent_glyph(nd, dissonance_glyph.value, win)
|
|
157
|
+
history = tuple(_structural_label(item) for item in nd.get("glyph_history", ()))
|
|
158
|
+
has_recent_dissonance = any(entry == DISSONANCE for entry in history[-win:])
|
|
159
|
+
if not has_recent_dissonance and norm_dn < dn_min:
|
|
160
|
+
# Return dissonance as fallback - structural requirement for mutation
|
|
161
|
+
# Maintains TNFR invariant: mutation requires prior dissonance (§3.4 operator closure)
|
|
162
|
+
return dissonance_glyph
|
|
163
|
+
return cand
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _check_thol_closure(
|
|
167
|
+
ctx: "GrammarContext", n, cand: Glyph | str, st: dict[str, Any]
|
|
168
|
+
) -> Glyph | str:
|
|
169
|
+
"""Close THOL blocks with canonical glyphs once stabilised."""
|
|
170
|
+
|
|
171
|
+
nd = ctx.G.nodes[n]
|
|
172
|
+
if st.get("thol_open", False):
|
|
173
|
+
glyph_to_name, name_to_glyph = _functional_translators()
|
|
174
|
+
cand_glyph = coerce_glyph(cand)
|
|
175
|
+
cand_name = glyph_to_name(cand_glyph if isinstance(cand_glyph, Glyph) else cand)
|
|
176
|
+
|
|
177
|
+
# Allow nested THOL (self_organization) blocks without incrementing length
|
|
178
|
+
# TNFR invariant: operational fractality (§3.7)
|
|
179
|
+
if cand_name == SELF_ORGANIZATION:
|
|
180
|
+
return cand
|
|
181
|
+
|
|
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
|
+
si_high = float(cfg.get("si_high", 0.66))
|
|
192
|
+
si = _si(nd)
|
|
193
|
+
target_name = SILENCE if si >= si_high else CONTRACTION
|
|
194
|
+
target_glyph = name_to_glyph(target_name)
|
|
195
|
+
|
|
196
|
+
if cand_name == target_name and isinstance(cand_glyph, Glyph):
|
|
197
|
+
return cand_glyph
|
|
198
|
+
|
|
199
|
+
if target_glyph is not None and cand_name in {CONTRACTION, SILENCE}:
|
|
200
|
+
return target_glyph
|
|
201
|
+
|
|
202
|
+
history = tuple(
|
|
203
|
+
_structural_label(item) for item in nd.get("glyph_history", ())
|
|
204
|
+
)
|
|
205
|
+
cand_label = cand_name if cand_name is not None else _structural_label(cand)
|
|
206
|
+
order = (*history[-st["thol_len"] :], cand_label)
|
|
207
|
+
from ..operators import grammar as _grammar
|
|
208
|
+
|
|
209
|
+
raise _grammar.TholClosureError(
|
|
210
|
+
rule="thol-closure",
|
|
211
|
+
candidate=cand_label,
|
|
212
|
+
message=(
|
|
213
|
+
f"{operator_display_name(SELF_ORGANIZATION)} block requires {operator_display_name(target_name)} closure"
|
|
214
|
+
),
|
|
215
|
+
window=st["thol_len"],
|
|
216
|
+
threshold=close_dn,
|
|
217
|
+
order=order,
|
|
218
|
+
context={
|
|
219
|
+
"thol_min_len": minlen,
|
|
220
|
+
"thol_max_len": maxlen,
|
|
221
|
+
"si": si,
|
|
222
|
+
"si_high": si_high,
|
|
223
|
+
"required_closure": target_name,
|
|
224
|
+
},
|
|
225
|
+
)
|
|
226
|
+
return cand
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _check_compatibility(ctx: "GrammarContext", n, cand: Glyph | str) -> Glyph | str:
|
|
230
|
+
"""Verify canonical transition compatibility based on TNFR structural dynamics.
|
|
231
|
+
|
|
232
|
+
Note: Frequency-based validation (R5) has been removed as it was not a
|
|
233
|
+
fundamental physical constraint. Only C1-C3 constraints remain:
|
|
234
|
+
- C1: EXISTENCE & CLOSURE (valid start/end)
|
|
235
|
+
- C2: BOUNDEDNESS (stabilizers required)
|
|
236
|
+
- C3: THRESHOLD PHYSICS (bifurcations need context)
|
|
237
|
+
|
|
238
|
+
These are validated in grammar.py, not here. This function now simply
|
|
239
|
+
allows all transitions - validation happens at sequence level.
|
|
240
|
+
"""
|
|
241
|
+
# All transitions allowed - validation at sequence level via C1-C3
|
|
242
|
+
return cand
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@lru_cache(maxsize=1)
|
|
246
|
+
def _functional_translators():
|
|
247
|
+
from ..operators import grammar as _grammar
|
|
248
|
+
|
|
249
|
+
return _grammar.glyph_function_name, _grammar.function_name_to_glyph
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# NOTE: Compatibility tables deprecated - grammar rules now emerge naturally
|
|
253
|
+
# from TNFR structural dynamics (frequency transitions only)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping
|
|
4
|
+
from typing import Any, TypeVar
|
|
5
|
+
|
|
6
|
+
from typing import Any, Mapping, TypeVar
|
|
7
|
+
|
|
8
|
+
from ..types import Glyph, NodeId
|
|
9
|
+
from .grammar import GrammarContext
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
"coerce_glyph",
|
|
13
|
+
"glyph_fallback",
|
|
14
|
+
"get_norm",
|
|
15
|
+
"normalized_dnfr",
|
|
16
|
+
"_norm_attr",
|
|
17
|
+
"_si",
|
|
18
|
+
"_check_oz_to_zhir",
|
|
19
|
+
"_check_thol_closure",
|
|
20
|
+
"_check_compatibility",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_T = TypeVar("_T")
|
|
24
|
+
|
|
25
|
+
def coerce_glyph(val: _T) -> Glyph | _T: ...
|
|
26
|
+
def glyph_fallback(cand_key: str, fallbacks: Mapping[str, Any]) -> Glyph | str: ...
|
|
27
|
+
def get_norm(ctx: GrammarContext, key: str) -> float: ...
|
|
28
|
+
def _norm_attr(
|
|
29
|
+
ctx: GrammarContext, nd: Mapping[str, Any], attr_alias: str, norm_key: str
|
|
30
|
+
) -> float: ...
|
|
31
|
+
def _si(nd: Mapping[str, Any]) -> float: ...
|
|
32
|
+
def normalized_dnfr(ctx: GrammarContext, nd: Mapping[str, Any]) -> float: ...
|
|
33
|
+
def _check_oz_to_zhir(
|
|
34
|
+
ctx: GrammarContext, n: NodeId, cand: Glyph | str
|
|
35
|
+
) -> Glyph | str: ...
|
|
36
|
+
def _check_thol_closure(
|
|
37
|
+
ctx: GrammarContext,
|
|
38
|
+
n: NodeId,
|
|
39
|
+
cand: Glyph | str,
|
|
40
|
+
st: dict[str, Any],
|
|
41
|
+
) -> Glyph | str: ...
|
|
42
|
+
def _check_compatibility(
|
|
43
|
+
ctx: GrammarContext, n: NodeId, cand: Glyph | str
|
|
44
|
+
) -> Glyph | str: ...
|
|
@@ -0,0 +1,279 @@
|
|
|
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
|
+
_is_scalar,
|
|
29
|
+
)
|
|
30
|
+
from ..utils import clamp, ensure_collection
|
|
31
|
+
from . import ValidationOutcome, Validator
|
|
32
|
+
from .graph import run_validators
|
|
33
|
+
|
|
34
|
+
HistoryLog = MutableSequence[MutableMapping[str, object]]
|
|
35
|
+
"""Mutable history buffer storing clamp alerts for strict graphs."""
|
|
36
|
+
|
|
37
|
+
__all__ = (
|
|
38
|
+
"GraphCanonicalValidator",
|
|
39
|
+
"apply_canonical_clamps",
|
|
40
|
+
"validate_canon",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _max_bepi_magnitude(value: Any) -> float:
|
|
45
|
+
element = ensure_bepi(value)
|
|
46
|
+
mags = [
|
|
47
|
+
(
|
|
48
|
+
float(np.max(np.abs(element.f_continuous)))
|
|
49
|
+
if element.f_continuous.size
|
|
50
|
+
else 0.0
|
|
51
|
+
),
|
|
52
|
+
float(np.max(np.abs(element.a_discrete))) if element.a_discrete.size else 0.0,
|
|
53
|
+
]
|
|
54
|
+
return float(max(mags)) if mags else 0.0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _clamp_component(values: Any, lower: float, upper: float) -> np.ndarray:
|
|
58
|
+
array = np.asarray(values, dtype=np.complex128)
|
|
59
|
+
if array.size == 0:
|
|
60
|
+
return array
|
|
61
|
+
magnitudes = np.abs(array)
|
|
62
|
+
result = array.copy()
|
|
63
|
+
above = magnitudes > upper
|
|
64
|
+
if np.any(above):
|
|
65
|
+
result[above] = array[above] * (upper / magnitudes[above])
|
|
66
|
+
if lower > 0.0:
|
|
67
|
+
below = (magnitudes < lower) & (magnitudes > 0.0)
|
|
68
|
+
if np.any(below):
|
|
69
|
+
result[below] = array[below] * (lower / magnitudes[below])
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _clamp_bepi(value: Any, lower: float, upper: float) -> Any:
|
|
74
|
+
element = ensure_bepi(value)
|
|
75
|
+
clamped_cont = _clamp_component(element.f_continuous, lower, upper)
|
|
76
|
+
clamped_disc = _clamp_component(element.a_discrete, lower, upper)
|
|
77
|
+
return ensure_bepi(
|
|
78
|
+
{"continuous": clamped_cont, "discrete": clamped_disc, "grid": element.x_grid}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _log_clamp(
|
|
83
|
+
hist: HistoryLog,
|
|
84
|
+
node: NodeId | None,
|
|
85
|
+
attr: str,
|
|
86
|
+
value: float,
|
|
87
|
+
lo: float,
|
|
88
|
+
hi: float,
|
|
89
|
+
) -> None:
|
|
90
|
+
if value < lo or value > hi:
|
|
91
|
+
hist.append({"node": node, "attr": attr, "value": float(value)})
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def apply_canonical_clamps(
|
|
95
|
+
nd: MutableMapping[str, Any],
|
|
96
|
+
G: TNFRGraph | None = None,
|
|
97
|
+
node: NodeId | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Clamp nodal EPI, νf and θ according to canonical bounds."""
|
|
100
|
+
|
|
101
|
+
from ..dynamics.aliases import ALIAS_EPI, ALIAS_VF
|
|
102
|
+
|
|
103
|
+
if G is not None:
|
|
104
|
+
graph_dict = cast(MutableMapping[str, Any], G.graph)
|
|
105
|
+
graph_data: Mapping[str, Any] = graph_dict
|
|
106
|
+
else:
|
|
107
|
+
graph_dict = None
|
|
108
|
+
graph_data = DEFAULTS
|
|
109
|
+
eps_min = float(graph_data.get("EPI_MIN", DEFAULTS["EPI_MIN"]))
|
|
110
|
+
eps_max = float(graph_data.get("EPI_MAX", DEFAULTS["EPI_MAX"]))
|
|
111
|
+
vf_min = float(graph_data.get("VF_MIN", DEFAULTS["VF_MIN"]))
|
|
112
|
+
vf_max = float(graph_data.get("VF_MAX", DEFAULTS["VF_MAX"]))
|
|
113
|
+
theta_wrap = bool(graph_data.get("THETA_WRAP", DEFAULTS["THETA_WRAP"]))
|
|
114
|
+
|
|
115
|
+
raw_epi = get_attr(nd, ALIAS_EPI, ZERO_BEPI_STORAGE, conv=lambda obj: obj)
|
|
116
|
+
was_scalar = _is_scalar(raw_epi)
|
|
117
|
+
original_scalar_value = float(raw_epi) if was_scalar else None
|
|
118
|
+
epi = ensure_bepi(raw_epi)
|
|
119
|
+
vf = get_attr(nd, ALIAS_VF, 0.0)
|
|
120
|
+
th_val = get_theta_attr(nd, 0.0)
|
|
121
|
+
th = 0.0 if th_val is None else float(th_val)
|
|
122
|
+
|
|
123
|
+
strict = bool(
|
|
124
|
+
graph_data.get("VALIDATORS_STRICT", DEFAULTS.get("VALIDATORS_STRICT", False))
|
|
125
|
+
)
|
|
126
|
+
if strict and graph_dict is not None:
|
|
127
|
+
history = cast(MutableMapping[str, Any], graph_dict.setdefault("history", {}))
|
|
128
|
+
alerts = history.get("clamp_alerts")
|
|
129
|
+
if alerts is None:
|
|
130
|
+
hist = cast(HistoryLog, history.setdefault("clamp_alerts", []))
|
|
131
|
+
elif isinstance(alerts, MutableSequence):
|
|
132
|
+
hist = cast(HistoryLog, alerts)
|
|
133
|
+
else:
|
|
134
|
+
materialized = ensure_collection(alerts, max_materialize=None)
|
|
135
|
+
hist = cast(HistoryLog, list(materialized))
|
|
136
|
+
history["clamp_alerts"] = hist
|
|
137
|
+
epi_mag = _max_bepi_magnitude(epi)
|
|
138
|
+
_log_clamp(hist, node, "EPI", epi_mag, eps_min, eps_max)
|
|
139
|
+
_log_clamp(hist, node, "VF", float(vf), vf_min, vf_max)
|
|
140
|
+
|
|
141
|
+
clamped_epi = _clamp_bepi(epi, eps_min, eps_max)
|
|
142
|
+
if was_scalar:
|
|
143
|
+
clamped_value = float(clamp(original_scalar_value, eps_min, eps_max))
|
|
144
|
+
set_attr_generic(nd, ALIAS_EPI, clamped_value, conv=lambda obj: obj)
|
|
145
|
+
else:
|
|
146
|
+
set_attr_generic(
|
|
147
|
+
nd, ALIAS_EPI, serialize_bepi(clamped_epi), conv=lambda obj: obj
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
vf_val = float(clamp(vf, vf_min, vf_max))
|
|
151
|
+
if G is not None and node is not None:
|
|
152
|
+
set_vf(G, node, vf_val, update_max=False)
|
|
153
|
+
else:
|
|
154
|
+
set_attr(nd, ALIAS_VF, vf_val)
|
|
155
|
+
|
|
156
|
+
if theta_wrap:
|
|
157
|
+
new_th = (th + math.pi) % (2 * math.pi) - math.pi
|
|
158
|
+
if G is not None and node is not None:
|
|
159
|
+
set_theta(G, node, new_th)
|
|
160
|
+
else:
|
|
161
|
+
set_theta_attr(nd, new_th)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class GraphCanonicalValidator(Validator[TNFRGraph]):
|
|
165
|
+
"""Validator enforcing canonical runtime contracts on TNFR graphs."""
|
|
166
|
+
|
|
167
|
+
recompute_frequency_maxima: bool
|
|
168
|
+
enforce_graph_validators: bool
|
|
169
|
+
|
|
170
|
+
def __init__(
|
|
171
|
+
self,
|
|
172
|
+
*,
|
|
173
|
+
recompute_frequency_maxima: bool = True,
|
|
174
|
+
enforce_graph_validators: bool = True,
|
|
175
|
+
) -> None:
|
|
176
|
+
self.recompute_frequency_maxima = bool(recompute_frequency_maxima)
|
|
177
|
+
self.enforce_graph_validators = bool(enforce_graph_validators)
|
|
178
|
+
|
|
179
|
+
def _diff_after_clamp(
|
|
180
|
+
self,
|
|
181
|
+
before: Mapping[str, float],
|
|
182
|
+
after: Mapping[str, float],
|
|
183
|
+
) -> Mapping[str, Mapping[str, float]]:
|
|
184
|
+
changes: dict[str, Mapping[str, float]] = {}
|
|
185
|
+
for key, before_val in before.items():
|
|
186
|
+
after_val = after.get(key)
|
|
187
|
+
if after_val is None:
|
|
188
|
+
continue
|
|
189
|
+
if math.isclose(before_val, after_val, rel_tol=0.0, abs_tol=1e-12):
|
|
190
|
+
continue
|
|
191
|
+
changes[key] = {"before": before_val, "after": after_val}
|
|
192
|
+
return changes
|
|
193
|
+
|
|
194
|
+
def validate(self, subject: TNFRGraph, /, **_: Any) -> ValidationOutcome[TNFRGraph]:
|
|
195
|
+
"""Clamp nodal attributes, refresh νf maxima and run graph validators."""
|
|
196
|
+
|
|
197
|
+
from ..dynamics.aliases import ALIAS_EPI, ALIAS_VF
|
|
198
|
+
|
|
199
|
+
clamped_nodes: list[dict[str, Any]] = []
|
|
200
|
+
for node, data in subject.nodes(data=True):
|
|
201
|
+
mapping = cast(MutableMapping[str, Any], data)
|
|
202
|
+
before = {
|
|
203
|
+
"EPI": _max_bepi_magnitude(
|
|
204
|
+
get_attr(
|
|
205
|
+
mapping, ALIAS_EPI, ZERO_BEPI_STORAGE, conv=lambda obj: obj
|
|
206
|
+
)
|
|
207
|
+
),
|
|
208
|
+
"VF": float(get_attr(mapping, ALIAS_VF, 0.0)),
|
|
209
|
+
"THETA": float(get_theta_attr(mapping, 0.0) or 0.0),
|
|
210
|
+
}
|
|
211
|
+
apply_canonical_clamps(mapping, subject, cast(NodeId, node))
|
|
212
|
+
after = {
|
|
213
|
+
"EPI": _max_bepi_magnitude(
|
|
214
|
+
get_attr(
|
|
215
|
+
mapping, ALIAS_EPI, ZERO_BEPI_STORAGE, conv=lambda obj: obj
|
|
216
|
+
)
|
|
217
|
+
),
|
|
218
|
+
"VF": float(get_attr(mapping, ALIAS_VF, 0.0)),
|
|
219
|
+
"THETA": float(get_theta_attr(mapping, 0.0) or 0.0),
|
|
220
|
+
}
|
|
221
|
+
changes = self._diff_after_clamp(before, after)
|
|
222
|
+
if changes:
|
|
223
|
+
clamped_nodes.append({"node": node, "attributes": changes})
|
|
224
|
+
|
|
225
|
+
maxima: Mapping[str, float] = {}
|
|
226
|
+
if self.recompute_frequency_maxima:
|
|
227
|
+
maxima = multi_recompute_abs_max(subject, {"_vfmax": ALIAS_VF})
|
|
228
|
+
subject.graph.update(maxima)
|
|
229
|
+
|
|
230
|
+
errors: list[str] = []
|
|
231
|
+
if self.enforce_graph_validators:
|
|
232
|
+
try:
|
|
233
|
+
run_validators(subject)
|
|
234
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
235
|
+
errors.append(str(exc))
|
|
236
|
+
|
|
237
|
+
summary: dict[str, Any] = {
|
|
238
|
+
"clamped": tuple(clamped_nodes),
|
|
239
|
+
"maxima": dict(maxima),
|
|
240
|
+
}
|
|
241
|
+
if errors:
|
|
242
|
+
summary["errors"] = tuple(errors)
|
|
243
|
+
|
|
244
|
+
passed = not errors
|
|
245
|
+
artifacts: dict[str, Any] = {}
|
|
246
|
+
if clamped_nodes:
|
|
247
|
+
artifacts["clamped_nodes"] = clamped_nodes
|
|
248
|
+
if maxima:
|
|
249
|
+
artifacts["maxima"] = dict(maxima)
|
|
250
|
+
|
|
251
|
+
return ValidationOutcome(
|
|
252
|
+
subject=subject,
|
|
253
|
+
passed=passed,
|
|
254
|
+
summary=summary,
|
|
255
|
+
artifacts=artifacts or None,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def report(self, outcome: ValidationOutcome[TNFRGraph]) -> str:
|
|
259
|
+
"""Return a concise textual summary of ``outcome``."""
|
|
260
|
+
|
|
261
|
+
if outcome.passed:
|
|
262
|
+
clamped = outcome.summary.get("clamped")
|
|
263
|
+
if clamped:
|
|
264
|
+
return "Graph canonical validation applied clamps successfully."
|
|
265
|
+
return "Graph canonical validation passed without adjustments."
|
|
266
|
+
|
|
267
|
+
errors = outcome.summary.get("errors")
|
|
268
|
+
if not errors:
|
|
269
|
+
return "Graph canonical validation failed without diagnostic details."
|
|
270
|
+
if isinstance(errors, (list, tuple)):
|
|
271
|
+
return "Graph canonical validation errors: " + ", ".join(map(str, errors))
|
|
272
|
+
return f"Graph canonical validation error: {errors}"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def validate_canon(G: TNFRGraph) -> ValidationOutcome[TNFRGraph]:
|
|
276
|
+
"""Validate ``G`` using :class:`GraphCanonicalValidator`."""
|
|
277
|
+
|
|
278
|
+
validator = GraphCanonicalValidator()
|
|
279
|
+
return validator.validate(G)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import MutableMapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..types import NodeId, TNFRGraph
|
|
7
|
+
from . import ValidationOutcome, Validator
|
|
8
|
+
|
|
9
|
+
class GraphCanonicalValidator(Validator[TNFRGraph]):
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
*,
|
|
13
|
+
recompute_frequency_maxima: bool = ...,
|
|
14
|
+
enforce_graph_validators: bool = ...,
|
|
15
|
+
) -> None: ...
|
|
16
|
+
def validate(
|
|
17
|
+
self, subject: TNFRGraph, /, **kwargs: Any
|
|
18
|
+
) -> ValidationOutcome[TNFRGraph]: ...
|
|
19
|
+
def report(self, outcome: ValidationOutcome[TNFRGraph]) -> str: ...
|
|
20
|
+
|
|
21
|
+
def apply_canonical_clamps(
|
|
22
|
+
nd: MutableMapping[str, Any],
|
|
23
|
+
G: TNFRGraph | None = ...,
|
|
24
|
+
node: NodeId | None = ...,
|
|
25
|
+
) -> None: ...
|
|
26
|
+
def validate_canon(G: TNFRGraph) -> ValidationOutcome[TNFRGraph]: ...
|
|
27
|
+
|
|
28
|
+
__all__: tuple[str, ...]
|