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/operators/__init__.py
CHANGED
|
@@ -1,39 +1,84 @@
|
|
|
1
|
-
"""Network operators.
|
|
1
|
+
"""Network operators.
|
|
2
|
+
|
|
3
|
+
Operator helpers interact with TNFR graphs adhering to
|
|
4
|
+
:class:`tnfr.types.GraphLike`, relying on ``nodes``/``neighbors`` views,
|
|
5
|
+
``number_of_nodes`` and the graph-level ``.graph`` metadata when applying
|
|
6
|
+
structural transformations.
|
|
7
|
+
"""
|
|
2
8
|
|
|
3
9
|
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import math
|
|
10
|
+
|
|
6
11
|
import heapq
|
|
12
|
+
import math
|
|
13
|
+
from collections.abc import Callable, Iterator
|
|
7
14
|
from itertools import islice
|
|
8
|
-
from statistics import
|
|
15
|
+
from statistics import StatisticsError, fmean
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
9
17
|
|
|
10
|
-
from
|
|
11
|
-
from ..constants import DEFAULTS, get_aliases, get_param
|
|
18
|
+
from tnfr import glyph_history
|
|
12
19
|
|
|
13
|
-
from ..
|
|
20
|
+
from ..alias import get_attr
|
|
21
|
+
from ..constants import DEFAULTS, get_param
|
|
22
|
+
from ..constants.aliases import ALIAS_EPI, ALIAS_VF
|
|
23
|
+
from ..utils import angle_diff
|
|
14
24
|
from ..metrics.trig import neighbor_phase_mean
|
|
15
|
-
from ..import_utils import get_nodonx
|
|
16
25
|
from ..rng import make_rng
|
|
17
|
-
from
|
|
18
|
-
from ..
|
|
19
|
-
|
|
26
|
+
from ..types import EPIValue, Glyph, NodeId, TNFRGraph
|
|
27
|
+
from ..utils import get_nodenx
|
|
28
|
+
from . import definitions as _definitions
|
|
20
29
|
from .jitter import (
|
|
21
30
|
JitterCache,
|
|
22
31
|
JitterCacheManager,
|
|
23
32
|
get_jitter_manager,
|
|
24
|
-
reset_jitter_manager,
|
|
25
33
|
random_jitter,
|
|
34
|
+
reset_jitter_manager,
|
|
26
35
|
)
|
|
36
|
+
from .registry import OPERATORS, discover_operators, get_operator_class
|
|
27
37
|
from .remesh import (
|
|
28
38
|
apply_network_remesh,
|
|
29
|
-
apply_topological_remesh,
|
|
30
39
|
apply_remesh_if_globally_stable,
|
|
40
|
+
apply_topological_remesh,
|
|
31
41
|
)
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
_remesh_doc = (
|
|
44
|
+
"Trigger a remesh once the stability window is satisfied.\n\n"
|
|
45
|
+
"Parameters\n----------\n"
|
|
46
|
+
"stable_step_window : int | None\n"
|
|
47
|
+
" Number of consecutive stable steps required before remeshing.\n"
|
|
48
|
+
" Only the English keyword 'stable_step_window' is supported."
|
|
49
|
+
)
|
|
50
|
+
if apply_remesh_if_globally_stable.__doc__:
|
|
51
|
+
apply_remesh_if_globally_stable.__doc__ += "\n\n" + _remesh_doc
|
|
52
|
+
else:
|
|
53
|
+
apply_remesh_if_globally_stable.__doc__ = _remesh_doc
|
|
54
|
+
|
|
55
|
+
discover_operators()
|
|
56
|
+
|
|
57
|
+
_DEFINITION_EXPORTS = {
|
|
58
|
+
name: getattr(_definitions, name) for name in getattr(_definitions, "__all__", ())
|
|
59
|
+
}
|
|
60
|
+
globals().update(_DEFINITION_EXPORTS)
|
|
35
61
|
|
|
36
|
-
|
|
62
|
+
if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
63
|
+
from ..node import NodeProtocol
|
|
64
|
+
|
|
65
|
+
GlyphFactors = dict[str, Any]
|
|
66
|
+
GlyphOperation = Callable[["NodeProtocol", GlyphFactors], None]
|
|
67
|
+
|
|
68
|
+
from .grammar import apply_glyph_with_grammar # noqa: E402
|
|
69
|
+
from .health_analyzer import SequenceHealthAnalyzer, SequenceHealthMetrics # noqa: E402
|
|
70
|
+
from .hamiltonian import (
|
|
71
|
+
InternalHamiltonian,
|
|
72
|
+
build_H_coherence,
|
|
73
|
+
build_H_frequency,
|
|
74
|
+
build_H_coupling,
|
|
75
|
+
) # noqa: E402
|
|
76
|
+
from .pattern_detection import ( # noqa: E402
|
|
77
|
+
PatternMatch,
|
|
78
|
+
UnifiedPatternDetector,
|
|
79
|
+
detect_pattern,
|
|
80
|
+
analyze_sequence,
|
|
81
|
+
)
|
|
37
82
|
|
|
38
83
|
__all__ = [
|
|
39
84
|
"JitterCache",
|
|
@@ -46,29 +91,153 @@ __all__ = [
|
|
|
46
91
|
"GLYPH_OPERATIONS",
|
|
47
92
|
"apply_glyph_obj",
|
|
48
93
|
"apply_glyph",
|
|
94
|
+
"apply_glyph_with_grammar",
|
|
49
95
|
"apply_network_remesh",
|
|
50
96
|
"apply_topological_remesh",
|
|
51
97
|
"apply_remesh_if_globally_stable",
|
|
98
|
+
"OPERATORS",
|
|
99
|
+
"discover_operators",
|
|
100
|
+
"get_operator_class",
|
|
101
|
+
"SequenceHealthMetrics",
|
|
102
|
+
"SequenceHealthAnalyzer",
|
|
103
|
+
"InternalHamiltonian",
|
|
104
|
+
"build_H_coherence",
|
|
105
|
+
"build_H_frequency",
|
|
106
|
+
"build_H_coupling",
|
|
107
|
+
# Pattern detection (unified module)
|
|
108
|
+
"PatternMatch",
|
|
109
|
+
"UnifiedPatternDetector",
|
|
110
|
+
"detect_pattern",
|
|
111
|
+
"analyze_sequence",
|
|
52
112
|
]
|
|
53
113
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
114
|
+
__all__.extend(_DEFINITION_EXPORTS.keys())
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_glyph_factors(node: NodeProtocol) -> GlyphFactors:
|
|
118
|
+
"""Fetch glyph tuning factors for a node.
|
|
119
|
+
|
|
120
|
+
The glyph factors expose per-operator coefficients that modulate how an
|
|
121
|
+
operator reorganizes a node's Primary Information Structure (EPI),
|
|
122
|
+
structural frequency (νf), internal reorganization differential (ΔNFR), and
|
|
123
|
+
phase. Missing factors fall back to the canonical defaults stored at the
|
|
124
|
+
graph level.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
node : NodeProtocol
|
|
129
|
+
TNFR node providing a ``graph`` mapping where glyph factors may be
|
|
130
|
+
cached under ``"GLYPH_FACTORS"``.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
GlyphFactors
|
|
135
|
+
Mapping with operator-specific coefficients merged with the canonical
|
|
136
|
+
defaults. Mutating the returned mapping does not affect the graph.
|
|
137
|
+
|
|
138
|
+
Examples
|
|
139
|
+
--------
|
|
140
|
+
>>> class MockNode:
|
|
141
|
+
... def __init__(self):
|
|
142
|
+
... self.graph = {"GLYPH_FACTORS": {"AL_boost": 0.2}}
|
|
143
|
+
>>> node = MockNode()
|
|
144
|
+
>>> factors = get_glyph_factors(node)
|
|
145
|
+
>>> factors["AL_boost"]
|
|
146
|
+
0.2
|
|
147
|
+
>>> factors["EN_mix"] # Fallback to the default reception mix
|
|
148
|
+
0.25
|
|
149
|
+
"""
|
|
57
150
|
return node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"].copy())
|
|
58
151
|
|
|
59
152
|
|
|
60
|
-
def get_factor(gf:
|
|
61
|
-
"""Return
|
|
62
|
-
|
|
153
|
+
def get_factor(gf: GlyphFactors, key: str, default: float) -> float:
|
|
154
|
+
"""Return a glyph factor as ``float`` with a default fallback.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
gf : GlyphFactors
|
|
159
|
+
Mapping of glyph names to numeric factors.
|
|
160
|
+
key : str
|
|
161
|
+
Factor identifier to look up.
|
|
162
|
+
default : float
|
|
163
|
+
Value used when ``key`` is absent. This typically corresponds to the
|
|
164
|
+
canonical operator tuning and protects structural invariants.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
float
|
|
169
|
+
The resolved factor converted to ``float``.
|
|
170
|
+
|
|
171
|
+
Notes
|
|
172
|
+
-----
|
|
173
|
+
This function performs defensive validation to ensure numeric safety.
|
|
174
|
+
Invalid values (non-numeric, nan, inf) are silently replaced with the
|
|
175
|
+
default to prevent operator failures. For strict validation, use
|
|
176
|
+
``validate_glyph_factors`` before passing factors to operators.
|
|
177
|
+
|
|
178
|
+
Examples
|
|
179
|
+
--------
|
|
180
|
+
>>> get_factor({"AL_boost": 0.3}, "AL_boost", 0.05)
|
|
181
|
+
0.3
|
|
182
|
+
>>> get_factor({}, "IL_dnfr_factor", 0.7)
|
|
183
|
+
0.7
|
|
184
|
+
"""
|
|
185
|
+
value = gf.get(key, default)
|
|
186
|
+
# Defensive validation: ensure the value is numeric and finite
|
|
187
|
+
# Use default for invalid values to prevent operator failures
|
|
188
|
+
if not isinstance(value, (int, float, str)):
|
|
189
|
+
return default
|
|
190
|
+
try:
|
|
191
|
+
value = float(value)
|
|
192
|
+
except (ValueError, TypeError):
|
|
193
|
+
return default
|
|
194
|
+
if not math.isfinite(value):
|
|
195
|
+
return default
|
|
196
|
+
return value
|
|
63
197
|
|
|
64
198
|
|
|
65
199
|
# -------------------------
|
|
66
|
-
# Glyphs (
|
|
200
|
+
# Glyphs (local operators)
|
|
67
201
|
# -------------------------
|
|
68
202
|
|
|
69
203
|
|
|
70
|
-
def get_neighbor_epi(node:
|
|
71
|
-
"""
|
|
204
|
+
def get_neighbor_epi(node: NodeProtocol) -> tuple[list[NodeProtocol], EPIValue]:
|
|
205
|
+
"""Collect neighbour nodes and their mean EPI.
|
|
206
|
+
|
|
207
|
+
The neighbour EPI is used by reception-like glyphs (e.g., EN, RA) to
|
|
208
|
+
harmonise the node's EPI with the surrounding field without mutating νf,
|
|
209
|
+
ΔNFR, or phase. When a neighbour lacks a direct ``EPI`` attribute the
|
|
210
|
+
function resolves it from NetworkX metadata using known aliases.
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
node : NodeProtocol
|
|
215
|
+
Node whose neighbours participate in the averaging.
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
list of NodeProtocol
|
|
220
|
+
Concrete neighbour objects that expose TNFR attributes.
|
|
221
|
+
EPIValue
|
|
222
|
+
Arithmetic mean of the neighbouring EPIs. Equals the node EPI when no
|
|
223
|
+
valid neighbours are found, allowing glyphs to preserve the node state.
|
|
224
|
+
|
|
225
|
+
Examples
|
|
226
|
+
--------
|
|
227
|
+
>>> class MockNode:
|
|
228
|
+
... def __init__(self, epi, neighbors):
|
|
229
|
+
... self.EPI = epi
|
|
230
|
+
... self._neighbors = neighbors
|
|
231
|
+
... self.graph = {}
|
|
232
|
+
... def neighbors(self):
|
|
233
|
+
... return self._neighbors
|
|
234
|
+
>>> neigh_a = MockNode(1.0, [])
|
|
235
|
+
>>> neigh_b = MockNode(2.0, [])
|
|
236
|
+
>>> node = MockNode(0.5, [neigh_a, neigh_b])
|
|
237
|
+
>>> neighbors, epi_bar = get_neighbor_epi(node)
|
|
238
|
+
>>> len(neighbors), round(epi_bar, 2)
|
|
239
|
+
(2, 1.5)
|
|
240
|
+
"""
|
|
72
241
|
|
|
73
242
|
epi = node.EPI
|
|
74
243
|
neigh = list(node.neighbors())
|
|
@@ -98,12 +267,11 @@ def get_neighbor_epi(node: NodoProtocol) -> tuple[list[NodoProtocol], float]:
|
|
|
98
267
|
return [], epi
|
|
99
268
|
epi_bar = total / count if count else float(epi)
|
|
100
269
|
if needs_conversion:
|
|
101
|
-
|
|
102
|
-
if
|
|
103
|
-
raise ImportError("
|
|
270
|
+
NodeNX = get_nodenx()
|
|
271
|
+
if NodeNX is None:
|
|
272
|
+
raise ImportError("NodeNX is unavailable")
|
|
104
273
|
neigh = [
|
|
105
|
-
v if hasattr(v, "EPI") else
|
|
106
|
-
for v in neigh
|
|
274
|
+
v if hasattr(v, "EPI") else NodeNX.from_graph(node.G, v) for v in neigh
|
|
107
275
|
]
|
|
108
276
|
else:
|
|
109
277
|
try:
|
|
@@ -115,9 +283,37 @@ def get_neighbor_epi(node: NodoProtocol) -> tuple[list[NodoProtocol], float]:
|
|
|
115
283
|
|
|
116
284
|
|
|
117
285
|
def _determine_dominant(
|
|
118
|
-
neigh: list[
|
|
286
|
+
neigh: list[NodeProtocol], default_kind: str
|
|
119
287
|
) -> tuple[str, float]:
|
|
120
|
-
"""
|
|
288
|
+
"""Resolve the dominant ``epi_kind`` across neighbours.
|
|
289
|
+
|
|
290
|
+
The dominant kind guides glyphs that synchronise EPI, ensuring that
|
|
291
|
+
reshaping a node's EPI also maintains a coherent semantic label for the
|
|
292
|
+
structural phase space.
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
neigh : list of NodeProtocol
|
|
297
|
+
Neighbouring nodes providing EPI magnitude and semantic kind.
|
|
298
|
+
default_kind : str
|
|
299
|
+
Fallback label when no neighbour exposes an ``epi_kind``.
|
|
300
|
+
|
|
301
|
+
Returns
|
|
302
|
+
-------
|
|
303
|
+
tuple of (str, float)
|
|
304
|
+
The dominant ``epi_kind`` together with the maximum absolute EPI. The
|
|
305
|
+
amplitude assists downstream logic when choosing between the node's own
|
|
306
|
+
label and the neighbour-driven kind.
|
|
307
|
+
|
|
308
|
+
Examples
|
|
309
|
+
--------
|
|
310
|
+
>>> class Mock:
|
|
311
|
+
... def __init__(self, epi, kind):
|
|
312
|
+
... self.EPI = epi
|
|
313
|
+
... self.epi_kind = kind
|
|
314
|
+
>>> _determine_dominant([Mock(0.2, "seed"), Mock(-1.0, "pulse")], "seed")
|
|
315
|
+
('pulse', 1.0)
|
|
316
|
+
"""
|
|
121
317
|
best_kind: str | None = None
|
|
122
318
|
best_abs = 0.0
|
|
123
319
|
for v in neigh:
|
|
@@ -131,13 +327,48 @@ def _determine_dominant(
|
|
|
131
327
|
|
|
132
328
|
|
|
133
329
|
def _mix_epi_with_neighbors(
|
|
134
|
-
node:
|
|
330
|
+
node: NodeProtocol, mix: float, default_glyph: Glyph | str
|
|
135
331
|
) -> tuple[float, str]:
|
|
136
|
-
"""
|
|
332
|
+
"""Blend node EPI with the neighbour field and update its semantic label.
|
|
333
|
+
|
|
334
|
+
The routine is shared by reception-like glyphs. It interpolates between the
|
|
335
|
+
node EPI and the neighbour mean while selecting a dominant ``epi_kind``.
|
|
336
|
+
ΔNFR, νf, and phase remain untouched; the function focuses on reconciling
|
|
337
|
+
form.
|
|
338
|
+
|
|
339
|
+
Parameters
|
|
340
|
+
----------
|
|
341
|
+
node : NodeProtocol
|
|
342
|
+
Node that exposes ``EPI`` and ``epi_kind`` attributes.
|
|
343
|
+
mix : float
|
|
344
|
+
Interpolation weight for the neighbour mean. ``mix = 0`` preserves the
|
|
345
|
+
current EPI, while ``mix = 1`` adopts the average neighbour field.
|
|
346
|
+
default_glyph : Glyph or str
|
|
347
|
+
Glyph driving the mix. Its value informs the fallback ``epi_kind``.
|
|
348
|
+
|
|
349
|
+
Returns
|
|
350
|
+
-------
|
|
351
|
+
tuple of (float, str)
|
|
352
|
+
The neighbour mean EPI and the resolved ``epi_kind`` after mixing.
|
|
353
|
+
|
|
354
|
+
Examples
|
|
355
|
+
--------
|
|
356
|
+
>>> class MockNode:
|
|
357
|
+
... def __init__(self, epi, kind, neighbors):
|
|
358
|
+
... self.EPI = epi
|
|
359
|
+
... self.epi_kind = kind
|
|
360
|
+
... self.graph = {}
|
|
361
|
+
... self._neighbors = neighbors
|
|
362
|
+
... def neighbors(self):
|
|
363
|
+
... return self._neighbors
|
|
364
|
+
>>> neigh = [MockNode(0.8, "wave", []), MockNode(1.2, "wave", [])]
|
|
365
|
+
>>> node = MockNode(0.0, "seed", neigh)
|
|
366
|
+
>>> _, kind = _mix_epi_with_neighbors(node, 0.5, Glyph.EN)
|
|
367
|
+
>>> round(node.EPI, 2), kind
|
|
368
|
+
(0.5, 'wave')
|
|
369
|
+
"""
|
|
137
370
|
default_kind = (
|
|
138
|
-
default_glyph.value
|
|
139
|
-
if isinstance(default_glyph, Glyph)
|
|
140
|
-
else str(default_glyph)
|
|
371
|
+
default_glyph.value if isinstance(default_glyph, Glyph) else str(default_glyph)
|
|
141
372
|
)
|
|
142
373
|
epi = node.EPI
|
|
143
374
|
neigh, epi_bar = get_neighbor_epi(node)
|
|
@@ -148,7 +379,7 @@ def _mix_epi_with_neighbors(
|
|
|
148
379
|
|
|
149
380
|
dominant, best_abs = _determine_dominant(neigh, default_kind)
|
|
150
381
|
new_epi = (1 - mix) * epi + mix * epi_bar
|
|
151
|
-
node
|
|
382
|
+
_set_epi_with_boundary_check(node, new_epi)
|
|
152
383
|
final = dominant if best_abs > abs(new_epi) else node.epi_kind
|
|
153
384
|
if not final:
|
|
154
385
|
final = default_kind
|
|
@@ -156,22 +387,122 @@ def _mix_epi_with_neighbors(
|
|
|
156
387
|
return epi_bar, final
|
|
157
388
|
|
|
158
389
|
|
|
159
|
-
def _op_AL(node:
|
|
390
|
+
def _op_AL(node: NodeProtocol, gf: GlyphFactors) -> None: # AL — Emission
|
|
391
|
+
"""Amplify the node EPI via the Emission glyph.
|
|
392
|
+
|
|
393
|
+
Emission injects additional coherence into the node by boosting its EPI
|
|
394
|
+
without touching νf, ΔNFR, or phase. The boost amplitude is controlled by
|
|
395
|
+
``AL_boost``.
|
|
396
|
+
|
|
397
|
+
Parameters
|
|
398
|
+
----------
|
|
399
|
+
node : NodeProtocol
|
|
400
|
+
Node whose EPI is increased.
|
|
401
|
+
gf : GlyphFactors
|
|
402
|
+
Factor mapping used to resolve ``AL_boost``.
|
|
403
|
+
|
|
404
|
+
Examples
|
|
405
|
+
--------
|
|
406
|
+
>>> class MockNode:
|
|
407
|
+
... def __init__(self, epi):
|
|
408
|
+
... self.EPI = epi
|
|
409
|
+
... self.graph = {}
|
|
410
|
+
>>> node = MockNode(0.8)
|
|
411
|
+
>>> _op_AL(node, {"AL_boost": 0.2})
|
|
412
|
+
>>> node.EPI <= 1.0 # Bounded by structural_clip
|
|
413
|
+
True
|
|
414
|
+
"""
|
|
160
415
|
f = get_factor(gf, "AL_boost", 0.05)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
416
|
+
new_epi = node.EPI + f
|
|
417
|
+
_set_epi_with_boundary_check(node, new_epi)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _op_EN(node: NodeProtocol, gf: GlyphFactors) -> None: # EN — Reception
|
|
421
|
+
"""Mix the node EPI with the neighbour field via Reception.
|
|
422
|
+
|
|
423
|
+
Reception reorganizes the node's EPI towards the neighbourhood mean while
|
|
424
|
+
choosing a coherent ``epi_kind``. νf, ΔNFR, and phase remain unchanged.
|
|
425
|
+
|
|
426
|
+
Parameters
|
|
427
|
+
----------
|
|
428
|
+
node : NodeProtocol
|
|
429
|
+
Node whose EPI is being reconciled.
|
|
430
|
+
gf : GlyphFactors
|
|
431
|
+
Source of the ``EN_mix`` blending coefficient.
|
|
432
|
+
|
|
433
|
+
Examples
|
|
434
|
+
--------
|
|
435
|
+
>>> class MockNode:
|
|
436
|
+
... def __init__(self, epi, neighbors):
|
|
437
|
+
... self.EPI = epi
|
|
438
|
+
... self.epi_kind = "seed"
|
|
439
|
+
... self.graph = {}
|
|
440
|
+
... self._neighbors = neighbors
|
|
441
|
+
... def neighbors(self):
|
|
442
|
+
... return self._neighbors
|
|
443
|
+
>>> neigh = [MockNode(1.0, []), MockNode(0.0, [])]
|
|
444
|
+
>>> node = MockNode(0.4, neigh)
|
|
445
|
+
>>> _op_EN(node, {"EN_mix": 0.5})
|
|
446
|
+
>>> round(node.EPI, 2)
|
|
447
|
+
0.7
|
|
448
|
+
"""
|
|
165
449
|
mix = get_factor(gf, "EN_mix", 0.25)
|
|
166
450
|
_mix_epi_with_neighbors(node, mix, Glyph.EN)
|
|
167
451
|
|
|
168
452
|
|
|
169
|
-
def _op_IL(node:
|
|
453
|
+
def _op_IL(node: NodeProtocol, gf: GlyphFactors) -> None: # IL — Coherence
|
|
454
|
+
"""Dampen ΔNFR magnitudes through the Coherence glyph.
|
|
455
|
+
|
|
456
|
+
Coherence contracts the internal reorganization differential (ΔNFR) while
|
|
457
|
+
leaving EPI, νf, and phase untouched. The contraction preserves the sign of
|
|
458
|
+
ΔNFR, increasing structural stability.
|
|
459
|
+
|
|
460
|
+
Parameters
|
|
461
|
+
----------
|
|
462
|
+
node : NodeProtocol
|
|
463
|
+
Node whose ΔNFR is being scaled.
|
|
464
|
+
gf : GlyphFactors
|
|
465
|
+
Provides ``IL_dnfr_factor`` controlling the contraction strength.
|
|
466
|
+
|
|
467
|
+
Examples
|
|
468
|
+
--------
|
|
469
|
+
>>> class MockNode:
|
|
470
|
+
... def __init__(self, dnfr):
|
|
471
|
+
... self.dnfr = dnfr
|
|
472
|
+
>>> node = MockNode(0.5)
|
|
473
|
+
>>> _op_IL(node, {"IL_dnfr_factor": 0.2})
|
|
474
|
+
>>> node.dnfr
|
|
475
|
+
0.1
|
|
476
|
+
"""
|
|
170
477
|
factor = get_factor(gf, "IL_dnfr_factor", 0.7)
|
|
171
478
|
node.dnfr = factor * getattr(node, "dnfr", 0.0)
|
|
172
479
|
|
|
173
480
|
|
|
174
|
-
def _op_OZ(node:
|
|
481
|
+
def _op_OZ(node: NodeProtocol, gf: GlyphFactors) -> None: # OZ — Dissonance
|
|
482
|
+
"""Excite ΔNFR through the Dissonance glyph.
|
|
483
|
+
|
|
484
|
+
Dissonance amplifies ΔNFR or injects jitter, testing the node's stability.
|
|
485
|
+
EPI, νf, and phase remain unaffected while ΔNFR grows to trigger potential
|
|
486
|
+
bifurcations.
|
|
487
|
+
|
|
488
|
+
Parameters
|
|
489
|
+
----------
|
|
490
|
+
node : NodeProtocol
|
|
491
|
+
Node whose ΔNFR is being stressed.
|
|
492
|
+
gf : GlyphFactors
|
|
493
|
+
Supplies ``OZ_dnfr_factor`` and optional noise parameters.
|
|
494
|
+
|
|
495
|
+
Examples
|
|
496
|
+
--------
|
|
497
|
+
>>> class MockNode:
|
|
498
|
+
... def __init__(self, dnfr):
|
|
499
|
+
... self.dnfr = dnfr
|
|
500
|
+
... self.graph = {}
|
|
501
|
+
>>> node = MockNode(0.2)
|
|
502
|
+
>>> _op_OZ(node, {"OZ_dnfr_factor": 2.0})
|
|
503
|
+
>>> node.dnfr
|
|
504
|
+
0.4
|
|
505
|
+
"""
|
|
175
506
|
factor = get_factor(gf, "OZ_dnfr_factor", 1.3)
|
|
176
507
|
dnfr = getattr(node, "dnfr", 0.0)
|
|
177
508
|
if bool(node.graph.get("OZ_NOISE_MODE", False)):
|
|
@@ -184,31 +515,29 @@ def _op_OZ(node: NodoProtocol, gf: dict[str, Any]) -> None: # OZ — Disonancia
|
|
|
184
515
|
node.dnfr = factor * dnfr if abs(dnfr) > 1e-9 else 0.1
|
|
185
516
|
|
|
186
517
|
|
|
187
|
-
def _um_candidate_iter(node:
|
|
518
|
+
def _um_candidate_iter(node: NodeProtocol) -> Iterator[NodeProtocol]:
|
|
188
519
|
sample_ids = node.graph.get("_node_sample")
|
|
189
520
|
if sample_ids is not None and hasattr(node, "G"):
|
|
190
|
-
|
|
191
|
-
if
|
|
192
|
-
raise ImportError("
|
|
193
|
-
base = (
|
|
521
|
+
NodeNX = get_nodenx()
|
|
522
|
+
if NodeNX is None:
|
|
523
|
+
raise ImportError("NodeNX is unavailable")
|
|
524
|
+
base = (NodeNX.from_graph(node.G, j) for j in sample_ids)
|
|
194
525
|
else:
|
|
195
526
|
base = node.all_nodes()
|
|
196
527
|
for j in base:
|
|
197
|
-
same = (j is node) or (
|
|
198
|
-
getattr(node, "n", None) == getattr(j, "n", None)
|
|
199
|
-
)
|
|
528
|
+
same = (j is node) or (getattr(node, "n", None) == getattr(j, "n", None))
|
|
200
529
|
if same or node.has_edge(j):
|
|
201
530
|
continue
|
|
202
531
|
yield j
|
|
203
532
|
|
|
204
533
|
|
|
205
534
|
def _um_select_candidates(
|
|
206
|
-
node:
|
|
207
|
-
candidates,
|
|
535
|
+
node: NodeProtocol,
|
|
536
|
+
candidates: Iterator[NodeProtocol],
|
|
208
537
|
limit: int,
|
|
209
538
|
mode: str,
|
|
210
539
|
th: float,
|
|
211
|
-
):
|
|
540
|
+
) -> list[NodeProtocol]:
|
|
212
541
|
"""Select a subset of ``candidates`` for UM coupling."""
|
|
213
542
|
rng = make_rng(int(node.graph.get("RANDOM_SEED", 0)), node.offset(), node.G)
|
|
214
543
|
|
|
@@ -232,14 +561,202 @@ def _um_select_candidates(
|
|
|
232
561
|
return reservoir
|
|
233
562
|
|
|
234
563
|
|
|
235
|
-
def
|
|
564
|
+
def compute_consensus_phase(phases: list[float]) -> float:
|
|
565
|
+
"""Compute circular mean (consensus phase) from a list of phase angles.
|
|
566
|
+
|
|
567
|
+
This function calculates the consensus phase using the circular mean
|
|
568
|
+
formula: arctan2(mean(sin), mean(cos)). This ensures proper handling
|
|
569
|
+
of phase wrapping at ±π boundaries.
|
|
570
|
+
|
|
571
|
+
Parameters
|
|
572
|
+
----------
|
|
573
|
+
phases : list[float]
|
|
574
|
+
List of phase angles in radians.
|
|
575
|
+
|
|
576
|
+
Returns
|
|
577
|
+
-------
|
|
578
|
+
float
|
|
579
|
+
Consensus phase angle in radians, in the range [-π, π).
|
|
580
|
+
|
|
581
|
+
Notes
|
|
582
|
+
-----
|
|
583
|
+
The consensus phase represents the central tendency of a set of angular
|
|
584
|
+
values, accounting for the circular nature of phase space. This is
|
|
585
|
+
critical for bidirectional phase synchronization in the UM operator.
|
|
586
|
+
|
|
587
|
+
Examples
|
|
588
|
+
--------
|
|
589
|
+
>>> import math
|
|
590
|
+
>>> phases = [0.0, math.pi/2, math.pi]
|
|
591
|
+
>>> result = compute_consensus_phase(phases)
|
|
592
|
+
>>> -math.pi <= result < math.pi
|
|
593
|
+
True
|
|
594
|
+
"""
|
|
595
|
+
if not phases:
|
|
596
|
+
return 0.0
|
|
597
|
+
|
|
598
|
+
cos_sum = sum(math.cos(ph) for ph in phases)
|
|
599
|
+
sin_sum = sum(math.sin(ph) for ph in phases)
|
|
600
|
+
return math.atan2(sin_sum, cos_sum)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _op_UM(node: NodeProtocol, gf: GlyphFactors) -> None: # UM — Coupling
|
|
604
|
+
"""Align node phase and frequency with neighbours and optionally create links.
|
|
605
|
+
|
|
606
|
+
Coupling shifts the node phase ``theta`` towards the neighbour mean while
|
|
607
|
+
respecting νf and EPI. When bidirectional mode is enabled (default), both
|
|
608
|
+
the node and its neighbors synchronize their phases mutually. Additionally,
|
|
609
|
+
structural frequency (νf) synchronization causes coupled nodes to converge
|
|
610
|
+
their reorganization rates. Coupling also reduces ΔNFR through mutual
|
|
611
|
+
stabilization, decreasing reorganization pressure proportional to phase
|
|
612
|
+
alignment strength. When functional links are enabled it may add edges
|
|
613
|
+
based on combined phase, EPI, and sense-index similarity.
|
|
614
|
+
|
|
615
|
+
Parameters
|
|
616
|
+
----------
|
|
617
|
+
node : NodeProtocol
|
|
618
|
+
Node whose phase and frequency are being synchronised.
|
|
619
|
+
gf : GlyphFactors
|
|
620
|
+
Provides ``UM_theta_push``, ``UM_vf_sync``, ``UM_dnfr_reduction`` and
|
|
621
|
+
optional selection parameters.
|
|
622
|
+
|
|
623
|
+
Notes
|
|
624
|
+
-----
|
|
625
|
+
Bidirectional synchronization (UM_BIDIRECTIONAL=True, default) implements
|
|
626
|
+
the canonical TNFR requirement φᵢ(t) ≈ φⱼ(t) by mutually adjusting phases
|
|
627
|
+
of both the node and its neighbors towards a consensus phase. This ensures
|
|
628
|
+
true coupling as defined in the theory.
|
|
629
|
+
|
|
630
|
+
Structural frequency synchronization (UM_SYNC_VF=True, default) implements
|
|
631
|
+
the TNFR requirement that coupling synchronizes not only phases but also
|
|
632
|
+
structural frequencies (νf). This enables coupled nodes to converge their
|
|
633
|
+
reorganization rates, which is essential for sustained resonance and coherent
|
|
634
|
+
network evolution as described by the nodal equation: ∂EPI/∂t = νf · ΔNFR(t).
|
|
635
|
+
|
|
636
|
+
ΔNFR stabilization (UM_STABILIZE_DNFR=True, default) implements the canonical
|
|
637
|
+
effect where coupling reduces reorganization pressure through mutual stabilization.
|
|
638
|
+
The reduction is proportional to phase alignment: well-coupled nodes (high phase
|
|
639
|
+
alignment) experience stronger ΔNFR reduction, promoting structural coherence.
|
|
640
|
+
|
|
641
|
+
Legacy unidirectional mode (UM_BIDIRECTIONAL=False) only adjusts the node's
|
|
642
|
+
phase towards its neighbors, preserving backward compatibility.
|
|
643
|
+
|
|
644
|
+
Examples
|
|
645
|
+
--------
|
|
646
|
+
>>> import math
|
|
647
|
+
>>> class MockNode:
|
|
648
|
+
... def __init__(self, theta, neighbors):
|
|
649
|
+
... self.theta = theta
|
|
650
|
+
... self.EPI = 1.0
|
|
651
|
+
... self.Si = 0.5
|
|
652
|
+
... self.graph = {}
|
|
653
|
+
... self._neighbors = neighbors
|
|
654
|
+
... def neighbors(self):
|
|
655
|
+
... return self._neighbors
|
|
656
|
+
... def offset(self):
|
|
657
|
+
... return 0
|
|
658
|
+
... def all_nodes(self):
|
|
659
|
+
... return []
|
|
660
|
+
... def has_edge(self, _):
|
|
661
|
+
... return False
|
|
662
|
+
... def add_edge(self, *_):
|
|
663
|
+
... raise AssertionError("not used in example")
|
|
664
|
+
>>> neighbor = MockNode(math.pi / 2, [])
|
|
665
|
+
>>> node = MockNode(0.0, [neighbor])
|
|
666
|
+
>>> _op_UM(node, {"UM_theta_push": 0.5})
|
|
667
|
+
>>> round(node.theta, 2)
|
|
668
|
+
0.79
|
|
669
|
+
"""
|
|
236
670
|
k = get_factor(gf, "UM_theta_push", 0.25)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
671
|
+
k_vf = get_factor(gf, "UM_vf_sync", 0.10)
|
|
672
|
+
th_i = node.theta
|
|
673
|
+
|
|
674
|
+
# Check if bidirectional synchronization is enabled (default: True)
|
|
675
|
+
bidirectional = bool(node.graph.get("UM_BIDIRECTIONAL", True))
|
|
676
|
+
|
|
677
|
+
if bidirectional:
|
|
678
|
+
# Bidirectional mode: mutually synchronize node and neighbors
|
|
679
|
+
neighbor_ids = list(node.neighbors())
|
|
680
|
+
if neighbor_ids:
|
|
681
|
+
# Get NodeNX wrapper for accessing neighbor attributes
|
|
682
|
+
NodeNX = get_nodenx()
|
|
683
|
+
if NodeNX is None or not hasattr(node, "G"):
|
|
684
|
+
# Fallback to unidirectional if NodeNX unavailable
|
|
685
|
+
thL = neighbor_phase_mean(node)
|
|
686
|
+
d = angle_diff(thL, th_i)
|
|
687
|
+
node.theta = th_i + k * d
|
|
688
|
+
else:
|
|
689
|
+
# Wrap neighbor IDs to access theta attribute
|
|
690
|
+
neighbors = [NodeNX.from_graph(node.G, nid) for nid in neighbor_ids]
|
|
241
691
|
|
|
242
|
-
|
|
692
|
+
# Collect all phases (node + neighbors)
|
|
693
|
+
phases = [th_i] + [n.theta for n in neighbors]
|
|
694
|
+
target_phase = compute_consensus_phase(phases)
|
|
695
|
+
|
|
696
|
+
# Adjust node phase towards consensus
|
|
697
|
+
node.theta = th_i + k * angle_diff(target_phase, th_i)
|
|
698
|
+
|
|
699
|
+
# Adjust neighbor phases towards consensus
|
|
700
|
+
for neighbor in neighbors:
|
|
701
|
+
th_j = neighbor.theta
|
|
702
|
+
neighbor.theta = th_j + k * angle_diff(target_phase, th_j)
|
|
703
|
+
else:
|
|
704
|
+
# Legacy unidirectional mode: only adjust node towards neighbors
|
|
705
|
+
thL = neighbor_phase_mean(node)
|
|
706
|
+
d = angle_diff(thL, th_i)
|
|
707
|
+
node.theta = th_i + k * d
|
|
708
|
+
|
|
709
|
+
# Structural frequency (νf) synchronization
|
|
710
|
+
# According to TNFR theory, coupling synchronizes both phase and frequency
|
|
711
|
+
sync_vf = bool(node.graph.get("UM_SYNC_VF", True))
|
|
712
|
+
if sync_vf:
|
|
713
|
+
neighbor_ids = list(node.neighbors())
|
|
714
|
+
if neighbor_ids and hasattr(node, "G"):
|
|
715
|
+
# Canonical access to vf through alias system
|
|
716
|
+
vf_i = node.vf
|
|
717
|
+
vf_neighbors = [
|
|
718
|
+
get_attr(node.G.nodes[nid], ALIAS_VF, 0.0) for nid in neighbor_ids
|
|
719
|
+
]
|
|
720
|
+
|
|
721
|
+
if vf_neighbors:
|
|
722
|
+
vf_mean = sum(vf_neighbors) / len(vf_neighbors)
|
|
723
|
+
|
|
724
|
+
# Gradual convergence towards mean (similar to phase sync)
|
|
725
|
+
node.vf = vf_i + k_vf * (vf_mean - vf_i)
|
|
726
|
+
|
|
727
|
+
# ΔNFR reduction by mutual stabilization
|
|
728
|
+
# Coupling produces a stabilizing effect that reduces reorganization pressure
|
|
729
|
+
stabilize_dnfr = bool(node.graph.get("UM_STABILIZE_DNFR", True))
|
|
730
|
+
|
|
731
|
+
if stabilize_dnfr:
|
|
732
|
+
k_dnfr = get_factor(gf, "UM_dnfr_reduction", 0.15)
|
|
733
|
+
|
|
734
|
+
# Calculate compatibility with neighbors based on phase alignment
|
|
735
|
+
neighbor_ids = list(node.neighbors())
|
|
736
|
+
if neighbor_ids:
|
|
737
|
+
# Get NodeNX wrapper for accessing neighbor attributes
|
|
738
|
+
NodeNX = get_nodenx()
|
|
739
|
+
if NodeNX is not None and hasattr(node, "G"):
|
|
740
|
+
neighbors = [NodeNX.from_graph(node.G, nid) for nid in neighbor_ids]
|
|
741
|
+
|
|
742
|
+
# Compute phase alignments with each neighbor
|
|
743
|
+
phase_alignments = []
|
|
744
|
+
# Compute phase alignment using canonical formula
|
|
745
|
+
from ..metrics.phase_compatibility import compute_phase_coupling_strength
|
|
746
|
+
|
|
747
|
+
for neighbor in neighbors:
|
|
748
|
+
alignment = compute_phase_coupling_strength(node.theta, neighbor.theta)
|
|
749
|
+
phase_alignments.append(alignment)
|
|
750
|
+
|
|
751
|
+
# Mean alignment represents coupling strength
|
|
752
|
+
mean_alignment = sum(phase_alignments) / len(phase_alignments)
|
|
753
|
+
|
|
754
|
+
# Reduce ΔNFR proportionally to coupling strength
|
|
755
|
+
# reduction_factor < 1.0 when well-coupled (high alignment)
|
|
756
|
+
reduction_factor = 1.0 - (k_dnfr * mean_alignment)
|
|
757
|
+
node.dnfr = node.dnfr * reduction_factor
|
|
758
|
+
|
|
759
|
+
if bool(node.graph.get("UM_FUNCTIONAL_LINKS", True)):
|
|
243
760
|
thr = float(
|
|
244
761
|
node.graph.get(
|
|
245
762
|
"UM_COMPAT_THRESHOLD",
|
|
@@ -252,69 +769,733 @@ def _op_UM(node: NodoProtocol, gf: dict[str, Any]) -> None: # UM — Coupling
|
|
|
252
769
|
limit = int(node.graph.get("UM_CANDIDATE_COUNT", 0))
|
|
253
770
|
mode = str(node.graph.get("UM_CANDIDATE_MODE", "sample")).lower()
|
|
254
771
|
candidates = _um_select_candidates(
|
|
255
|
-
node, _um_candidate_iter(node), limit, mode,
|
|
772
|
+
node, _um_candidate_iter(node), limit, mode, th_i
|
|
256
773
|
)
|
|
257
774
|
|
|
258
775
|
for j in candidates:
|
|
259
|
-
|
|
260
|
-
|
|
776
|
+
# Use canonical phase coupling strength formula
|
|
777
|
+
from ..metrics.phase_compatibility import compute_phase_coupling_strength
|
|
778
|
+
|
|
779
|
+
phase_coupling = compute_phase_coupling_strength(th_i, j.theta)
|
|
780
|
+
|
|
261
781
|
epi_j = j.EPI
|
|
262
782
|
si_j = j.Si
|
|
263
|
-
epi_sim = 1.0 - abs(epi_i - epi_j) / (
|
|
264
|
-
abs(epi_i) + abs(epi_j) + 1e-9
|
|
265
|
-
)
|
|
783
|
+
epi_sim = 1.0 - abs(epi_i - epi_j) / (abs(epi_i) + abs(epi_j) + 1e-9)
|
|
266
784
|
si_sim = 1.0 - abs(si_i - si_j)
|
|
267
|
-
|
|
785
|
+
# Compatibility combines phase coupling (50%), EPI similarity (25%), Si similarity (25%)
|
|
786
|
+
compat = phase_coupling * 0.5 + 0.25 * epi_sim + 0.25 * si_sim
|
|
268
787
|
if compat >= thr:
|
|
269
788
|
node.add_edge(j, compat)
|
|
270
789
|
|
|
271
790
|
|
|
272
|
-
def _op_RA(node:
|
|
791
|
+
def _op_RA(node: NodeProtocol, gf: GlyphFactors) -> None: # RA — Resonance
|
|
792
|
+
"""Propagate coherence through resonance with νf amplification.
|
|
793
|
+
|
|
794
|
+
Resonance (RA) propagates EPI along existing couplings while amplifying
|
|
795
|
+
the structural frequency (νf) to reflect network coherence propagation.
|
|
796
|
+
According to TNFR theory, RA creates "resonant cascades" where coherence
|
|
797
|
+
amplifies across the network, increasing collective νf and global C(t).
|
|
798
|
+
|
|
799
|
+
**Canonical Effects (always active):**
|
|
800
|
+
|
|
801
|
+
- **EPI Propagation**: Diffuses EPI to neighbors (identity-preserving)
|
|
802
|
+
- **νf Amplification**: Increases structural frequency when propagating coherence
|
|
803
|
+
- **Phase Alignment**: Strengthens phase synchrony across propagation path
|
|
804
|
+
- **Network C(t)**: Contributes to global coherence increase
|
|
805
|
+
- **Identity Preservation**: Maintains structural identity during propagation
|
|
806
|
+
|
|
807
|
+
Parameters
|
|
808
|
+
----------
|
|
809
|
+
node : NodeProtocol
|
|
810
|
+
Node harmonising with its neighbourhood.
|
|
811
|
+
gf : GlyphFactors
|
|
812
|
+
Provides ``RA_epi_diff`` (mixing coefficient, default 0.15),
|
|
813
|
+
``RA_vf_amplification`` (νf boost factor, default 0.05), and
|
|
814
|
+
``RA_phase_coupling`` (phase alignment factor, default 0.10).
|
|
815
|
+
|
|
816
|
+
Notes
|
|
817
|
+
-----
|
|
818
|
+
**νf Amplification (Canonical)**: When neighbors have coherence (|epi_bar| > 1e-9),
|
|
819
|
+
node.vf is multiplied by (1.0 + RA_vf_amplification). This reflects
|
|
820
|
+
the canonical TNFR property that resonance amplifies collective νf.
|
|
821
|
+
This is NOT optional - it is a fundamental property of resonance per TNFR theory.
|
|
822
|
+
|
|
823
|
+
**Phase Alignment Strengthening (Canonical)**: RA strengthens phase alignment
|
|
824
|
+
with neighbors by applying a small phase correction toward the network mean.
|
|
825
|
+
This ensures that "Phase alignment: Strengthens across propagation path" as
|
|
826
|
+
stated in the theoretical foundations. Uses existing phase utility functions
|
|
827
|
+
to avoid code duplication.
|
|
828
|
+
|
|
829
|
+
**Network Coherence Tracking (Optional)**: If ``TRACK_NETWORK_COHERENCE`` is enabled,
|
|
830
|
+
global C(t) is measured before/after RA application to quantify network-level
|
|
831
|
+
coherence increase.
|
|
832
|
+
|
|
833
|
+
**Identity Preservation (Canonical)**: EPI structure (kind and sign) are preserved
|
|
834
|
+
during propagation to ensure structural identity is maintained as required by theory.
|
|
835
|
+
|
|
836
|
+
Examples
|
|
837
|
+
--------
|
|
838
|
+
>>> class MockNode:
|
|
839
|
+
... def __init__(self, epi, neighbors):
|
|
840
|
+
... self.EPI = epi
|
|
841
|
+
... self.epi_kind = "seed"
|
|
842
|
+
... self.vf = 1.0
|
|
843
|
+
... self.theta = 0.0
|
|
844
|
+
... self.graph = {}
|
|
845
|
+
... self._neighbors = neighbors
|
|
846
|
+
... def neighbors(self):
|
|
847
|
+
... return self._neighbors
|
|
848
|
+
>>> neighbor = MockNode(1.0, [])
|
|
849
|
+
>>> neighbor.theta = 0.1
|
|
850
|
+
>>> node = MockNode(0.2, [neighbor])
|
|
851
|
+
>>> _op_RA(node, {"RA_epi_diff": 0.25, "RA_vf_amplification": 0.05})
|
|
852
|
+
>>> round(node.EPI, 2)
|
|
853
|
+
0.4
|
|
854
|
+
>>> node.vf # Amplified due to neighbor coherence (canonical effect)
|
|
855
|
+
1.05
|
|
856
|
+
"""
|
|
857
|
+
# Get configuration factors
|
|
273
858
|
diff = get_factor(gf, "RA_epi_diff", 0.15)
|
|
274
|
-
|
|
859
|
+
vf_boost = get_factor(gf, "RA_vf_amplification", 0.05)
|
|
860
|
+
phase_coupling = get_factor(
|
|
861
|
+
gf, "RA_phase_coupling", 0.10
|
|
862
|
+
) # Canonical phase strengthening
|
|
863
|
+
|
|
864
|
+
# Track network C(t) before RA if enabled (optional telemetry)
|
|
865
|
+
track_coherence = bool(node.graph.get("TRACK_NETWORK_COHERENCE", False))
|
|
866
|
+
c_before = None
|
|
867
|
+
if track_coherence and hasattr(node, "G"):
|
|
868
|
+
try:
|
|
869
|
+
from ..metrics.coherence import compute_network_coherence
|
|
870
|
+
|
|
871
|
+
c_before = compute_network_coherence(node.G)
|
|
872
|
+
if "_ra_c_tracking" not in node.graph:
|
|
873
|
+
node.graph["_ra_c_tracking"] = []
|
|
874
|
+
except ImportError:
|
|
875
|
+
pass # Metrics module not available
|
|
275
876
|
|
|
877
|
+
# Capture state before for metrics
|
|
878
|
+
vf_before = node.vf
|
|
879
|
+
epi_before = node.EPI
|
|
880
|
+
kind_before = node.epi_kind
|
|
881
|
+
theta_before = node.theta if hasattr(node, "theta") else None
|
|
276
882
|
|
|
277
|
-
|
|
883
|
+
# EPI diffusion (existing behavior)
|
|
884
|
+
neigh, epi_bar = get_neighbor_epi(node)
|
|
885
|
+
epi_bar_result, kind_result = _mix_epi_with_neighbors(node, diff, Glyph.RA)
|
|
886
|
+
|
|
887
|
+
# CANONICAL EFFECT 1: νf amplification through resonance
|
|
888
|
+
# This is always active - it's a fundamental property of resonance per TNFR theory
|
|
889
|
+
# Only amplify if neighbors have coherence to propagate
|
|
890
|
+
if abs(epi_bar_result) > 1e-9 and len(neigh) > 0:
|
|
891
|
+
node.vf *= 1.0 + vf_boost
|
|
892
|
+
|
|
893
|
+
# CANONICAL EFFECT 2: Phase alignment strengthening
|
|
894
|
+
# Per theory: "Phase alignment: Strengthens across propagation path"
|
|
895
|
+
# Uses existing phase locking logic from IL operator (avoid duplication)
|
|
896
|
+
phase_strengthened = False
|
|
897
|
+
if len(neigh) > 0 and hasattr(node, "theta") and hasattr(node, "G"):
|
|
898
|
+
try:
|
|
899
|
+
# Use existing phase locking utility from IL operator
|
|
900
|
+
from ..alias import get_attr
|
|
901
|
+
from ..constants.aliases import ALIAS_THETA
|
|
902
|
+
import cmath
|
|
903
|
+
import math
|
|
904
|
+
|
|
905
|
+
# Get neighbor phases using existing utilities
|
|
906
|
+
neighbor_phases = []
|
|
907
|
+
for n in neigh:
|
|
908
|
+
try:
|
|
909
|
+
theta_n = float(get_attr(n, ALIAS_THETA, 0.0))
|
|
910
|
+
neighbor_phases.append(theta_n)
|
|
911
|
+
except (KeyError, ValueError, TypeError):
|
|
912
|
+
continue
|
|
913
|
+
|
|
914
|
+
if neighbor_phases:
|
|
915
|
+
# Circular mean using the same method as in phase_coherence.py
|
|
916
|
+
complex_phases = [cmath.exp(1j * theta) for theta in neighbor_phases]
|
|
917
|
+
mean_real = sum(z.real for z in complex_phases) / len(complex_phases)
|
|
918
|
+
mean_imag = sum(z.imag for z in complex_phases) / len(complex_phases)
|
|
919
|
+
mean_complex = complex(mean_real, mean_imag)
|
|
920
|
+
mean_phase = cmath.phase(mean_complex)
|
|
921
|
+
|
|
922
|
+
# Ensure positive phase [0, 2π]
|
|
923
|
+
if mean_phase < 0:
|
|
924
|
+
mean_phase += 2 * math.pi
|
|
925
|
+
|
|
926
|
+
# Calculate phase difference (shortest arc)
|
|
927
|
+
delta_theta = mean_phase - node.theta
|
|
928
|
+
if delta_theta > math.pi:
|
|
929
|
+
delta_theta -= 2 * math.pi
|
|
930
|
+
elif delta_theta < -math.pi:
|
|
931
|
+
delta_theta += 2 * math.pi
|
|
932
|
+
|
|
933
|
+
# Apply phase strengthening (move toward network mean)
|
|
934
|
+
# Same approach as IL operator phase locking
|
|
935
|
+
node.theta = node.theta + phase_coupling * delta_theta
|
|
936
|
+
|
|
937
|
+
# Normalize to [0, 2π]
|
|
938
|
+
node.theta = node.theta % (2 * math.pi)
|
|
939
|
+
phase_strengthened = True
|
|
940
|
+
except (AttributeError, ImportError):
|
|
941
|
+
pass # Phase alignment not possible in this context
|
|
942
|
+
|
|
943
|
+
# Track identity preservation (canonical validation)
|
|
944
|
+
identity_preserved = (
|
|
945
|
+
kind_result == kind_before or kind_result == Glyph.RA.value
|
|
946
|
+
) and (
|
|
947
|
+
float(epi_before) * float(node.EPI) >= 0
|
|
948
|
+
) # Sign preserved
|
|
949
|
+
|
|
950
|
+
# Collect propagation metrics if enabled (optional telemetry)
|
|
951
|
+
collect_metrics = bool(node.graph.get("COLLECT_RA_METRICS", False))
|
|
952
|
+
if collect_metrics:
|
|
953
|
+
metrics = {
|
|
954
|
+
"operator": "RA",
|
|
955
|
+
"epi_propagated": epi_bar_result,
|
|
956
|
+
"vf_amplification": node.vf / vf_before if vf_before > 0 else 1.0,
|
|
957
|
+
"neighbors_influenced": len(neigh),
|
|
958
|
+
"identity_preserved": identity_preserved,
|
|
959
|
+
"epi_before": epi_before,
|
|
960
|
+
"epi_after": float(node.EPI),
|
|
961
|
+
"vf_before": vf_before,
|
|
962
|
+
"vf_after": node.vf,
|
|
963
|
+
"phase_before": theta_before,
|
|
964
|
+
"phase_after": node.theta if hasattr(node, "theta") else None,
|
|
965
|
+
"phase_alignment_strengthened": phase_strengthened,
|
|
966
|
+
}
|
|
967
|
+
if "ra_metrics" not in node.graph:
|
|
968
|
+
node.graph["ra_metrics"] = []
|
|
969
|
+
node.graph["ra_metrics"].append(metrics)
|
|
970
|
+
|
|
971
|
+
# Track network C(t) after RA if enabled (optional telemetry)
|
|
972
|
+
if track_coherence and c_before is not None and hasattr(node, "G"):
|
|
973
|
+
try:
|
|
974
|
+
from ..metrics.coherence import compute_network_coherence
|
|
975
|
+
|
|
976
|
+
c_after = compute_network_coherence(node.G)
|
|
977
|
+
node.graph["_ra_c_tracking"].append(
|
|
978
|
+
{
|
|
979
|
+
"node": getattr(node, "n", None),
|
|
980
|
+
"c_before": c_before,
|
|
981
|
+
"c_after": c_after,
|
|
982
|
+
"c_delta": c_after - c_before,
|
|
983
|
+
}
|
|
984
|
+
)
|
|
985
|
+
except ImportError:
|
|
986
|
+
pass
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _op_SHA(node: NodeProtocol, gf: GlyphFactors) -> None: # SHA — Silence
|
|
990
|
+
"""Reduce νf while preserving EPI, ΔNFR, and phase.
|
|
991
|
+
|
|
992
|
+
Silence decelerates a node by scaling νf (structural frequency) towards
|
|
993
|
+
stillness. EPI, ΔNFR, and phase remain unchanged, signalling a temporary
|
|
994
|
+
suspension of structural evolution.
|
|
995
|
+
|
|
996
|
+
**TNFR Canonical Behavior:**
|
|
997
|
+
|
|
998
|
+
According to the nodal equation ∂EPI/∂t = νf · ΔNFR(t), reducing νf → νf_min ≈ 0
|
|
999
|
+
causes structural evolution to freeze (∂EPI/∂t → 0) regardless of ΔNFR magnitude.
|
|
1000
|
+
This implements **structural silence** - a state where the node's form (EPI) is
|
|
1001
|
+
preserved intact despite external pressures, enabling memory consolidation and
|
|
1002
|
+
protective latency.
|
|
1003
|
+
|
|
1004
|
+
Parameters
|
|
1005
|
+
----------
|
|
1006
|
+
node : NodeProtocol
|
|
1007
|
+
Node whose νf is being attenuated.
|
|
1008
|
+
gf : GlyphFactors
|
|
1009
|
+
Provides ``SHA_vf_factor`` to scale νf (default 0.85 for gradual reduction).
|
|
1010
|
+
|
|
1011
|
+
Examples
|
|
1012
|
+
--------
|
|
1013
|
+
>>> class MockNode:
|
|
1014
|
+
... def __init__(self, vf):
|
|
1015
|
+
... self.vf = vf
|
|
1016
|
+
>>> node = MockNode(1.0)
|
|
1017
|
+
>>> _op_SHA(node, {"SHA_vf_factor": 0.5})
|
|
1018
|
+
>>> node.vf
|
|
1019
|
+
0.5
|
|
1020
|
+
"""
|
|
278
1021
|
factor = get_factor(gf, "SHA_vf_factor", 0.85)
|
|
1022
|
+
# Canonical SHA effect: reduce structural frequency toward zero
|
|
1023
|
+
# This implements: νf → νf_min ≈ 0 ⇒ ∂EPI/∂t → 0 (structural preservation)
|
|
279
1024
|
node.vf = factor * node.vf
|
|
280
1025
|
|
|
281
1026
|
|
|
282
|
-
factor_val = 1.
|
|
1027
|
+
factor_val = 1.05 # Conservative scale prevents EPI overflow near boundaries
|
|
283
1028
|
factor_nul = 0.85
|
|
284
1029
|
_SCALE_FACTORS = {Glyph.VAL: factor_val, Glyph.NUL: factor_nul}
|
|
285
1030
|
|
|
286
1031
|
|
|
287
|
-
def
|
|
1032
|
+
def _set_epi_with_boundary_check(
|
|
1033
|
+
node: NodeProtocol, new_epi: float, *, apply_clip: bool = True
|
|
1034
|
+
) -> None:
|
|
1035
|
+
"""Canonical EPI assignment with structural boundary preservation.
|
|
1036
|
+
|
|
1037
|
+
This is the unified function all operators should use when modifying EPI
|
|
1038
|
+
to ensure structural boundaries are respected. Provides single point of
|
|
1039
|
+
enforcement for TNFR canonical invariant: EPI ∈ [EPI_MIN, EPI_MAX].
|
|
1040
|
+
|
|
1041
|
+
Parameters
|
|
1042
|
+
----------
|
|
1043
|
+
node : NodeProtocol
|
|
1044
|
+
Node whose EPI is being updated
|
|
1045
|
+
new_epi : float
|
|
1046
|
+
New EPI value to assign
|
|
1047
|
+
apply_clip : bool, default True
|
|
1048
|
+
If True, applies structural_clip to enforce boundaries.
|
|
1049
|
+
If False, assigns value directly (use only when boundaries
|
|
1050
|
+
are known to be satisfied, e.g., from edge-aware pre-computation).
|
|
1051
|
+
|
|
1052
|
+
Notes
|
|
1053
|
+
-----
|
|
1054
|
+
TNFR Principle: This function embodies the canonical invariant that EPI
|
|
1055
|
+
must remain within structural boundaries. All operator EPI modifications
|
|
1056
|
+
should flow through this function to maintain coherence.
|
|
1057
|
+
|
|
1058
|
+
The function uses the graph-level configuration for EPI_MIN, EPI_MAX,
|
|
1059
|
+
and CLIP_MODE to ensure consistent boundary enforcement across all operators.
|
|
1060
|
+
|
|
1061
|
+
Examples
|
|
1062
|
+
--------
|
|
1063
|
+
>>> class MockNode:
|
|
1064
|
+
... def __init__(self, epi):
|
|
1065
|
+
... self.EPI = epi
|
|
1066
|
+
... self.graph = {"EPI_MAX": 1.0, "EPI_MIN": -1.0}
|
|
1067
|
+
>>> node = MockNode(0.5)
|
|
1068
|
+
>>> _set_epi_with_boundary_check(node, 1.2) # Will be clipped to 1.0
|
|
1069
|
+
>>> float(node.EPI)
|
|
1070
|
+
1.0
|
|
1071
|
+
"""
|
|
1072
|
+
from ..dynamics.structural_clip import structural_clip
|
|
1073
|
+
|
|
1074
|
+
if not apply_clip:
|
|
1075
|
+
node.EPI = new_epi
|
|
1076
|
+
return
|
|
1077
|
+
|
|
1078
|
+
# Ensure new_epi is float (in case it's a BEPI or other structure)
|
|
1079
|
+
new_epi_float = float(new_epi)
|
|
1080
|
+
|
|
1081
|
+
# Get boundary configuration from graph (with defensive fallback)
|
|
1082
|
+
graph_attrs = getattr(node, "graph", {})
|
|
1083
|
+
epi_min = float(graph_attrs.get("EPI_MIN", DEFAULTS.get("EPI_MIN", -1.0)))
|
|
1084
|
+
epi_max = float(graph_attrs.get("EPI_MAX", DEFAULTS.get("EPI_MAX", 1.0)))
|
|
1085
|
+
clip_mode_str = str(graph_attrs.get("CLIP_MODE", "hard"))
|
|
1086
|
+
|
|
1087
|
+
# Validate clip mode
|
|
1088
|
+
if clip_mode_str not in ("hard", "soft"):
|
|
1089
|
+
clip_mode_str = "hard"
|
|
1090
|
+
|
|
1091
|
+
# Apply structural boundary preservation
|
|
1092
|
+
clipped_epi = structural_clip(
|
|
1093
|
+
new_epi_float,
|
|
1094
|
+
lo=epi_min,
|
|
1095
|
+
hi=epi_max,
|
|
1096
|
+
mode=clip_mode_str, # type: ignore[arg-type]
|
|
1097
|
+
record_stats=False,
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
node.EPI = clipped_epi
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def _compute_val_edge_aware_scale(
|
|
1104
|
+
epi_current: float, scale: float, epi_max: float, epsilon: float
|
|
1105
|
+
) -> float:
|
|
1106
|
+
"""Compute edge-aware scale factor for VAL (Expansion) operator.
|
|
1107
|
+
|
|
1108
|
+
Adapts the expansion scale to prevent EPI overflow beyond EPI_MAX.
|
|
1109
|
+
When EPI is near the upper boundary, the effective scale is reduced
|
|
1110
|
+
to ensure EPI * scale_eff <= EPI_MAX.
|
|
1111
|
+
|
|
1112
|
+
Parameters
|
|
1113
|
+
----------
|
|
1114
|
+
epi_current : float
|
|
1115
|
+
Current EPI value
|
|
1116
|
+
scale : float
|
|
1117
|
+
Desired expansion scale factor (e.g., VAL_scale = 1.05)
|
|
1118
|
+
epi_max : float
|
|
1119
|
+
Upper EPI boundary (typically 1.0)
|
|
1120
|
+
epsilon : float
|
|
1121
|
+
Small value to prevent division by zero (e.g., 1e-12)
|
|
1122
|
+
|
|
1123
|
+
Returns
|
|
1124
|
+
-------
|
|
1125
|
+
float
|
|
1126
|
+
Effective scale factor, adapted to respect EPI_MAX boundary
|
|
1127
|
+
|
|
1128
|
+
Notes
|
|
1129
|
+
-----
|
|
1130
|
+
TNFR Principle: This implements "resonance to the edge" - expansion
|
|
1131
|
+
scales adaptively to explore volume while respecting structural envelope.
|
|
1132
|
+
The adaptation is a dynamic compatibility check, not a fixed constant.
|
|
1133
|
+
|
|
1134
|
+
Examples
|
|
1135
|
+
--------
|
|
1136
|
+
>>> # Normal case: EPI far from boundary
|
|
1137
|
+
>>> _compute_val_edge_aware_scale(0.5, 1.05, 1.0, 1e-12)
|
|
1138
|
+
1.05
|
|
1139
|
+
|
|
1140
|
+
>>> # Edge case: EPI near boundary, scale adapts
|
|
1141
|
+
>>> scale = _compute_val_edge_aware_scale(0.96, 1.05, 1.0, 1e-12)
|
|
1142
|
+
>>> abs(scale - 1.0417) < 0.001 # Roughly 1.0/0.96
|
|
1143
|
+
True
|
|
1144
|
+
"""
|
|
1145
|
+
abs_epi = abs(epi_current)
|
|
1146
|
+
if abs_epi < epsilon:
|
|
1147
|
+
# EPI near zero, full scale can be applied safely
|
|
1148
|
+
return scale
|
|
1149
|
+
|
|
1150
|
+
# Compute maximum safe scale that keeps EPI within bounds
|
|
1151
|
+
max_safe_scale = epi_max / abs_epi
|
|
1152
|
+
|
|
1153
|
+
# Return the minimum of desired scale and safe scale
|
|
1154
|
+
return min(scale, max_safe_scale)
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def _compute_nul_edge_aware_scale(
|
|
1158
|
+
epi_current: float, scale: float, epi_min: float, epsilon: float
|
|
1159
|
+
) -> float:
|
|
1160
|
+
"""Compute edge-aware scale factor for NUL (Contraction) operator.
|
|
1161
|
+
|
|
1162
|
+
Adapts the contraction scale to prevent EPI underflow below EPI_MIN.
|
|
1163
|
+
|
|
1164
|
+
Parameters
|
|
1165
|
+
----------
|
|
1166
|
+
epi_current : float
|
|
1167
|
+
Current EPI value
|
|
1168
|
+
scale : float
|
|
1169
|
+
Desired contraction scale factor (e.g., NUL_scale = 0.85)
|
|
1170
|
+
epi_min : float
|
|
1171
|
+
Lower EPI boundary (typically -1.0)
|
|
1172
|
+
epsilon : float
|
|
1173
|
+
Small value to prevent division by zero (e.g., 1e-12)
|
|
1174
|
+
|
|
1175
|
+
Returns
|
|
1176
|
+
-------
|
|
1177
|
+
float
|
|
1178
|
+
Effective scale factor, adapted to respect EPI_MIN boundary
|
|
1179
|
+
|
|
1180
|
+
Notes
|
|
1181
|
+
-----
|
|
1182
|
+
TNFR Principle: Contraction concentrates structure toward core while
|
|
1183
|
+
maintaining coherence.
|
|
1184
|
+
|
|
1185
|
+
For typical NUL_scale < 1.0, contraction naturally moves EPI toward zero
|
|
1186
|
+
(the center), which is always safe regardless of whether EPI is positive
|
|
1187
|
+
or negative. Edge-awareness is only needed if scale could somehow push
|
|
1188
|
+
EPI beyond boundaries.
|
|
1189
|
+
|
|
1190
|
+
In practice, with NUL_scale = 0.85 < 1.0:
|
|
1191
|
+
- Positive EPI contracts toward zero: safe
|
|
1192
|
+
- Negative EPI contracts toward zero: safe
|
|
1193
|
+
|
|
1194
|
+
Edge-awareness is provided for completeness and future extensibility.
|
|
1195
|
+
|
|
1196
|
+
Examples
|
|
1197
|
+
--------
|
|
1198
|
+
>>> # Normal contraction (always safe with scale < 1.0)
|
|
1199
|
+
>>> _compute_nul_edge_aware_scale(0.5, 0.85, -1.0, 1e-12)
|
|
1200
|
+
0.85
|
|
1201
|
+
>>> _compute_nul_edge_aware_scale(-0.5, 0.85, -1.0, 1e-12)
|
|
1202
|
+
0.85
|
|
1203
|
+
"""
|
|
1204
|
+
# With NUL_scale < 1.0, contraction moves toward zero (always safe)
|
|
1205
|
+
# No adaptation needed in typical case
|
|
1206
|
+
return scale
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
def _op_scale(node: NodeProtocol, factor: float) -> None:
|
|
1210
|
+
"""Scale νf with the provided factor.
|
|
1211
|
+
|
|
1212
|
+
Parameters
|
|
1213
|
+
----------
|
|
1214
|
+
node : NodeProtocol
|
|
1215
|
+
Node whose νf is being updated.
|
|
1216
|
+
factor : float
|
|
1217
|
+
Multiplicative change applied to νf.
|
|
1218
|
+
"""
|
|
288
1219
|
node.vf *= factor
|
|
289
1220
|
|
|
290
1221
|
|
|
291
|
-
def _make_scale_op(glyph: Glyph):
|
|
292
|
-
def _op(node:
|
|
1222
|
+
def _make_scale_op(glyph: Glyph) -> GlyphOperation:
|
|
1223
|
+
def _op(node: NodeProtocol, gf: GlyphFactors) -> None:
|
|
293
1224
|
key = "VAL_scale" if glyph is Glyph.VAL else "NUL_scale"
|
|
294
1225
|
default = _SCALE_FACTORS[glyph]
|
|
295
1226
|
factor = get_factor(gf, key, default)
|
|
1227
|
+
|
|
1228
|
+
# Always scale νf (existing behavior)
|
|
296
1229
|
_op_scale(node, factor)
|
|
297
1230
|
|
|
1231
|
+
# NUL canonical ΔNFR densification (implements structural pressure concentration)
|
|
1232
|
+
if glyph is Glyph.NUL:
|
|
1233
|
+
# Volume reduction: V' = V · scale_factor (where scale_factor < 1.0)
|
|
1234
|
+
# Density increase: ρ_ΔNFR = ΔNFR / V' = ΔNFR / (V · scale_factor)
|
|
1235
|
+
# Result: ΔNFR' = ΔNFR · densification_factor
|
|
1236
|
+
#
|
|
1237
|
+
# Physics: When volume contracts by factor λ < 1, structural pressure
|
|
1238
|
+
# concentrates by factor 1/λ > 1. For NUL_scale = 0.85, densification ≈ 1.176
|
|
1239
|
+
#
|
|
1240
|
+
# Default densification_factor from config (typically 1.3-1.5) provides
|
|
1241
|
+
# additional canonical amplification beyond geometric 1/λ to account for
|
|
1242
|
+
# nonlinear structural effects at smaller scales.
|
|
1243
|
+
densification_key = "NUL_densification_factor"
|
|
1244
|
+
densification_default = 1.35 # Canonical default: moderate amplification
|
|
1245
|
+
densification_factor = get_factor(gf, densification_key, densification_default)
|
|
1246
|
+
|
|
1247
|
+
# Apply densification to ΔNFR (use lowercase dnfr for NodeProtocol)
|
|
1248
|
+
current_dnfr = node.dnfr
|
|
1249
|
+
node.dnfr = current_dnfr * densification_factor
|
|
1250
|
+
|
|
1251
|
+
# Record densification telemetry for traceability
|
|
1252
|
+
telemetry = node.graph.setdefault("nul_densification_log", [])
|
|
1253
|
+
telemetry.append(
|
|
1254
|
+
{
|
|
1255
|
+
"dnfr_before": current_dnfr,
|
|
1256
|
+
"dnfr_after": float(node.dnfr),
|
|
1257
|
+
"densification_factor": densification_factor,
|
|
1258
|
+
"contraction_scale": factor,
|
|
1259
|
+
}
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
# Edge-aware EPI scaling (new behavior) if enabled
|
|
1263
|
+
edge_aware_enabled = bool(
|
|
1264
|
+
node.graph.get(
|
|
1265
|
+
"EDGE_AWARE_ENABLED", DEFAULTS.get("EDGE_AWARE_ENABLED", True)
|
|
1266
|
+
)
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
if edge_aware_enabled:
|
|
1270
|
+
epsilon = float(
|
|
1271
|
+
node.graph.get(
|
|
1272
|
+
"EDGE_AWARE_EPSILON", DEFAULTS.get("EDGE_AWARE_EPSILON", 1e-12)
|
|
1273
|
+
)
|
|
1274
|
+
)
|
|
1275
|
+
epi_min = float(node.graph.get("EPI_MIN", DEFAULTS.get("EPI_MIN", -1.0)))
|
|
1276
|
+
epi_max = float(node.graph.get("EPI_MAX", DEFAULTS.get("EPI_MAX", 1.0)))
|
|
1277
|
+
|
|
1278
|
+
epi_current = node.EPI
|
|
1279
|
+
|
|
1280
|
+
# Compute edge-aware scale factor
|
|
1281
|
+
if glyph is Glyph.VAL:
|
|
1282
|
+
scale_eff = _compute_val_edge_aware_scale(
|
|
1283
|
+
epi_current, factor, epi_max, epsilon
|
|
1284
|
+
)
|
|
1285
|
+
else: # Glyph.NUL
|
|
1286
|
+
scale_eff = _compute_nul_edge_aware_scale(
|
|
1287
|
+
epi_current, factor, epi_min, epsilon
|
|
1288
|
+
)
|
|
1289
|
+
|
|
1290
|
+
# Apply edge-aware EPI scaling with boundary check
|
|
1291
|
+
# Edge-aware already computed safe scale, but use unified function
|
|
1292
|
+
# for consistency (with apply_clip=True as safety net)
|
|
1293
|
+
new_epi = epi_current * scale_eff
|
|
1294
|
+
_set_epi_with_boundary_check(node, new_epi, apply_clip=True)
|
|
1295
|
+
|
|
1296
|
+
# Record telemetry if scale was adapted
|
|
1297
|
+
if abs(scale_eff - factor) > epsilon:
|
|
1298
|
+
telemetry = node.graph.setdefault("edge_aware_interventions", [])
|
|
1299
|
+
telemetry.append(
|
|
1300
|
+
{
|
|
1301
|
+
"glyph": glyph.name if hasattr(glyph, "name") else str(glyph),
|
|
1302
|
+
"epi_before": epi_current,
|
|
1303
|
+
"epi_after": float(
|
|
1304
|
+
node.EPI
|
|
1305
|
+
), # Get actual value after boundary check
|
|
1306
|
+
"scale_requested": factor,
|
|
1307
|
+
"scale_effective": scale_eff,
|
|
1308
|
+
"adapted": True,
|
|
1309
|
+
}
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
_op.__doc__ = """{} glyph scales νf and EPI with edge-aware adaptation.
|
|
1313
|
+
|
|
1314
|
+
VAL (expansion) increases νf and EPI, whereas NUL (contraction) decreases them.
|
|
1315
|
+
Edge-aware scaling adapts the scale factor near EPI boundaries to prevent
|
|
1316
|
+
overflow/underflow, maintaining structural coherence within [-1.0, 1.0].
|
|
1317
|
+
|
|
1318
|
+
When EDGE_AWARE_ENABLED is True (default), the effective scale is computed as:
|
|
1319
|
+
- VAL: scale_eff = min(VAL_scale, EPI_MAX / |EPI_current|)
|
|
1320
|
+
- NUL: scale_eff = min(NUL_scale, |EPI_MIN| / |EPI_current|) for negative EPI
|
|
1321
|
+
|
|
1322
|
+
This implements TNFR principle: "resonance to the edge" without breaking
|
|
1323
|
+
the structural envelope. Telemetry records adaptation events.
|
|
1324
|
+
|
|
1325
|
+
Parameters
|
|
1326
|
+
----------
|
|
1327
|
+
node : NodeProtocol
|
|
1328
|
+
Node whose νf and EPI are updated.
|
|
1329
|
+
gf : GlyphFactors
|
|
1330
|
+
Provides the respective scale factor (``VAL_scale`` or
|
|
1331
|
+
``NUL_scale``).
|
|
1332
|
+
|
|
1333
|
+
Examples
|
|
1334
|
+
--------
|
|
1335
|
+
>>> class MockNode:
|
|
1336
|
+
... def __init__(self, vf, epi):
|
|
1337
|
+
... self.vf = vf
|
|
1338
|
+
... self.EPI = epi
|
|
1339
|
+
... self.graph = {{"EDGE_AWARE_ENABLED": True, "EPI_MAX": 1.0}}
|
|
1340
|
+
>>> node = MockNode(1.0, 0.96)
|
|
1341
|
+
>>> op = _make_scale_op(Glyph.VAL)
|
|
1342
|
+
>>> op(node, {{"VAL_scale": 1.05}})
|
|
1343
|
+
>>> node.vf # νf scaled normally
|
|
1344
|
+
1.05
|
|
1345
|
+
>>> node.EPI <= 1.0 # EPI kept within bounds
|
|
1346
|
+
True
|
|
1347
|
+
""".format(
|
|
1348
|
+
glyph.name
|
|
1349
|
+
)
|
|
298
1350
|
return _op
|
|
299
1351
|
|
|
300
1352
|
|
|
301
|
-
def _op_THOL(
|
|
302
|
-
|
|
303
|
-
|
|
1353
|
+
def _op_THOL(node: NodeProtocol, gf: GlyphFactors) -> None: # THOL — Self-organization
|
|
1354
|
+
"""Inject curvature from ``d2EPI`` into ΔNFR to trigger self-organization.
|
|
1355
|
+
|
|
1356
|
+
The glyph keeps EPI, νf, and phase fixed while increasing ΔNFR according to
|
|
1357
|
+
the second derivative of EPI, accelerating structural rearrangement.
|
|
1358
|
+
|
|
1359
|
+
Parameters
|
|
1360
|
+
----------
|
|
1361
|
+
node : NodeProtocol
|
|
1362
|
+
Node contributing ``d2EPI`` to ΔNFR.
|
|
1363
|
+
gf : GlyphFactors
|
|
1364
|
+
Source of the ``THOL_accel`` multiplier.
|
|
1365
|
+
|
|
1366
|
+
Examples
|
|
1367
|
+
--------
|
|
1368
|
+
>>> class MockNode:
|
|
1369
|
+
... def __init__(self, dnfr, curvature):
|
|
1370
|
+
... self.dnfr = dnfr
|
|
1371
|
+
... self.d2EPI = curvature
|
|
1372
|
+
>>> node = MockNode(0.1, 0.5)
|
|
1373
|
+
>>> _op_THOL(node, {"THOL_accel": 0.2})
|
|
1374
|
+
>>> node.dnfr
|
|
1375
|
+
0.2
|
|
1376
|
+
"""
|
|
304
1377
|
a = get_factor(gf, "THOL_accel", 0.10)
|
|
305
1378
|
node.dnfr = node.dnfr + a * getattr(node, "d2EPI", 0.0)
|
|
306
1379
|
|
|
307
1380
|
|
|
308
|
-
def _op_ZHIR(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
node
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
1381
|
+
def _op_ZHIR(node: NodeProtocol, gf: GlyphFactors) -> None: # ZHIR — Mutation
|
|
1382
|
+
"""Apply canonical phase transformation θ → θ' based on structural dynamics.
|
|
1383
|
+
|
|
1384
|
+
ZHIR (Mutation) implements the canonical TNFR phase transformation that depends on
|
|
1385
|
+
the node's reorganization state (ΔNFR). Unlike a fixed rotation, the transformation
|
|
1386
|
+
magnitude and direction are determined by structural pressure, implementing the
|
|
1387
|
+
physics: θ → θ' when ΔEPI/Δt > ξ (AGENTS.md §11, TNFR.pdf §2.2.11).
|
|
1388
|
+
|
|
1389
|
+
**Canonical Behavior**:
|
|
1390
|
+
- Direction: Based on ΔNFR sign (positive → forward phase, negative → backward)
|
|
1391
|
+
- Magnitude: Proportional to theta_shift_factor and |ΔNFR|
|
|
1392
|
+
- Regime detection: Identifies quadrant crossings (π/2 boundaries)
|
|
1393
|
+
- Deterministic: Same seed produces same transformation
|
|
1394
|
+
|
|
1395
|
+
The transformation preserves structural identity (epi_kind) while shifting the
|
|
1396
|
+
operational regime, enabling adaptation without losing coherence.
|
|
1397
|
+
|
|
1398
|
+
Parameters
|
|
1399
|
+
----------
|
|
1400
|
+
node : NodeProtocol
|
|
1401
|
+
Node whose phase is transformed based on its structural state.
|
|
1402
|
+
gf : GlyphFactors
|
|
1403
|
+
Supplies ``ZHIR_theta_shift_factor`` (default: 0.3) controlling transformation
|
|
1404
|
+
magnitude. Can override with explicit ``ZHIR_theta_shift`` for fixed rotation.
|
|
1405
|
+
|
|
1406
|
+
Examples
|
|
1407
|
+
--------
|
|
1408
|
+
>>> import math
|
|
1409
|
+
>>> class MockNode:
|
|
1410
|
+
... def __init__(self, theta, dnfr):
|
|
1411
|
+
... self.theta = theta
|
|
1412
|
+
... self.dnfr = dnfr
|
|
1413
|
+
... self.graph = {}
|
|
1414
|
+
>>> # Positive ΔNFR → forward phase shift
|
|
1415
|
+
>>> node = MockNode(0.0, 0.5)
|
|
1416
|
+
>>> _op_ZHIR(node, {"ZHIR_theta_shift_factor": 0.3})
|
|
1417
|
+
>>> 0.2 < node.theta < 0.3 # ~π/4 * 0.3 ≈ 0.24
|
|
1418
|
+
True
|
|
1419
|
+
>>> # Negative ΔNFR → backward phase shift
|
|
1420
|
+
>>> node2 = MockNode(math.pi, -0.5)
|
|
1421
|
+
>>> _op_ZHIR(node2, {"ZHIR_theta_shift_factor": 0.3})
|
|
1422
|
+
>>> 2.9 < node2.theta < 3.0 # π - 0.24 ≈ 2.90
|
|
1423
|
+
True
|
|
1424
|
+
>>> # Fixed shift overrides dynamic behavior
|
|
1425
|
+
>>> node3 = MockNode(0.0, 0.5)
|
|
1426
|
+
>>> _op_ZHIR(node3, {"ZHIR_theta_shift": math.pi / 2})
|
|
1427
|
+
>>> round(node3.theta, 2)
|
|
1428
|
+
1.57
|
|
1429
|
+
"""
|
|
1430
|
+
# Check for explicit fixed shift (backward compatibility)
|
|
1431
|
+
if "ZHIR_theta_shift" in gf:
|
|
1432
|
+
shift = get_factor(gf, "ZHIR_theta_shift", math.pi / 2)
|
|
1433
|
+
node.theta = node.theta + shift
|
|
1434
|
+
# Store telemetry for fixed shift mode
|
|
1435
|
+
storage = node._glyph_storage()
|
|
1436
|
+
storage["_zhir_theta_shift"] = shift
|
|
1437
|
+
storage["_zhir_fixed_mode"] = True
|
|
1438
|
+
return
|
|
1439
|
+
|
|
1440
|
+
# Canonical transformation: θ → θ' based on ΔNFR
|
|
1441
|
+
theta_before = node.theta
|
|
1442
|
+
dnfr = node.dnfr
|
|
1443
|
+
|
|
1444
|
+
# Transformation magnitude controlled by factor
|
|
1445
|
+
theta_shift_factor = get_factor(gf, "ZHIR_theta_shift_factor", 0.3)
|
|
1446
|
+
|
|
1447
|
+
# Direction based on ΔNFR sign (coherent with structural pressure)
|
|
1448
|
+
# Magnitude based on |ΔNFR| (stronger pressure → larger shift)
|
|
1449
|
+
# Base shift is π/4, scaled by factor and ΔNFR
|
|
1450
|
+
base_shift = math.pi / 4
|
|
1451
|
+
shift = theta_shift_factor * math.copysign(1.0, dnfr) * base_shift
|
|
1452
|
+
|
|
1453
|
+
# Apply transformation with phase wrapping [0, 2π)
|
|
1454
|
+
theta_new = (theta_before + shift) % (2 * math.pi)
|
|
1455
|
+
node.theta = theta_new
|
|
1456
|
+
|
|
1457
|
+
# Detect regime change (crossing quadrant boundaries)
|
|
1458
|
+
regime_before = int(theta_before // (math.pi / 2))
|
|
1459
|
+
regime_after = int(theta_new // (math.pi / 2))
|
|
1460
|
+
regime_changed = regime_before != regime_after
|
|
1461
|
+
|
|
1462
|
+
# Store telemetry for metrics collection
|
|
1463
|
+
storage = node._glyph_storage()
|
|
1464
|
+
storage["_zhir_theta_shift"] = shift
|
|
1465
|
+
storage["_zhir_theta_before"] = theta_before
|
|
1466
|
+
storage["_zhir_theta_after"] = theta_new
|
|
1467
|
+
storage["_zhir_regime_changed"] = regime_changed
|
|
1468
|
+
storage["_zhir_regime_before"] = regime_before
|
|
1469
|
+
storage["_zhir_regime_after"] = regime_after
|
|
1470
|
+
storage["_zhir_fixed_mode"] = False
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def _op_NAV(node: NodeProtocol, gf: GlyphFactors) -> None: # NAV — Transition
|
|
1474
|
+
"""Rebalance ΔNFR towards νf while permitting jitter.
|
|
1475
|
+
|
|
1476
|
+
Transition pulls ΔNFR towards a νf-aligned target, optionally adding jitter
|
|
1477
|
+
to explore nearby states. EPI and phase remain untouched; νf may be used as
|
|
1478
|
+
a reference but is not directly changed.
|
|
1479
|
+
|
|
1480
|
+
Parameters
|
|
1481
|
+
----------
|
|
1482
|
+
node : NodeProtocol
|
|
1483
|
+
Node whose ΔNFR is redirected.
|
|
1484
|
+
gf : GlyphFactors
|
|
1485
|
+
Supplies ``NAV_eta`` and ``NAV_jitter`` tuning parameters.
|
|
1486
|
+
|
|
1487
|
+
Examples
|
|
1488
|
+
--------
|
|
1489
|
+
>>> class MockNode:
|
|
1490
|
+
... def __init__(self, dnfr, vf):
|
|
1491
|
+
... self.dnfr = dnfr
|
|
1492
|
+
... self.vf = vf
|
|
1493
|
+
... self.graph = {"NAV_RANDOM": False}
|
|
1494
|
+
>>> node = MockNode(-0.6, 0.4)
|
|
1495
|
+
>>> _op_NAV(node, {"NAV_eta": 0.5, "NAV_jitter": 0.0})
|
|
1496
|
+
>>> round(node.dnfr, 2)
|
|
1497
|
+
-0.1
|
|
1498
|
+
"""
|
|
318
1499
|
dnfr = node.dnfr
|
|
319
1500
|
vf = node.vf
|
|
320
1501
|
eta = get_factor(gf, "NAV_eta", 0.5)
|
|
@@ -334,14 +1515,37 @@ def _op_NAV(
|
|
|
334
1515
|
|
|
335
1516
|
|
|
336
1517
|
def _op_REMESH(
|
|
337
|
-
node:
|
|
338
|
-
) -> None: # REMESH —
|
|
1518
|
+
node: NodeProtocol, gf: GlyphFactors | None = None
|
|
1519
|
+
) -> None: # REMESH — advisory
|
|
1520
|
+
"""Record an advisory requesting network-scale remeshing.
|
|
1521
|
+
|
|
1522
|
+
REMESH does not change node-level EPI, νf, ΔNFR, or phase. Instead it
|
|
1523
|
+
annotates the glyph history so orchestrators can trigger global remesh
|
|
1524
|
+
procedures once the stability conditions are met.
|
|
1525
|
+
|
|
1526
|
+
Parameters
|
|
1527
|
+
----------
|
|
1528
|
+
node : NodeProtocol
|
|
1529
|
+
Node whose history records the advisory.
|
|
1530
|
+
gf : GlyphFactors, optional
|
|
1531
|
+
Unused but accepted for API symmetry.
|
|
1532
|
+
|
|
1533
|
+
Examples
|
|
1534
|
+
--------
|
|
1535
|
+
>>> class MockNode:
|
|
1536
|
+
... def __init__(self):
|
|
1537
|
+
... self.graph = {}
|
|
1538
|
+
>>> node = MockNode()
|
|
1539
|
+
>>> _op_REMESH(node)
|
|
1540
|
+
>>> "_remesh_warn_step" in node.graph
|
|
1541
|
+
True
|
|
1542
|
+
"""
|
|
339
1543
|
step_idx = glyph_history.current_step_idx(node)
|
|
340
1544
|
last_warn = node.graph.get("_remesh_warn_step", None)
|
|
341
1545
|
if last_warn != step_idx:
|
|
342
1546
|
msg = (
|
|
343
|
-
"REMESH
|
|
344
|
-
"stable(G)
|
|
1547
|
+
"REMESH operates at network scale. Use apply_remesh_if_globally_"
|
|
1548
|
+
"stable(G) or apply_network_remesh(G)."
|
|
345
1549
|
)
|
|
346
1550
|
hist = glyph_history.ensure_history(node)
|
|
347
1551
|
glyph_history.append_metric(
|
|
@@ -357,7 +1561,7 @@ def _op_REMESH(
|
|
|
357
1561
|
# Dispatcher
|
|
358
1562
|
# -------------------------
|
|
359
1563
|
|
|
360
|
-
GLYPH_OPERATIONS: dict[Glyph,
|
|
1564
|
+
GLYPH_OPERATIONS: dict[Glyph, GlyphOperation] = {
|
|
361
1565
|
Glyph.AL: _op_AL,
|
|
362
1566
|
Glyph.EN: _op_EN,
|
|
363
1567
|
Glyph.IL: _op_IL,
|
|
@@ -375,13 +1579,25 @@ GLYPH_OPERATIONS: dict[Glyph, Callable[["NodoProtocol", dict[str, Any]], None]]
|
|
|
375
1579
|
|
|
376
1580
|
|
|
377
1581
|
def apply_glyph_obj(
|
|
378
|
-
node:
|
|
1582
|
+
node: NodeProtocol, glyph: Glyph | str, *, window: int | None = None
|
|
379
1583
|
) -> None:
|
|
380
|
-
"""Apply ``glyph`` to an object satisfying :class:`
|
|
1584
|
+
"""Apply ``glyph`` to an object satisfying :class:`NodeProtocol`."""
|
|
381
1585
|
|
|
1586
|
+
from .grammar import function_name_to_glyph
|
|
1587
|
+
from ..validation.input_validation import ValidationError, validate_glyph
|
|
1588
|
+
|
|
1589
|
+
# Validate glyph parameter
|
|
382
1590
|
try:
|
|
383
|
-
|
|
384
|
-
|
|
1591
|
+
if not isinstance(glyph, Glyph):
|
|
1592
|
+
validated_glyph = validate_glyph(glyph)
|
|
1593
|
+
glyph = (
|
|
1594
|
+
validated_glyph.value
|
|
1595
|
+
if isinstance(validated_glyph, Glyph)
|
|
1596
|
+
else str(glyph)
|
|
1597
|
+
)
|
|
1598
|
+
else:
|
|
1599
|
+
glyph = glyph.value
|
|
1600
|
+
except ValidationError as e:
|
|
385
1601
|
step_idx = glyph_history.current_step_idx(node)
|
|
386
1602
|
hist = glyph_history.ensure_history(node)
|
|
387
1603
|
glyph_history.append_metric(
|
|
@@ -392,15 +1608,38 @@ def apply_glyph_obj(
|
|
|
392
1608
|
{
|
|
393
1609
|
"step": step_idx,
|
|
394
1610
|
"node": getattr(node, "n", None),
|
|
395
|
-
"msg": f"glyph
|
|
1611
|
+
"msg": f"invalid glyph: {e}",
|
|
396
1612
|
},
|
|
397
1613
|
),
|
|
398
1614
|
)
|
|
399
|
-
raise ValueError(f"glyph
|
|
1615
|
+
raise ValueError(f"invalid glyph: {e}") from e
|
|
1616
|
+
|
|
1617
|
+
# Try direct glyph code first
|
|
1618
|
+
try:
|
|
1619
|
+
g = Glyph(str(glyph))
|
|
1620
|
+
except ValueError:
|
|
1621
|
+
# Try structural function name mapping
|
|
1622
|
+
g = function_name_to_glyph(glyph)
|
|
1623
|
+
if g is None:
|
|
1624
|
+
step_idx = glyph_history.current_step_idx(node)
|
|
1625
|
+
hist = glyph_history.ensure_history(node)
|
|
1626
|
+
glyph_history.append_metric(
|
|
1627
|
+
hist,
|
|
1628
|
+
"events",
|
|
1629
|
+
(
|
|
1630
|
+
"warn",
|
|
1631
|
+
{
|
|
1632
|
+
"step": step_idx,
|
|
1633
|
+
"node": getattr(node, "n", None),
|
|
1634
|
+
"msg": f"unknown glyph: {glyph}",
|
|
1635
|
+
},
|
|
1636
|
+
),
|
|
1637
|
+
)
|
|
1638
|
+
raise ValueError(f"unknown glyph: {glyph}")
|
|
400
1639
|
|
|
401
1640
|
op = GLYPH_OPERATIONS.get(g)
|
|
402
1641
|
if op is None:
|
|
403
|
-
raise ValueError(f"glyph
|
|
1642
|
+
raise ValueError(f"glyph has no registered operator: {g}")
|
|
404
1643
|
if window is None:
|
|
405
1644
|
window = int(get_param(node, "GLYPH_HYSTERESIS_WINDOW"))
|
|
406
1645
|
gf = get_glyph_factors(node)
|
|
@@ -410,11 +1649,24 @@ def apply_glyph_obj(
|
|
|
410
1649
|
|
|
411
1650
|
|
|
412
1651
|
def apply_glyph(
|
|
413
|
-
G, n, glyph: Glyph | str, *, window: int | None = None
|
|
1652
|
+
G: TNFRGraph, n: NodeId, glyph: Glyph | str, *, window: int | None = None
|
|
414
1653
|
) -> None:
|
|
415
1654
|
"""Adapter to operate on ``networkx`` graphs."""
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
1655
|
+
from ..validation.input_validation import (
|
|
1656
|
+
ValidationError,
|
|
1657
|
+
validate_node_id,
|
|
1658
|
+
validate_tnfr_graph,
|
|
1659
|
+
)
|
|
1660
|
+
|
|
1661
|
+
# Validate graph and node parameters
|
|
1662
|
+
try:
|
|
1663
|
+
validate_tnfr_graph(G)
|
|
1664
|
+
validate_node_id(n)
|
|
1665
|
+
except ValidationError as e:
|
|
1666
|
+
raise ValueError(f"Invalid parameters for apply_glyph: {e}") from e
|
|
1667
|
+
|
|
1668
|
+
NodeNX = get_nodenx()
|
|
1669
|
+
if NodeNX is None:
|
|
1670
|
+
raise ImportError("NodeNX is unavailable")
|
|
1671
|
+
node = NodeNX(G, n)
|
|
420
1672
|
apply_glyph_obj(node, glyph, window=window)
|