tnfr 4.5.2__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 +334 -50
- 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 +214 -37
- 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 +149 -556
- tnfr/cache.pyi +13 -0
- tnfr/cli/__init__.py +51 -16
- tnfr/cli/__init__.pyi +26 -0
- tnfr/cli/arguments.py +344 -32
- tnfr/cli/arguments.pyi +29 -0
- tnfr/cli/execution.py +676 -50
- tnfr/cli/execution.pyi +70 -0
- tnfr/cli/interactive_validator.py +614 -0
- tnfr/cli/utils.py +18 -3
- 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/{constants_glyphs.py → config/constants.py} +26 -20
- tnfr/config/constants.pyi +12 -0
- tnfr/config/defaults.py +54 -0
- tnfr/{constants/core.py → config/defaults_core.py} +59 -6
- 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 +51 -133
- tnfr/constants/__init__.pyi +92 -0
- tnfr/constants/aliases.py +33 -0
- tnfr/constants/aliases.pyi +27 -0
- tnfr/constants/init.py +3 -1
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +9 -15
- 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 +213 -633
- 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 +2699 -398
- 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 +496 -102
- 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 +10 -5
- 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 +77 -55
- 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 +29 -50
- tnfr/flatten.pyi +21 -0
- tnfr/gamma.py +66 -53
- tnfr/gamma.pyi +36 -0
- tnfr/glyph_history.py +144 -57
- tnfr/glyph_history.pyi +35 -0
- tnfr/glyph_runtime.py +19 -0
- tnfr/glyph_runtime.pyi +8 -0
- tnfr/immutable.py +70 -30
- tnfr/immutable.pyi +36 -0
- tnfr/initialization.py +22 -16
- tnfr/initialization.pyi +65 -0
- tnfr/io.py +5 -241
- tnfr/io.pyi +13 -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 +47 -9
- 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 +1510 -330
- tnfr/metrics/coherence.pyi +129 -0
- tnfr/metrics/common.py +23 -16
- tnfr/metrics/common.pyi +35 -0
- tnfr/metrics/core.py +251 -36
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +709 -110
- tnfr/metrics/diagnosis.pyi +86 -0
- tnfr/metrics/emergence.py +245 -0
- tnfr/metrics/export.py +60 -18
- tnfr/metrics/export.pyi +7 -0
- tnfr/metrics/glyph_timing.py +233 -43
- 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 +63 -28
- tnfr/metrics/reporting.pyi +25 -0
- tnfr/metrics/sense_index.py +1126 -43
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +215 -23
- tnfr/metrics/trig.pyi +13 -0
- tnfr/metrics/trig_cache.py +148 -24
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/multiscale/__init__.py +32 -0
- tnfr/multiscale/hierarchical.py +517 -0
- tnfr/node.py +646 -140
- tnfr/node.pyi +139 -0
- tnfr/observers.py +160 -45
- tnfr/observers.pyi +31 -0
- tnfr/ontosim.py +23 -19
- tnfr/ontosim.pyi +28 -0
- tnfr/operators/__init__.py +1358 -106
- 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 +107 -38
- 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 +1415 -91
- 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 +75 -151
- 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 +59 -22
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +92 -67
- 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 +639 -263
- 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 +2 -4
- 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 +300 -126
- 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 +743 -12
- 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/{callback_utils.py → utils/callbacks.py} +26 -39
- 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/{helpers → utils}/numeric.py +51 -24
- 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-4.5.2.dist-info → tnfr-8.5.0.dist-info}/entry_points.txt +1 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/licenses/LICENSE.md +1 -1
- tnfr/collections_utils.py +0 -300
- tnfr/config.py +0 -32
- 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-8.5.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
tnfr/dynamics/runtime.py
ADDED
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
"""Runtime orchestration for TNFR dynamics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import sys
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
from collections import deque
|
|
9
|
+
from collections.abc import Iterable, Mapping, MutableMapping
|
|
10
|
+
from numbers import Real
|
|
11
|
+
from typing import Any, cast
|
|
12
|
+
|
|
13
|
+
from ..alias import get_attr
|
|
14
|
+
from ..utils import CallbackEvent, callback_manager
|
|
15
|
+
from ..constants import get_graph_param, get_param
|
|
16
|
+
from ..glyph_history import ensure_history
|
|
17
|
+
from ..metrics.sense_index import compute_Si
|
|
18
|
+
from ..operators import apply_remesh_if_globally_stable
|
|
19
|
+
from ..telemetry import publish_graph_cache_metrics
|
|
20
|
+
from ..types import HistoryState, NodeId, TNFRGraph
|
|
21
|
+
from ..utils import normalize_optional_int
|
|
22
|
+
from ..validation import apply_canonical_clamps
|
|
23
|
+
from . import adaptation, coordination, integrators, selectors
|
|
24
|
+
from .aliases import ALIAS_DNFR, ALIAS_EPI, ALIAS_SI, ALIAS_THETA, ALIAS_VF
|
|
25
|
+
|
|
26
|
+
try: # pragma: no cover - optional NumPy dependency
|
|
27
|
+
import numpy as np
|
|
28
|
+
except ImportError: # pragma: no cover - optional dependency missing
|
|
29
|
+
np = None # type: ignore[assignment]
|
|
30
|
+
|
|
31
|
+
try: # pragma: no cover - optional math extras
|
|
32
|
+
from ..mathematics.dynamics import MathematicalDynamicsEngine
|
|
33
|
+
from ..mathematics.projection import BasicStateProjector
|
|
34
|
+
from ..mathematics.runtime import (
|
|
35
|
+
coherence as runtime_coherence,
|
|
36
|
+
frequency_positive as runtime_frequency_positive,
|
|
37
|
+
normalized as runtime_normalized,
|
|
38
|
+
)
|
|
39
|
+
except Exception: # pragma: no cover - fallback when extras not available
|
|
40
|
+
MathematicalDynamicsEngine = None # type: ignore[assignment]
|
|
41
|
+
BasicStateProjector = None # type: ignore[assignment]
|
|
42
|
+
runtime_coherence = None # type: ignore[assignment]
|
|
43
|
+
runtime_frequency_positive = None # type: ignore[assignment]
|
|
44
|
+
runtime_normalized = None # type: ignore[assignment]
|
|
45
|
+
from .dnfr import default_compute_delta_nfr
|
|
46
|
+
from .sampling import update_node_sample as _update_node_sample
|
|
47
|
+
|
|
48
|
+
__all__ = (
|
|
49
|
+
"ALIAS_VF",
|
|
50
|
+
"ALIAS_DNFR",
|
|
51
|
+
"ALIAS_EPI",
|
|
52
|
+
"ALIAS_SI",
|
|
53
|
+
"_normalize_job_overrides",
|
|
54
|
+
"_resolve_jobs_override",
|
|
55
|
+
"_prepare_dnfr",
|
|
56
|
+
"_update_nodes",
|
|
57
|
+
"_update_epi_hist",
|
|
58
|
+
"_maybe_remesh",
|
|
59
|
+
"_run_validators",
|
|
60
|
+
"_run_before_callbacks",
|
|
61
|
+
"_run_after_callbacks",
|
|
62
|
+
"step",
|
|
63
|
+
"run",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _normalize_job_overrides(
|
|
68
|
+
job_overrides: Mapping[str, Any] | None,
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
"""Canonicalise job override keys for ΔNFR, νf and phase orchestration.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
job_overrides : Mapping[str, Any] | None
|
|
75
|
+
User-provided mapping whose keys may use legacy ``*_N_JOBS`` forms or
|
|
76
|
+
mixed casing. The values tune the parallel workloads that update ΔNFR,
|
|
77
|
+
νf adaptation and global phase coordination.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
dict[str, Any]
|
|
82
|
+
A dictionary where keys are upper-cased without the ``_N_JOBS`` suffix,
|
|
83
|
+
ready for downstream lookup in the runtime schedulers.
|
|
84
|
+
|
|
85
|
+
Notes
|
|
86
|
+
-----
|
|
87
|
+
``None`` keys are silently skipped to preserve resiliency when
|
|
88
|
+
orchestrating ΔNFR workers.
|
|
89
|
+
|
|
90
|
+
Examples
|
|
91
|
+
--------
|
|
92
|
+
>>> _normalize_job_overrides({"dnfr_n_jobs": 2, "vf_adapt": 4})
|
|
93
|
+
{'DNFR': 2, 'VF_ADAPT': 4}
|
|
94
|
+
>>> _normalize_job_overrides(None)
|
|
95
|
+
{}
|
|
96
|
+
"""
|
|
97
|
+
if not job_overrides:
|
|
98
|
+
return {}
|
|
99
|
+
|
|
100
|
+
normalized: dict[str, Any] = {}
|
|
101
|
+
for key, value in job_overrides.items():
|
|
102
|
+
if key is None:
|
|
103
|
+
continue
|
|
104
|
+
key_str = str(key).upper()
|
|
105
|
+
if key_str.endswith("_N_JOBS"):
|
|
106
|
+
key_str = key_str[: -len("_N_JOBS")]
|
|
107
|
+
normalized[key_str] = value
|
|
108
|
+
return normalized
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _resolve_jobs_override(
|
|
112
|
+
overrides: Mapping[str, Any],
|
|
113
|
+
key: str,
|
|
114
|
+
graph_value: Any,
|
|
115
|
+
*,
|
|
116
|
+
allow_non_positive: bool,
|
|
117
|
+
) -> int | None:
|
|
118
|
+
"""Resolve job overrides prioritising user hints over graph defaults.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
overrides : Mapping[str, Any]
|
|
123
|
+
Normalised overrides produced by :func:`_normalize_job_overrides` that
|
|
124
|
+
steer the ΔNFR computation, νf adaptation or phase coupling workers.
|
|
125
|
+
key : str
|
|
126
|
+
Logical subsystem key such as ``"DNFR"`` or ``"VF_ADAPT"``.
|
|
127
|
+
graph_value : Any
|
|
128
|
+
Baseline job count stored in the graph configuration.
|
|
129
|
+
allow_non_positive : bool
|
|
130
|
+
Propagated policy describing whether zero or negative values are valid
|
|
131
|
+
for the subsystem.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
int | None
|
|
136
|
+
Final job count that each scheduler will use, or ``None`` when no
|
|
137
|
+
explicit override or valid fallback exists.
|
|
138
|
+
|
|
139
|
+
Notes
|
|
140
|
+
-----
|
|
141
|
+
Preference resolution is pure and returns ``None`` instead of raising when
|
|
142
|
+
overrides cannot be coerced into valid integers.
|
|
143
|
+
|
|
144
|
+
Examples
|
|
145
|
+
--------
|
|
146
|
+
>>> overrides = _normalize_job_overrides({"phase": 0})
|
|
147
|
+
>>> _resolve_jobs_override(overrides, "phase", 2, allow_non_positive=True)
|
|
148
|
+
0
|
|
149
|
+
>>> _resolve_jobs_override({}, "vf_adapt", 4, allow_non_positive=False)
|
|
150
|
+
4
|
|
151
|
+
"""
|
|
152
|
+
norm_key = key.upper()
|
|
153
|
+
if overrides and norm_key in overrides:
|
|
154
|
+
return normalize_optional_int(
|
|
155
|
+
overrides.get(norm_key),
|
|
156
|
+
allow_non_positive=allow_non_positive,
|
|
157
|
+
strict=False,
|
|
158
|
+
sentinels=None,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return normalize_optional_int(
|
|
162
|
+
graph_value,
|
|
163
|
+
allow_non_positive=allow_non_positive,
|
|
164
|
+
strict=False,
|
|
165
|
+
sentinels=None,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
_INTEGRATOR_CACHE_KEY = "_integrator_cache"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _call_integrator_factory(factory: Any, G: TNFRGraph) -> Any:
|
|
173
|
+
"""Invoke an integrator factory respecting optional graph injection."""
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
signature = inspect.signature(factory)
|
|
177
|
+
except (TypeError, ValueError):
|
|
178
|
+
return factory()
|
|
179
|
+
|
|
180
|
+
params = list(signature.parameters.values())
|
|
181
|
+
required = [
|
|
182
|
+
p
|
|
183
|
+
for p in params
|
|
184
|
+
if p.default is inspect._empty
|
|
185
|
+
and p.kind
|
|
186
|
+
in (
|
|
187
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
188
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
189
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
190
|
+
)
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
if any(p.kind is inspect.Parameter.KEYWORD_ONLY for p in required):
|
|
194
|
+
raise TypeError(
|
|
195
|
+
"Integrator factory cannot require keyword-only arguments",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
positional_required = [
|
|
199
|
+
p
|
|
200
|
+
for p in required
|
|
201
|
+
if p.kind
|
|
202
|
+
in (
|
|
203
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
204
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
205
|
+
)
|
|
206
|
+
]
|
|
207
|
+
if len(positional_required) > 1:
|
|
208
|
+
raise TypeError(
|
|
209
|
+
"Integrator factory must accept at most one positional argument",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if positional_required:
|
|
213
|
+
return factory(G)
|
|
214
|
+
|
|
215
|
+
positional = [
|
|
216
|
+
p
|
|
217
|
+
for p in params
|
|
218
|
+
if p.kind
|
|
219
|
+
in (
|
|
220
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
221
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
222
|
+
)
|
|
223
|
+
]
|
|
224
|
+
if len(positional) == 1:
|
|
225
|
+
return factory(G)
|
|
226
|
+
elif len(positional) > 1:
|
|
227
|
+
raise TypeError(
|
|
228
|
+
"Integrator factory must accept at most one positional argument",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Check for any remaining required positional or keyword-only arguments
|
|
232
|
+
remaining_required = [
|
|
233
|
+
p
|
|
234
|
+
for p in params
|
|
235
|
+
if p.default is inspect._empty
|
|
236
|
+
and p.kind
|
|
237
|
+
in (
|
|
238
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
239
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
240
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
241
|
+
)
|
|
242
|
+
]
|
|
243
|
+
if remaining_required:
|
|
244
|
+
raise TypeError(
|
|
245
|
+
f"Integrator factory requires arguments: {', '.join(p.name for p in remaining_required)}"
|
|
246
|
+
)
|
|
247
|
+
return factory()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _resolve_integrator_instance(G: TNFRGraph) -> integrators.AbstractIntegrator:
|
|
251
|
+
"""Return an integrator instance configured on ``G`` or a default."""
|
|
252
|
+
|
|
253
|
+
cache_entry = G.graph.get(_INTEGRATOR_CACHE_KEY)
|
|
254
|
+
candidate = G.graph.get("integrator")
|
|
255
|
+
if (
|
|
256
|
+
isinstance(cache_entry, tuple)
|
|
257
|
+
and len(cache_entry) == 2
|
|
258
|
+
and cache_entry[0] is candidate
|
|
259
|
+
and isinstance(cache_entry[1], integrators.AbstractIntegrator)
|
|
260
|
+
):
|
|
261
|
+
return cache_entry[1]
|
|
262
|
+
|
|
263
|
+
if isinstance(candidate, integrators.AbstractIntegrator):
|
|
264
|
+
instance = candidate
|
|
265
|
+
elif inspect.isclass(candidate) and issubclass(
|
|
266
|
+
candidate, integrators.AbstractIntegrator
|
|
267
|
+
):
|
|
268
|
+
instance = candidate()
|
|
269
|
+
elif callable(candidate):
|
|
270
|
+
instance = cast(
|
|
271
|
+
integrators.AbstractIntegrator,
|
|
272
|
+
_call_integrator_factory(candidate, G),
|
|
273
|
+
)
|
|
274
|
+
elif candidate is None:
|
|
275
|
+
instance = integrators.DefaultIntegrator()
|
|
276
|
+
else:
|
|
277
|
+
raise TypeError(
|
|
278
|
+
"Graph integrator must be an AbstractIntegrator, subclass or callable",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if not isinstance(instance, integrators.AbstractIntegrator):
|
|
282
|
+
raise TypeError(
|
|
283
|
+
"Configured integrator must implement AbstractIntegrator.integrate",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
G.graph[_INTEGRATOR_CACHE_KEY] = (candidate, instance)
|
|
287
|
+
return instance
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _run_before_callbacks(
|
|
291
|
+
G: TNFRGraph,
|
|
292
|
+
*,
|
|
293
|
+
step_idx: int,
|
|
294
|
+
dt: float | None,
|
|
295
|
+
use_Si: bool,
|
|
296
|
+
apply_glyphs: bool,
|
|
297
|
+
) -> None:
|
|
298
|
+
"""Notify ``BEFORE_STEP`` observers with execution context."""
|
|
299
|
+
|
|
300
|
+
callback_manager.invoke_callbacks(
|
|
301
|
+
G,
|
|
302
|
+
CallbackEvent.BEFORE_STEP.value,
|
|
303
|
+
{
|
|
304
|
+
"step": step_idx,
|
|
305
|
+
"dt": dt,
|
|
306
|
+
"use_Si": use_Si,
|
|
307
|
+
"apply_glyphs": apply_glyphs,
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _prepare_dnfr(
|
|
313
|
+
G: TNFRGraph,
|
|
314
|
+
*,
|
|
315
|
+
use_Si: bool,
|
|
316
|
+
job_overrides: Mapping[str, Any] | None = None,
|
|
317
|
+
) -> None:
|
|
318
|
+
"""Recompute ΔNFR (and optionally Si) ahead of an integration step."""
|
|
319
|
+
|
|
320
|
+
compute_dnfr_cb = G.graph.get("compute_delta_nfr", default_compute_delta_nfr)
|
|
321
|
+
overrides = job_overrides or {}
|
|
322
|
+
n_jobs = _resolve_jobs_override(
|
|
323
|
+
overrides,
|
|
324
|
+
"DNFR",
|
|
325
|
+
G.graph.get("DNFR_N_JOBS"),
|
|
326
|
+
allow_non_positive=False,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
supports_n_jobs = False
|
|
330
|
+
try:
|
|
331
|
+
signature = inspect.signature(compute_dnfr_cb)
|
|
332
|
+
except (TypeError, ValueError):
|
|
333
|
+
signature = None
|
|
334
|
+
if signature is not None:
|
|
335
|
+
params = signature.parameters
|
|
336
|
+
if "n_jobs" in params:
|
|
337
|
+
kind = params["n_jobs"].kind
|
|
338
|
+
supports_n_jobs = kind in (
|
|
339
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
340
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
341
|
+
)
|
|
342
|
+
elif any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()):
|
|
343
|
+
supports_n_jobs = True
|
|
344
|
+
|
|
345
|
+
if supports_n_jobs:
|
|
346
|
+
compute_dnfr_cb(G, n_jobs=n_jobs)
|
|
347
|
+
else:
|
|
348
|
+
try:
|
|
349
|
+
compute_dnfr_cb(G, n_jobs=n_jobs)
|
|
350
|
+
except TypeError as exc:
|
|
351
|
+
if "n_jobs" in str(exc):
|
|
352
|
+
compute_dnfr_cb(G)
|
|
353
|
+
else:
|
|
354
|
+
raise
|
|
355
|
+
G.graph.pop("_sel_norms", None)
|
|
356
|
+
if use_Si:
|
|
357
|
+
si_jobs = _resolve_jobs_override(
|
|
358
|
+
overrides,
|
|
359
|
+
"SI",
|
|
360
|
+
G.graph.get("SI_N_JOBS"),
|
|
361
|
+
allow_non_positive=False,
|
|
362
|
+
)
|
|
363
|
+
dynamics_module = sys.modules.get("tnfr.dynamics")
|
|
364
|
+
compute_si_fn = (
|
|
365
|
+
getattr(dynamics_module, "compute_Si", None)
|
|
366
|
+
if dynamics_module is not None
|
|
367
|
+
else None
|
|
368
|
+
)
|
|
369
|
+
if compute_si_fn is None:
|
|
370
|
+
compute_si_fn = compute_Si
|
|
371
|
+
compute_si_fn(G, inplace=True, n_jobs=si_jobs)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _update_nodes(
|
|
375
|
+
G: TNFRGraph,
|
|
376
|
+
*,
|
|
377
|
+
dt: float | None,
|
|
378
|
+
use_Si: bool,
|
|
379
|
+
apply_glyphs: bool,
|
|
380
|
+
step_idx: int,
|
|
381
|
+
hist: HistoryState,
|
|
382
|
+
job_overrides: Mapping[str, Any] | None = None,
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Apply glyphs, integrate ΔNFR and refresh derived nodal state."""
|
|
385
|
+
|
|
386
|
+
_update_node_sample(G, step=step_idx)
|
|
387
|
+
overrides = job_overrides or {}
|
|
388
|
+
_prepare_dnfr(G, use_Si=use_Si, job_overrides=overrides)
|
|
389
|
+
selector = selectors._apply_selector(G)
|
|
390
|
+
if apply_glyphs:
|
|
391
|
+
selectors._apply_glyphs(G, selector, hist)
|
|
392
|
+
_dt = get_graph_param(G, "DT") if dt is None else float(dt)
|
|
393
|
+
method = get_graph_param(G, "INTEGRATOR_METHOD", str)
|
|
394
|
+
n_jobs = _resolve_jobs_override(
|
|
395
|
+
overrides,
|
|
396
|
+
"INTEGRATOR",
|
|
397
|
+
G.graph.get("INTEGRATOR_N_JOBS"),
|
|
398
|
+
allow_non_positive=True,
|
|
399
|
+
)
|
|
400
|
+
integrator = _resolve_integrator_instance(G)
|
|
401
|
+
integrator.integrate(
|
|
402
|
+
G,
|
|
403
|
+
dt=_dt,
|
|
404
|
+
t=None,
|
|
405
|
+
method=cast(str | None, method),
|
|
406
|
+
n_jobs=n_jobs,
|
|
407
|
+
)
|
|
408
|
+
for n, nd in G.nodes(data=True):
|
|
409
|
+
apply_canonical_clamps(cast(MutableMapping[str, Any], nd), G, cast(NodeId, n))
|
|
410
|
+
phase_jobs = _resolve_jobs_override(
|
|
411
|
+
overrides,
|
|
412
|
+
"PHASE",
|
|
413
|
+
G.graph.get("PHASE_N_JOBS"),
|
|
414
|
+
allow_non_positive=True,
|
|
415
|
+
)
|
|
416
|
+
coordination.coordinate_global_local_phase(G, None, None, n_jobs=phase_jobs)
|
|
417
|
+
vf_jobs = _resolve_jobs_override(
|
|
418
|
+
overrides,
|
|
419
|
+
"VF_ADAPT",
|
|
420
|
+
G.graph.get("VF_ADAPT_N_JOBS"),
|
|
421
|
+
allow_non_positive=False,
|
|
422
|
+
)
|
|
423
|
+
adaptation.adapt_vf_by_coherence(G, n_jobs=vf_jobs)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _update_epi_hist(G: TNFRGraph) -> None:
|
|
427
|
+
"""Maintain the rolling EPI history used by remeshing heuristics."""
|
|
428
|
+
|
|
429
|
+
tau_g = int(get_param(G, "REMESH_TAU_GLOBAL"))
|
|
430
|
+
tau_l = int(get_param(G, "REMESH_TAU_LOCAL"))
|
|
431
|
+
tau = max(tau_g, tau_l)
|
|
432
|
+
maxlen = max(2 * tau + 5, 64)
|
|
433
|
+
epi_hist = G.graph.get("_epi_hist")
|
|
434
|
+
if not isinstance(epi_hist, deque) or epi_hist.maxlen != maxlen:
|
|
435
|
+
epi_hist = deque(list(epi_hist or [])[-maxlen:], maxlen=maxlen)
|
|
436
|
+
G.graph["_epi_hist"] = epi_hist
|
|
437
|
+
epi_hist.append({n: get_attr(nd, ALIAS_EPI, 0.0) for n, nd in G.nodes(data=True)})
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _maybe_remesh(G: TNFRGraph) -> None:
|
|
441
|
+
"""Trigger remeshing when stability thresholds are satisfied."""
|
|
442
|
+
|
|
443
|
+
apply_remesh_if_globally_stable(G)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _run_validators(G: TNFRGraph) -> None:
|
|
447
|
+
"""Execute registered validators ensuring canonical invariants hold."""
|
|
448
|
+
|
|
449
|
+
from ..validation import run_validators
|
|
450
|
+
|
|
451
|
+
run_validators(G)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _run_after_callbacks(G, *, step_idx: int) -> None:
|
|
455
|
+
"""Notify ``AFTER_STEP`` observers with the latest structural metrics."""
|
|
456
|
+
|
|
457
|
+
h = ensure_history(G)
|
|
458
|
+
ctx = {"step": step_idx}
|
|
459
|
+
metric_pairs = [
|
|
460
|
+
("C", "C_steps"),
|
|
461
|
+
("stable_frac", "stable_frac"),
|
|
462
|
+
("phase_sync", "phase_sync"),
|
|
463
|
+
("glyph_disr", "glyph_load_disr"),
|
|
464
|
+
("Si_mean", "Si_mean"),
|
|
465
|
+
]
|
|
466
|
+
for dst, src in metric_pairs:
|
|
467
|
+
values = h.get(src)
|
|
468
|
+
if values:
|
|
469
|
+
ctx[dst] = values[-1]
|
|
470
|
+
callback_manager.invoke_callbacks(G, CallbackEvent.AFTER_STEP.value, ctx)
|
|
471
|
+
|
|
472
|
+
telemetry = G.graph.get("telemetry")
|
|
473
|
+
if isinstance(telemetry, MutableMapping):
|
|
474
|
+
history = telemetry.get("nu_f")
|
|
475
|
+
history_key = "nu_f_history"
|
|
476
|
+
if isinstance(history, list) and history_key not in telemetry:
|
|
477
|
+
telemetry[history_key] = history
|
|
478
|
+
payload = telemetry.get("nu_f_snapshot")
|
|
479
|
+
if isinstance(payload, Mapping):
|
|
480
|
+
bridge_raw = telemetry.get("nu_f_bridge")
|
|
481
|
+
try:
|
|
482
|
+
bridge = float(bridge_raw) if bridge_raw is not None else None
|
|
483
|
+
except (TypeError, ValueError):
|
|
484
|
+
bridge = None
|
|
485
|
+
nu_f_summary = {
|
|
486
|
+
"total_reorganisations": payload.get("total_reorganisations"),
|
|
487
|
+
"total_duration": payload.get("total_duration"),
|
|
488
|
+
"rate_hz_str": payload.get("rate_hz_str"),
|
|
489
|
+
"rate_hz": payload.get("rate_hz"),
|
|
490
|
+
"variance_hz_str": payload.get("variance_hz_str"),
|
|
491
|
+
"variance_hz": payload.get("variance_hz"),
|
|
492
|
+
"confidence_level": payload.get("confidence_level"),
|
|
493
|
+
"ci_hz_str": {
|
|
494
|
+
"lower": payload.get("ci_lower_hz_str"),
|
|
495
|
+
"upper": payload.get("ci_upper_hz_str"),
|
|
496
|
+
},
|
|
497
|
+
"ci_hz": {
|
|
498
|
+
"lower": payload.get("ci_lower_hz"),
|
|
499
|
+
"upper": payload.get("ci_upper_hz"),
|
|
500
|
+
},
|
|
501
|
+
"bridge": bridge,
|
|
502
|
+
}
|
|
503
|
+
telemetry["nu_f"] = nu_f_summary
|
|
504
|
+
math_summary = telemetry.get("math_engine")
|
|
505
|
+
if isinstance(math_summary, MutableMapping):
|
|
506
|
+
math_summary["nu_f"] = dict(nu_f_summary)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _get_math_engine_config(G: TNFRGraph) -> MutableMapping[str, Any] | None:
|
|
510
|
+
"""Return the mutable math-engine configuration stored on ``G``."""
|
|
511
|
+
|
|
512
|
+
cfg_raw = G.graph.get("MATH_ENGINE")
|
|
513
|
+
if not isinstance(cfg_raw, Mapping) or not cfg_raw.get("enabled", False):
|
|
514
|
+
return None
|
|
515
|
+
if isinstance(cfg_raw, MutableMapping):
|
|
516
|
+
return cfg_raw
|
|
517
|
+
cfg_mutable: MutableMapping[str, Any] = dict(cfg_raw)
|
|
518
|
+
G.graph["MATH_ENGINE"] = cfg_mutable
|
|
519
|
+
return cfg_mutable
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _initialise_math_state(
|
|
523
|
+
G: TNFRGraph,
|
|
524
|
+
cfg: MutableMapping[str, Any],
|
|
525
|
+
*,
|
|
526
|
+
hilbert_space: Any,
|
|
527
|
+
projector: BasicStateProjector,
|
|
528
|
+
) -> np.ndarray | None:
|
|
529
|
+
"""Project graph nodes into the Hilbert space to seed the math engine."""
|
|
530
|
+
|
|
531
|
+
dimension = getattr(hilbert_space, "dimension", None)
|
|
532
|
+
if dimension is None:
|
|
533
|
+
raise AttributeError("Hilbert space configuration is missing 'dimension'.")
|
|
534
|
+
|
|
535
|
+
vectors: list[np.ndarray] = []
|
|
536
|
+
for _, nd in G.nodes(data=True):
|
|
537
|
+
epi = float(get_attr(nd, ALIAS_EPI, 0.0) or 0.0)
|
|
538
|
+
nu_f = float(get_attr(nd, ALIAS_VF, 0.0) or 0.0)
|
|
539
|
+
theta_val = float(get_attr(nd, ALIAS_THETA, 0.0) or 0.0)
|
|
540
|
+
try:
|
|
541
|
+
vector = projector(epi=epi, nu_f=nu_f, theta=theta_val, dim=int(dimension))
|
|
542
|
+
except ValueError:
|
|
543
|
+
continue
|
|
544
|
+
vectors.append(np.asarray(vector, dtype=np.complex128))
|
|
545
|
+
|
|
546
|
+
if not vectors:
|
|
547
|
+
return None
|
|
548
|
+
|
|
549
|
+
stacked = np.vstack(vectors)
|
|
550
|
+
averaged = np.mean(stacked, axis=0)
|
|
551
|
+
atol = float(getattr(projector, "atol", 1e-9))
|
|
552
|
+
norm = float(getattr(hilbert_space, "norm")(averaged))
|
|
553
|
+
if np.isclose(norm, 0.0, atol=atol):
|
|
554
|
+
averaged = vectors[0]
|
|
555
|
+
norm = float(getattr(hilbert_space, "norm")(averaged))
|
|
556
|
+
if np.isclose(norm, 0.0, atol=atol):
|
|
557
|
+
return None
|
|
558
|
+
normalised = averaged / norm
|
|
559
|
+
cfg.setdefault("_state_origin", "projected")
|
|
560
|
+
return normalised
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _advance_math_engine(
|
|
564
|
+
G: TNFRGraph,
|
|
565
|
+
*,
|
|
566
|
+
dt: float,
|
|
567
|
+
step_idx: int,
|
|
568
|
+
hist: HistoryState,
|
|
569
|
+
) -> None:
|
|
570
|
+
"""Advance the optional math engine and record spectral telemetry."""
|
|
571
|
+
|
|
572
|
+
cfg = _get_math_engine_config(G)
|
|
573
|
+
if cfg is None:
|
|
574
|
+
return
|
|
575
|
+
|
|
576
|
+
if (
|
|
577
|
+
np is None
|
|
578
|
+
or MathematicalDynamicsEngine is None
|
|
579
|
+
or runtime_normalized is None
|
|
580
|
+
or runtime_coherence is None
|
|
581
|
+
):
|
|
582
|
+
raise RuntimeError(
|
|
583
|
+
"Mathematical dynamics require NumPy and tnfr.mathematics extras to be installed."
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
hilbert_space = cfg.get("hilbert_space")
|
|
587
|
+
coherence_operator = cfg.get("coherence_operator")
|
|
588
|
+
coherence_threshold = cfg.get("coherence_threshold")
|
|
589
|
+
if (
|
|
590
|
+
hilbert_space is None
|
|
591
|
+
or coherence_operator is None
|
|
592
|
+
or coherence_threshold is None
|
|
593
|
+
):
|
|
594
|
+
raise ValueError(
|
|
595
|
+
"MATH_ENGINE requires 'hilbert_space', 'coherence_operator' and "
|
|
596
|
+
"'coherence_threshold' entries."
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
if BasicStateProjector is None: # pragma: no cover - guarded by import above
|
|
600
|
+
raise RuntimeError(
|
|
601
|
+
"Mathematical dynamics require the BasicStateProjector helper."
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
projector = cfg.get("state_projector")
|
|
605
|
+
if not isinstance(projector, BasicStateProjector):
|
|
606
|
+
projector = BasicStateProjector()
|
|
607
|
+
cfg["state_projector"] = projector
|
|
608
|
+
|
|
609
|
+
engine = cfg.get("dynamics_engine")
|
|
610
|
+
if not isinstance(engine, MathematicalDynamicsEngine):
|
|
611
|
+
generator = cfg.get("generator_matrix")
|
|
612
|
+
if generator is None:
|
|
613
|
+
raise ValueError(
|
|
614
|
+
"MATH_ENGINE requires either a 'dynamics_engine' instance or a "
|
|
615
|
+
"'generator_matrix'."
|
|
616
|
+
)
|
|
617
|
+
generator_matrix = np.asarray(generator, dtype=np.complex128)
|
|
618
|
+
engine = MathematicalDynamicsEngine(
|
|
619
|
+
generator_matrix, hilbert_space=hilbert_space
|
|
620
|
+
)
|
|
621
|
+
cfg["dynamics_engine"] = engine
|
|
622
|
+
|
|
623
|
+
state_vector = cfg.get("_state_vector")
|
|
624
|
+
if state_vector is None:
|
|
625
|
+
state_vector = _initialise_math_state(
|
|
626
|
+
G,
|
|
627
|
+
cfg,
|
|
628
|
+
hilbert_space=hilbert_space,
|
|
629
|
+
projector=projector,
|
|
630
|
+
)
|
|
631
|
+
if state_vector is None:
|
|
632
|
+
return
|
|
633
|
+
else:
|
|
634
|
+
state_vector = np.asarray(state_vector, dtype=np.complex128)
|
|
635
|
+
dimension = getattr(hilbert_space, "dimension", state_vector.shape[0])
|
|
636
|
+
if state_vector.shape != (int(dimension),):
|
|
637
|
+
state_vector = _initialise_math_state(
|
|
638
|
+
G,
|
|
639
|
+
cfg,
|
|
640
|
+
hilbert_space=hilbert_space,
|
|
641
|
+
projector=projector,
|
|
642
|
+
)
|
|
643
|
+
if state_vector is None:
|
|
644
|
+
return
|
|
645
|
+
|
|
646
|
+
advanced = engine.step(state_vector, dt=float(dt), normalize=True)
|
|
647
|
+
cfg["_state_vector"] = advanced
|
|
648
|
+
|
|
649
|
+
atol = float(cfg.get("atol", getattr(engine, "atol", 1e-9)))
|
|
650
|
+
label = f"step[{step_idx}]"
|
|
651
|
+
|
|
652
|
+
normalized_passed, norm_value = runtime_normalized(
|
|
653
|
+
advanced,
|
|
654
|
+
hilbert_space,
|
|
655
|
+
atol=atol,
|
|
656
|
+
label=label,
|
|
657
|
+
)
|
|
658
|
+
coherence_passed, coherence_value = runtime_coherence(
|
|
659
|
+
advanced,
|
|
660
|
+
coherence_operator,
|
|
661
|
+
float(coherence_threshold),
|
|
662
|
+
normalise=False,
|
|
663
|
+
atol=atol,
|
|
664
|
+
label=label,
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
frequency_operator = cfg.get("frequency_operator")
|
|
668
|
+
frequency_summary: dict[str, Any] | None = None
|
|
669
|
+
if frequency_operator is not None:
|
|
670
|
+
if runtime_frequency_positive is None: # pragma: no cover - guarded above
|
|
671
|
+
raise RuntimeError(
|
|
672
|
+
"Frequency positivity checks require tnfr.mathematics extras."
|
|
673
|
+
)
|
|
674
|
+
freq_raw = runtime_frequency_positive(
|
|
675
|
+
advanced,
|
|
676
|
+
frequency_operator,
|
|
677
|
+
normalise=False,
|
|
678
|
+
enforce=True,
|
|
679
|
+
atol=atol,
|
|
680
|
+
label=label,
|
|
681
|
+
)
|
|
682
|
+
frequency_summary = {
|
|
683
|
+
"passed": bool(freq_raw.get("passed", False)),
|
|
684
|
+
"value": float(freq_raw.get("value", 0.0)),
|
|
685
|
+
"projection_passed": bool(freq_raw.get("projection_passed", False)),
|
|
686
|
+
"spectrum_psd": bool(freq_raw.get("spectrum_psd", False)),
|
|
687
|
+
"enforced": bool(freq_raw.get("enforce", True)),
|
|
688
|
+
}
|
|
689
|
+
if "spectrum_min" in freq_raw:
|
|
690
|
+
frequency_summary["spectrum_min"] = float(freq_raw.get("spectrum_min", 0.0))
|
|
691
|
+
|
|
692
|
+
summary = {
|
|
693
|
+
"step": step_idx,
|
|
694
|
+
"normalized": bool(normalized_passed),
|
|
695
|
+
"norm": float(norm_value),
|
|
696
|
+
"coherence": {
|
|
697
|
+
"passed": bool(coherence_passed),
|
|
698
|
+
"value": float(coherence_value),
|
|
699
|
+
"threshold": float(coherence_threshold),
|
|
700
|
+
},
|
|
701
|
+
"frequency": frequency_summary,
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
hist.setdefault("math_engine_summary", []).append(summary)
|
|
705
|
+
hist.setdefault("math_engine_norm", []).append(summary["norm"])
|
|
706
|
+
hist.setdefault("math_engine_normalized", []).append(summary["normalized"])
|
|
707
|
+
hist.setdefault("math_engine_coherence", []).append(summary["coherence"]["value"])
|
|
708
|
+
hist.setdefault("math_engine_coherence_passed", []).append(
|
|
709
|
+
summary["coherence"]["passed"]
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
if frequency_summary is None:
|
|
713
|
+
hist.setdefault("math_engine_frequency", []).append(None)
|
|
714
|
+
hist.setdefault("math_engine_frequency_passed", []).append(None)
|
|
715
|
+
hist.setdefault("math_engine_frequency_projection_passed", []).append(None)
|
|
716
|
+
else:
|
|
717
|
+
hist.setdefault("math_engine_frequency", []).append(frequency_summary["value"])
|
|
718
|
+
hist.setdefault("math_engine_frequency_passed", []).append(
|
|
719
|
+
frequency_summary["passed"]
|
|
720
|
+
)
|
|
721
|
+
hist.setdefault("math_engine_frequency_projection_passed", []).append(
|
|
722
|
+
frequency_summary["projection_passed"]
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
cfg["last_summary"] = summary
|
|
726
|
+
telemetry = G.graph.setdefault("telemetry", {})
|
|
727
|
+
telemetry["math_engine"] = deepcopy(summary)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def step(
|
|
731
|
+
G: TNFRGraph,
|
|
732
|
+
*,
|
|
733
|
+
dt: float | None = None,
|
|
734
|
+
use_Si: bool = True,
|
|
735
|
+
apply_glyphs: bool = True,
|
|
736
|
+
n_jobs: Mapping[str, Any] | None = None,
|
|
737
|
+
) -> None:
|
|
738
|
+
"""Advance the runtime one ΔNFR step updating νf, phase and glyphs.
|
|
739
|
+
|
|
740
|
+
Parameters
|
|
741
|
+
----------
|
|
742
|
+
G : TNFRGraph
|
|
743
|
+
Graph whose nodes store EPI, νf and phase metadata. The graph must
|
|
744
|
+
expose a ΔNFR hook under ``G.graph['compute_delta_nfr']`` and optional
|
|
745
|
+
selector or callback registrations.
|
|
746
|
+
dt : float | None, optional
|
|
747
|
+
Time increment injected into the integrator. ``None`` falls back to the
|
|
748
|
+
``DT`` attribute stored in ``G.graph`` which keeps ΔNFR integration
|
|
749
|
+
aligned with the nodal equation.
|
|
750
|
+
use_Si : bool, default True
|
|
751
|
+
When ``True`` the Sense Index (Si) is recomputed to modulate ΔNFR and
|
|
752
|
+
νf adaptation heuristics.
|
|
753
|
+
apply_glyphs : bool, default True
|
|
754
|
+
Enables canonical glyph selection so that phase and coherence glyphs
|
|
755
|
+
continue to modulate ΔNFR.
|
|
756
|
+
n_jobs : Mapping[str, Any] | None, optional
|
|
757
|
+
Optional overrides that tune the parallel workers used for ΔNFR, phase
|
|
758
|
+
coordination and νf adaptation. The mapping is processed by
|
|
759
|
+
:func:`_normalize_job_overrides`.
|
|
760
|
+
|
|
761
|
+
Returns
|
|
762
|
+
-------
|
|
763
|
+
None
|
|
764
|
+
Mutates ``G`` in place by recomputing ΔNFR, νf and phase metrics.
|
|
765
|
+
|
|
766
|
+
Notes
|
|
767
|
+
-----
|
|
768
|
+
Registered callbacks execute within :func:`step` and any exceptions they
|
|
769
|
+
raise propagate according to the callback manager configuration.
|
|
770
|
+
|
|
771
|
+
Examples
|
|
772
|
+
--------
|
|
773
|
+
Register a hook that records phase synchrony while using the parametric
|
|
774
|
+
selector to choose glyphs before advancing one runtime step.
|
|
775
|
+
|
|
776
|
+
>>> from tnfr.utils import CallbackEvent, callback_manager
|
|
777
|
+
>>> from tnfr.dynamics import selectors
|
|
778
|
+
>>> from tnfr.dynamics.runtime import ALIAS_VF
|
|
779
|
+
>>> from tnfr.structural import create_nfr
|
|
780
|
+
>>> G, node = create_nfr("seed", epi=0.2, vf=1.5)
|
|
781
|
+
>>> callback_manager.register_callback(
|
|
782
|
+
... G,
|
|
783
|
+
... CallbackEvent.AFTER_STEP,
|
|
784
|
+
... lambda graph, ctx: graph.graph.setdefault("phase_log", []).append(ctx.get("phase_sync")),
|
|
785
|
+
... )
|
|
786
|
+
>>> G.graph["glyph_selector"] = selectors.ParametricGlyphSelector()
|
|
787
|
+
>>> step(G, dt=0.05, n_jobs={"dnfr_n_jobs": 1})
|
|
788
|
+
>>> ALIAS_VF in G.nodes[node]
|
|
789
|
+
True
|
|
790
|
+
"""
|
|
791
|
+
job_overrides = _normalize_job_overrides(n_jobs)
|
|
792
|
+
hist = ensure_history(G)
|
|
793
|
+
step_idx = len(hist.setdefault("C_steps", []))
|
|
794
|
+
_run_before_callbacks(
|
|
795
|
+
G, step_idx=step_idx, dt=dt, use_Si=use_Si, apply_glyphs=apply_glyphs
|
|
796
|
+
)
|
|
797
|
+
_update_nodes(
|
|
798
|
+
G,
|
|
799
|
+
dt=dt,
|
|
800
|
+
use_Si=use_Si,
|
|
801
|
+
apply_glyphs=apply_glyphs,
|
|
802
|
+
step_idx=step_idx,
|
|
803
|
+
hist=hist,
|
|
804
|
+
job_overrides=job_overrides,
|
|
805
|
+
)
|
|
806
|
+
resolved_dt = get_graph_param(G, "DT") if dt is None else float(dt)
|
|
807
|
+
_advance_math_engine(
|
|
808
|
+
G,
|
|
809
|
+
dt=resolved_dt,
|
|
810
|
+
step_idx=step_idx,
|
|
811
|
+
hist=hist,
|
|
812
|
+
)
|
|
813
|
+
_update_epi_hist(G)
|
|
814
|
+
_maybe_remesh(G)
|
|
815
|
+
_run_validators(G)
|
|
816
|
+
_run_after_callbacks(G, step_idx=step_idx)
|
|
817
|
+
publish_graph_cache_metrics(G)
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def run(
|
|
821
|
+
G: TNFRGraph,
|
|
822
|
+
steps: int,
|
|
823
|
+
*,
|
|
824
|
+
dt: float | None = None,
|
|
825
|
+
use_Si: bool = True,
|
|
826
|
+
apply_glyphs: bool = True,
|
|
827
|
+
n_jobs: Mapping[str, Any] | None = None,
|
|
828
|
+
) -> None:
|
|
829
|
+
"""Iterate :func:`step` to evolve ΔNFR, νf and phase trajectories.
|
|
830
|
+
|
|
831
|
+
Parameters
|
|
832
|
+
----------
|
|
833
|
+
G : TNFRGraph
|
|
834
|
+
Graph that stores the coherent structures. Callbacks and selectors
|
|
835
|
+
configured on ``G.graph`` orchestrate glyph application and telemetry.
|
|
836
|
+
steps : int
|
|
837
|
+
Number of times :func:`step` is invoked. Each iteration integrates ΔNFR
|
|
838
|
+
and νf according to ``dt`` and the configured selector.
|
|
839
|
+
dt : float | None, optional
|
|
840
|
+
Time increment for each step. ``None`` uses the graph's default ``DT``.
|
|
841
|
+
use_Si : bool, default True
|
|
842
|
+
Recompute the Sense Index during each iteration to keep ΔNFR feedback
|
|
843
|
+
loops tied to νf adjustments.
|
|
844
|
+
apply_glyphs : bool, default True
|
|
845
|
+
Enables glyph selection and application per step.
|
|
846
|
+
n_jobs : Mapping[str, Any] | None, optional
|
|
847
|
+
Shared overrides forwarded to each :func:`step` call.
|
|
848
|
+
|
|
849
|
+
Returns
|
|
850
|
+
-------
|
|
851
|
+
None
|
|
852
|
+
The graph ``G`` is updated in place.
|
|
853
|
+
|
|
854
|
+
Raises
|
|
855
|
+
------
|
|
856
|
+
ValueError
|
|
857
|
+
Raised when ``steps`` is negative because the runtime cannot evolve a
|
|
858
|
+
negative number of ΔNFR updates.
|
|
859
|
+
|
|
860
|
+
Examples
|
|
861
|
+
--------
|
|
862
|
+
Install a before-step callback and use the default glyph selector while
|
|
863
|
+
running two iterations that synchronise phase and νf.
|
|
864
|
+
|
|
865
|
+
>>> from tnfr.utils import CallbackEvent, callback_manager
|
|
866
|
+
>>> from tnfr.dynamics import selectors
|
|
867
|
+
>>> from tnfr.structural import create_nfr
|
|
868
|
+
>>> G, node = create_nfr("seed", epi=0.3, vf=1.2)
|
|
869
|
+
>>> callback_manager.register_callback(
|
|
870
|
+
... G,
|
|
871
|
+
... CallbackEvent.BEFORE_STEP,
|
|
872
|
+
... lambda graph, ctx: graph.graph.setdefault("dt_trace", []).append(ctx["dt"]),
|
|
873
|
+
... )
|
|
874
|
+
>>> G.graph["glyph_selector"] = selectors.default_glyph_selector
|
|
875
|
+
>>> run(G, 2, dt=0.1)
|
|
876
|
+
>>> len(G.graph["dt_trace"])
|
|
877
|
+
2
|
|
878
|
+
"""
|
|
879
|
+
steps_int = int(steps)
|
|
880
|
+
if steps_int < 0:
|
|
881
|
+
raise ValueError("'steps' must be non-negative")
|
|
882
|
+
stop_cfg = get_graph_param(G, "STOP_EARLY", dict)
|
|
883
|
+
stop_enabled = False
|
|
884
|
+
if stop_cfg and stop_cfg.get("enabled", False):
|
|
885
|
+
w = max(1, int(stop_cfg.get("window", 25)))
|
|
886
|
+
frac = float(stop_cfg.get("fraction", 0.90))
|
|
887
|
+
stop_enabled = True
|
|
888
|
+
job_overrides = _normalize_job_overrides(n_jobs)
|
|
889
|
+
for _ in range(steps_int):
|
|
890
|
+
step(
|
|
891
|
+
G,
|
|
892
|
+
dt=dt,
|
|
893
|
+
use_Si=use_Si,
|
|
894
|
+
apply_glyphs=apply_glyphs,
|
|
895
|
+
n_jobs=job_overrides,
|
|
896
|
+
)
|
|
897
|
+
if stop_enabled:
|
|
898
|
+
history = ensure_history(G)
|
|
899
|
+
raw_series = dict.get(history, "stable_frac", [])
|
|
900
|
+
if not isinstance(raw_series, Iterable):
|
|
901
|
+
series = []
|
|
902
|
+
elif isinstance(raw_series, list):
|
|
903
|
+
series = raw_series
|
|
904
|
+
else:
|
|
905
|
+
series = list(raw_series)
|
|
906
|
+
numeric_series = [v for v in series if isinstance(v, Real)]
|
|
907
|
+
if len(numeric_series) >= w and all(v >= frac for v in numeric_series[-w:]):
|
|
908
|
+
break
|