tnfr 3.0.3__py3-none-any.whl → 8.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tnfr might be problematic. Click here for more details.
- tnfr/__init__.py +375 -56
- tnfr/__init__.pyi +33 -0
- tnfr/_compat.py +10 -0
- tnfr/_generated_version.py +34 -0
- tnfr/_version.py +49 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +723 -0
- tnfr/alias.pyi +108 -0
- tnfr/backends/__init__.py +354 -0
- tnfr/backends/jax_backend.py +173 -0
- tnfr/backends/numpy_backend.py +238 -0
- tnfr/backends/optimized_numpy.py +420 -0
- tnfr/backends/torch_backend.py +408 -0
- tnfr/cache.py +171 -0
- tnfr/cache.pyi +13 -0
- tnfr/cli/__init__.py +110 -0
- tnfr/cli/__init__.pyi +26 -0
- tnfr/cli/arguments.py +489 -0
- tnfr/cli/arguments.pyi +29 -0
- tnfr/cli/execution.py +914 -0
- tnfr/cli/execution.pyi +70 -0
- tnfr/cli/interactive_validator.py +614 -0
- tnfr/cli/utils.py +51 -0
- tnfr/cli/utils.pyi +7 -0
- tnfr/cli/validate.py +236 -0
- tnfr/compat/__init__.py +85 -0
- tnfr/compat/dataclass.py +136 -0
- tnfr/compat/jsonschema_stub.py +61 -0
- tnfr/compat/matplotlib_stub.py +73 -0
- tnfr/compat/numpy_stub.py +155 -0
- tnfr/config/__init__.py +224 -0
- tnfr/config/__init__.pyi +10 -0
- tnfr/config/constants.py +104 -0
- tnfr/config/constants.pyi +12 -0
- tnfr/config/defaults.py +54 -0
- tnfr/config/defaults_core.py +212 -0
- tnfr/config/defaults_init.py +33 -0
- tnfr/config/defaults_metric.py +104 -0
- tnfr/config/feature_flags.py +81 -0
- tnfr/config/feature_flags.pyi +16 -0
- tnfr/config/glyph_constants.py +31 -0
- tnfr/config/init.py +77 -0
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +254 -0
- tnfr/config/operator_names.pyi +36 -0
- tnfr/config/physics_derivation.py +354 -0
- tnfr/config/presets.py +83 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/config/security.py +927 -0
- tnfr/config/thresholds.py +114 -0
- tnfr/config/tnfr_config.py +498 -0
- tnfr/constants/__init__.py +92 -0
- tnfr/constants/__init__.pyi +92 -0
- tnfr/constants/aliases.py +33 -0
- tnfr/constants/aliases.pyi +27 -0
- tnfr/constants/init.py +33 -0
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +104 -0
- tnfr/constants/metric.pyi +19 -0
- tnfr/core/__init__.py +33 -0
- tnfr/core/container.py +226 -0
- tnfr/core/default_implementations.py +329 -0
- tnfr/core/interfaces.py +279 -0
- tnfr/dynamics/__init__.py +238 -0
- tnfr/dynamics/__init__.pyi +83 -0
- tnfr/dynamics/adaptation.py +267 -0
- tnfr/dynamics/adaptation.pyi +7 -0
- tnfr/dynamics/adaptive_sequences.py +189 -0
- tnfr/dynamics/adaptive_sequences.pyi +14 -0
- tnfr/dynamics/aliases.py +23 -0
- tnfr/dynamics/aliases.pyi +19 -0
- tnfr/dynamics/bifurcation.py +232 -0
- tnfr/dynamics/canonical.py +229 -0
- tnfr/dynamics/canonical.pyi +48 -0
- tnfr/dynamics/coordination.py +385 -0
- tnfr/dynamics/coordination.pyi +25 -0
- tnfr/dynamics/dnfr.py +3034 -0
- tnfr/dynamics/dnfr.pyi +26 -0
- tnfr/dynamics/dynamic_limits.py +225 -0
- tnfr/dynamics/feedback.py +252 -0
- tnfr/dynamics/feedback.pyi +24 -0
- tnfr/dynamics/fused_dnfr.py +454 -0
- tnfr/dynamics/homeostasis.py +157 -0
- tnfr/dynamics/homeostasis.pyi +14 -0
- tnfr/dynamics/integrators.py +661 -0
- tnfr/dynamics/integrators.pyi +36 -0
- tnfr/dynamics/learning.py +310 -0
- tnfr/dynamics/learning.pyi +33 -0
- tnfr/dynamics/metabolism.py +254 -0
- tnfr/dynamics/nbody.py +796 -0
- tnfr/dynamics/nbody_tnfr.py +783 -0
- tnfr/dynamics/propagation.py +326 -0
- tnfr/dynamics/runtime.py +908 -0
- tnfr/dynamics/runtime.pyi +77 -0
- tnfr/dynamics/sampling.py +36 -0
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +711 -0
- tnfr/dynamics/selectors.pyi +85 -0
- tnfr/dynamics/structural_clip.py +207 -0
- tnfr/errors/__init__.py +37 -0
- tnfr/errors/contextual.py +492 -0
- tnfr/execution.py +223 -0
- tnfr/execution.pyi +45 -0
- tnfr/extensions/__init__.py +205 -0
- tnfr/extensions/__init__.pyi +18 -0
- tnfr/extensions/base.py +173 -0
- tnfr/extensions/base.pyi +35 -0
- tnfr/extensions/business/__init__.py +71 -0
- tnfr/extensions/business/__init__.pyi +11 -0
- tnfr/extensions/business/cookbook.py +88 -0
- tnfr/extensions/business/cookbook.pyi +8 -0
- tnfr/extensions/business/health_analyzers.py +202 -0
- tnfr/extensions/business/health_analyzers.pyi +9 -0
- tnfr/extensions/business/patterns.py +183 -0
- tnfr/extensions/business/patterns.pyi +8 -0
- tnfr/extensions/medical/__init__.py +73 -0
- tnfr/extensions/medical/__init__.pyi +11 -0
- tnfr/extensions/medical/cookbook.py +88 -0
- tnfr/extensions/medical/cookbook.pyi +8 -0
- tnfr/extensions/medical/health_analyzers.py +181 -0
- tnfr/extensions/medical/health_analyzers.pyi +9 -0
- tnfr/extensions/medical/patterns.py +163 -0
- tnfr/extensions/medical/patterns.pyi +8 -0
- tnfr/flatten.py +262 -0
- tnfr/flatten.pyi +21 -0
- tnfr/gamma.py +354 -0
- tnfr/gamma.pyi +36 -0
- tnfr/glyph_history.py +377 -0
- tnfr/glyph_history.pyi +35 -0
- tnfr/glyph_runtime.py +19 -0
- tnfr/glyph_runtime.pyi +8 -0
- tnfr/immutable.py +218 -0
- tnfr/immutable.pyi +36 -0
- tnfr/initialization.py +203 -0
- tnfr/initialization.pyi +65 -0
- tnfr/io.py +10 -0
- tnfr/io.pyi +13 -0
- tnfr/locking.py +37 -0
- tnfr/locking.pyi +7 -0
- tnfr/mathematics/__init__.py +79 -0
- tnfr/mathematics/backend.py +453 -0
- tnfr/mathematics/backend.pyi +99 -0
- tnfr/mathematics/dynamics.py +408 -0
- tnfr/mathematics/dynamics.pyi +90 -0
- tnfr/mathematics/epi.py +391 -0
- tnfr/mathematics/epi.pyi +65 -0
- tnfr/mathematics/generators.py +242 -0
- tnfr/mathematics/generators.pyi +29 -0
- tnfr/mathematics/metrics.py +119 -0
- tnfr/mathematics/metrics.pyi +16 -0
- tnfr/mathematics/operators.py +239 -0
- tnfr/mathematics/operators.pyi +59 -0
- tnfr/mathematics/operators_factory.py +124 -0
- tnfr/mathematics/operators_factory.pyi +11 -0
- tnfr/mathematics/projection.py +87 -0
- tnfr/mathematics/projection.pyi +33 -0
- tnfr/mathematics/runtime.py +182 -0
- tnfr/mathematics/runtime.pyi +64 -0
- tnfr/mathematics/spaces.py +256 -0
- tnfr/mathematics/spaces.pyi +83 -0
- tnfr/mathematics/transforms.py +305 -0
- tnfr/mathematics/transforms.pyi +62 -0
- tnfr/metrics/__init__.py +79 -0
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/buffer_cache.py +163 -0
- tnfr/metrics/buffer_cache.pyi +24 -0
- tnfr/metrics/cache_utils.py +214 -0
- tnfr/metrics/coherence.py +2009 -0
- tnfr/metrics/coherence.pyi +129 -0
- tnfr/metrics/common.py +158 -0
- tnfr/metrics/common.pyi +35 -0
- tnfr/metrics/core.py +316 -0
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +833 -0
- tnfr/metrics/diagnosis.pyi +86 -0
- tnfr/metrics/emergence.py +245 -0
- tnfr/metrics/export.py +179 -0
- tnfr/metrics/export.pyi +7 -0
- tnfr/metrics/glyph_timing.py +379 -0
- tnfr/metrics/glyph_timing.pyi +81 -0
- tnfr/metrics/learning_metrics.py +280 -0
- tnfr/metrics/learning_metrics.pyi +21 -0
- tnfr/metrics/phase_coherence.py +351 -0
- tnfr/metrics/phase_compatibility.py +349 -0
- tnfr/metrics/reporting.py +183 -0
- tnfr/metrics/reporting.pyi +25 -0
- tnfr/metrics/sense_index.py +1203 -0
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +373 -0
- tnfr/metrics/trig.pyi +13 -0
- tnfr/metrics/trig_cache.py +233 -0
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/multiscale/__init__.py +32 -0
- tnfr/multiscale/hierarchical.py +517 -0
- tnfr/node.py +763 -0
- tnfr/node.pyi +139 -0
- tnfr/observers.py +255 -130
- tnfr/observers.pyi +31 -0
- tnfr/ontosim.py +144 -137
- tnfr/ontosim.pyi +28 -0
- tnfr/operators/__init__.py +1672 -0
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/algebra.py +277 -0
- tnfr/operators/canonical_patterns.py +420 -0
- tnfr/operators/cascade.py +267 -0
- tnfr/operators/cycle_detection.py +358 -0
- tnfr/operators/definitions.py +4108 -0
- tnfr/operators/definitions.pyi +78 -0
- tnfr/operators/grammar.py +1164 -0
- tnfr/operators/grammar.pyi +140 -0
- tnfr/operators/hamiltonian.py +710 -0
- tnfr/operators/health_analyzer.py +809 -0
- tnfr/operators/jitter.py +272 -0
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/lifecycle.py +314 -0
- tnfr/operators/metabolism.py +618 -0
- tnfr/operators/metrics.py +2138 -0
- tnfr/operators/network_analysis/__init__.py +27 -0
- tnfr/operators/network_analysis/source_detection.py +186 -0
- tnfr/operators/nodal_equation.py +395 -0
- tnfr/operators/pattern_detection.py +660 -0
- tnfr/operators/patterns.py +669 -0
- tnfr/operators/postconditions/__init__.py +38 -0
- tnfr/operators/postconditions/mutation.py +236 -0
- tnfr/operators/preconditions/__init__.py +1226 -0
- tnfr/operators/preconditions/coherence.py +305 -0
- tnfr/operators/preconditions/dissonance.py +236 -0
- tnfr/operators/preconditions/emission.py +128 -0
- tnfr/operators/preconditions/mutation.py +580 -0
- tnfr/operators/preconditions/reception.py +125 -0
- tnfr/operators/preconditions/resonance.py +364 -0
- tnfr/operators/registry.py +74 -0
- tnfr/operators/registry.pyi +9 -0
- tnfr/operators/remesh.py +1809 -0
- tnfr/operators/remesh.pyi +26 -0
- tnfr/operators/structural_units.py +268 -0
- tnfr/operators/unified_grammar.py +105 -0
- tnfr/parallel/__init__.py +54 -0
- tnfr/parallel/auto_scaler.py +234 -0
- tnfr/parallel/distributed.py +384 -0
- tnfr/parallel/engine.py +238 -0
- tnfr/parallel/gpu_engine.py +420 -0
- tnfr/parallel/monitoring.py +248 -0
- tnfr/parallel/partitioner.py +459 -0
- tnfr/py.typed +0 -0
- tnfr/recipes/__init__.py +22 -0
- tnfr/recipes/cookbook.py +743 -0
- tnfr/rng.py +178 -0
- tnfr/rng.pyi +26 -0
- tnfr/schemas/__init__.py +8 -0
- tnfr/schemas/grammar.json +94 -0
- tnfr/sdk/__init__.py +107 -0
- tnfr/sdk/__init__.pyi +19 -0
- tnfr/sdk/adaptive_system.py +173 -0
- tnfr/sdk/adaptive_system.pyi +21 -0
- tnfr/sdk/builders.py +370 -0
- tnfr/sdk/builders.pyi +51 -0
- tnfr/sdk/fluent.py +1121 -0
- tnfr/sdk/fluent.pyi +74 -0
- tnfr/sdk/templates.py +342 -0
- tnfr/sdk/templates.pyi +41 -0
- tnfr/sdk/utils.py +341 -0
- tnfr/secure_config.py +46 -0
- tnfr/security/__init__.py +70 -0
- tnfr/security/database.py +514 -0
- tnfr/security/subprocess.py +503 -0
- tnfr/security/validation.py +290 -0
- tnfr/selector.py +247 -0
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +378 -0
- tnfr/sense.pyi +23 -0
- tnfr/services/__init__.py +17 -0
- tnfr/services/orchestrator.py +325 -0
- tnfr/sparse/__init__.py +39 -0
- tnfr/sparse/representations.py +492 -0
- tnfr/structural.py +705 -0
- tnfr/structural.pyi +83 -0
- tnfr/telemetry/__init__.py +35 -0
- tnfr/telemetry/cache_metrics.py +226 -0
- tnfr/telemetry/cache_metrics.pyi +64 -0
- tnfr/telemetry/nu_f.py +422 -0
- tnfr/telemetry/nu_f.pyi +108 -0
- tnfr/telemetry/verbosity.py +36 -0
- tnfr/telemetry/verbosity.pyi +15 -0
- tnfr/tokens.py +58 -0
- tnfr/tokens.pyi +36 -0
- tnfr/tools/__init__.py +20 -0
- tnfr/tools/domain_templates.py +478 -0
- tnfr/tools/sequence_generator.py +846 -0
- tnfr/topology/__init__.py +13 -0
- tnfr/topology/asymmetry.py +151 -0
- tnfr/trace.py +543 -0
- tnfr/trace.pyi +42 -0
- tnfr/tutorials/__init__.py +38 -0
- tnfr/tutorials/autonomous_evolution.py +285 -0
- tnfr/tutorials/interactive.py +1576 -0
- tnfr/tutorials/structural_metabolism.py +238 -0
- tnfr/types.py +775 -0
- tnfr/types.pyi +357 -0
- tnfr/units.py +68 -0
- tnfr/units.pyi +13 -0
- tnfr/utils/__init__.py +282 -0
- tnfr/utils/__init__.pyi +215 -0
- tnfr/utils/cache.py +4223 -0
- tnfr/utils/cache.pyi +470 -0
- tnfr/utils/callbacks.py +375 -0
- tnfr/utils/callbacks.pyi +49 -0
- tnfr/utils/chunks.py +108 -0
- tnfr/utils/chunks.pyi +22 -0
- tnfr/utils/data.py +428 -0
- tnfr/utils/data.pyi +74 -0
- tnfr/utils/graph.py +85 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +821 -0
- tnfr/utils/init.pyi +80 -0
- tnfr/utils/io.py +559 -0
- tnfr/utils/io.pyi +66 -0
- tnfr/utils/numeric.py +114 -0
- tnfr/utils/numeric.pyi +21 -0
- tnfr/validation/__init__.py +257 -0
- tnfr/validation/__init__.pyi +85 -0
- tnfr/validation/compatibility.py +460 -0
- tnfr/validation/compatibility.pyi +6 -0
- tnfr/validation/config.py +73 -0
- tnfr/validation/graph.py +139 -0
- tnfr/validation/graph.pyi +18 -0
- tnfr/validation/input_validation.py +755 -0
- tnfr/validation/invariants.py +712 -0
- tnfr/validation/rules.py +253 -0
- tnfr/validation/rules.pyi +44 -0
- tnfr/validation/runtime.py +279 -0
- tnfr/validation/runtime.pyi +28 -0
- tnfr/validation/sequence_validator.py +162 -0
- tnfr/validation/soft_filters.py +170 -0
- tnfr/validation/soft_filters.pyi +32 -0
- tnfr/validation/spectral.py +164 -0
- tnfr/validation/spectral.pyi +42 -0
- tnfr/validation/validator.py +1266 -0
- tnfr/validation/window.py +39 -0
- tnfr/validation/window.pyi +1 -0
- tnfr/visualization/__init__.py +98 -0
- tnfr/visualization/cascade_viz.py +256 -0
- tnfr/visualization/hierarchy.py +284 -0
- tnfr/visualization/sequence_plotter.py +784 -0
- tnfr/viz/__init__.py +60 -0
- tnfr/viz/matplotlib.py +278 -0
- tnfr/viz/matplotlib.pyi +35 -0
- tnfr-8.5.0.dist-info/METADATA +573 -0
- tnfr-8.5.0.dist-info/RECORD +353 -0
- tnfr-8.5.0.dist-info/entry_points.txt +3 -0
- tnfr-3.0.3.dist-info/licenses/LICENSE.txt → tnfr-8.5.0.dist-info/licenses/LICENSE.md +1 -1
- tnfr/constants.py +0 -183
- tnfr/dynamics.py +0 -543
- tnfr/helpers.py +0 -198
- tnfr/main.py +0 -37
- tnfr/operators.py +0 -296
- tnfr-3.0.3.dist-info/METADATA +0 -35
- tnfr-3.0.3.dist-info/RECORD +0 -13
- {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
- {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1672 @@
|
|
|
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
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import heapq
|
|
12
|
+
import math
|
|
13
|
+
from collections.abc import Callable, Iterator
|
|
14
|
+
from itertools import islice
|
|
15
|
+
from statistics import StatisticsError, fmean
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from tnfr import glyph_history
|
|
19
|
+
|
|
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
|
|
24
|
+
from ..metrics.trig import neighbor_phase_mean
|
|
25
|
+
from ..rng import make_rng
|
|
26
|
+
from ..types import EPIValue, Glyph, NodeId, TNFRGraph
|
|
27
|
+
from ..utils import get_nodenx
|
|
28
|
+
from . import definitions as _definitions
|
|
29
|
+
from .jitter import (
|
|
30
|
+
JitterCache,
|
|
31
|
+
JitterCacheManager,
|
|
32
|
+
get_jitter_manager,
|
|
33
|
+
random_jitter,
|
|
34
|
+
reset_jitter_manager,
|
|
35
|
+
)
|
|
36
|
+
from .registry import OPERATORS, discover_operators, get_operator_class
|
|
37
|
+
from .remesh import (
|
|
38
|
+
apply_network_remesh,
|
|
39
|
+
apply_remesh_if_globally_stable,
|
|
40
|
+
apply_topological_remesh,
|
|
41
|
+
)
|
|
42
|
+
|
|
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)
|
|
61
|
+
|
|
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
|
+
)
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
"JitterCache",
|
|
85
|
+
"JitterCacheManager",
|
|
86
|
+
"get_jitter_manager",
|
|
87
|
+
"reset_jitter_manager",
|
|
88
|
+
"random_jitter",
|
|
89
|
+
"get_neighbor_epi",
|
|
90
|
+
"get_glyph_factors",
|
|
91
|
+
"GLYPH_OPERATIONS",
|
|
92
|
+
"apply_glyph_obj",
|
|
93
|
+
"apply_glyph",
|
|
94
|
+
"apply_glyph_with_grammar",
|
|
95
|
+
"apply_network_remesh",
|
|
96
|
+
"apply_topological_remesh",
|
|
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",
|
|
112
|
+
]
|
|
113
|
+
|
|
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
|
+
"""
|
|
150
|
+
return node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"].copy())
|
|
151
|
+
|
|
152
|
+
|
|
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
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# -------------------------
|
|
200
|
+
# Glyphs (local operators)
|
|
201
|
+
# -------------------------
|
|
202
|
+
|
|
203
|
+
|
|
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
|
+
"""
|
|
241
|
+
|
|
242
|
+
epi = node.EPI
|
|
243
|
+
neigh = list(node.neighbors())
|
|
244
|
+
if not neigh:
|
|
245
|
+
return [], epi
|
|
246
|
+
|
|
247
|
+
if hasattr(node, "G"):
|
|
248
|
+
G = node.G
|
|
249
|
+
total = 0.0
|
|
250
|
+
count = 0
|
|
251
|
+
has_valid_neighbor = False
|
|
252
|
+
needs_conversion = False
|
|
253
|
+
for v in neigh:
|
|
254
|
+
if hasattr(v, "EPI"):
|
|
255
|
+
total += float(v.EPI)
|
|
256
|
+
has_valid_neighbor = True
|
|
257
|
+
else:
|
|
258
|
+
attr = get_attr(G.nodes[v], ALIAS_EPI, None)
|
|
259
|
+
if attr is not None:
|
|
260
|
+
total += float(attr)
|
|
261
|
+
has_valid_neighbor = True
|
|
262
|
+
else:
|
|
263
|
+
total += float(epi)
|
|
264
|
+
needs_conversion = True
|
|
265
|
+
count += 1
|
|
266
|
+
if not has_valid_neighbor:
|
|
267
|
+
return [], epi
|
|
268
|
+
epi_bar = total / count if count else float(epi)
|
|
269
|
+
if needs_conversion:
|
|
270
|
+
NodeNX = get_nodenx()
|
|
271
|
+
if NodeNX is None:
|
|
272
|
+
raise ImportError("NodeNX is unavailable")
|
|
273
|
+
neigh = [
|
|
274
|
+
v if hasattr(v, "EPI") else NodeNX.from_graph(node.G, v) for v in neigh
|
|
275
|
+
]
|
|
276
|
+
else:
|
|
277
|
+
try:
|
|
278
|
+
epi_bar = fmean(v.EPI for v in neigh)
|
|
279
|
+
except StatisticsError:
|
|
280
|
+
epi_bar = epi
|
|
281
|
+
|
|
282
|
+
return neigh, epi_bar
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _determine_dominant(
|
|
286
|
+
neigh: list[NodeProtocol], default_kind: str
|
|
287
|
+
) -> tuple[str, float]:
|
|
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
|
+
"""
|
|
317
|
+
best_kind: str | None = None
|
|
318
|
+
best_abs = 0.0
|
|
319
|
+
for v in neigh:
|
|
320
|
+
abs_v = abs(v.EPI)
|
|
321
|
+
if abs_v > best_abs:
|
|
322
|
+
best_abs = abs_v
|
|
323
|
+
best_kind = v.epi_kind
|
|
324
|
+
if not best_kind:
|
|
325
|
+
return default_kind, 0.0
|
|
326
|
+
return best_kind, best_abs
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _mix_epi_with_neighbors(
|
|
330
|
+
node: NodeProtocol, mix: float, default_glyph: Glyph | str
|
|
331
|
+
) -> tuple[float, str]:
|
|
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
|
+
"""
|
|
370
|
+
default_kind = (
|
|
371
|
+
default_glyph.value if isinstance(default_glyph, Glyph) else str(default_glyph)
|
|
372
|
+
)
|
|
373
|
+
epi = node.EPI
|
|
374
|
+
neigh, epi_bar = get_neighbor_epi(node)
|
|
375
|
+
|
|
376
|
+
if not neigh:
|
|
377
|
+
node.epi_kind = default_kind
|
|
378
|
+
return epi, default_kind
|
|
379
|
+
|
|
380
|
+
dominant, best_abs = _determine_dominant(neigh, default_kind)
|
|
381
|
+
new_epi = (1 - mix) * epi + mix * epi_bar
|
|
382
|
+
_set_epi_with_boundary_check(node, new_epi)
|
|
383
|
+
final = dominant if best_abs > abs(new_epi) else node.epi_kind
|
|
384
|
+
if not final:
|
|
385
|
+
final = default_kind
|
|
386
|
+
node.epi_kind = final
|
|
387
|
+
return epi_bar, final
|
|
388
|
+
|
|
389
|
+
|
|
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
|
+
"""
|
|
415
|
+
f = get_factor(gf, "AL_boost", 0.05)
|
|
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
|
+
"""
|
|
449
|
+
mix = get_factor(gf, "EN_mix", 0.25)
|
|
450
|
+
_mix_epi_with_neighbors(node, mix, Glyph.EN)
|
|
451
|
+
|
|
452
|
+
|
|
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
|
+
"""
|
|
477
|
+
factor = get_factor(gf, "IL_dnfr_factor", 0.7)
|
|
478
|
+
node.dnfr = factor * getattr(node, "dnfr", 0.0)
|
|
479
|
+
|
|
480
|
+
|
|
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
|
+
"""
|
|
506
|
+
factor = get_factor(gf, "OZ_dnfr_factor", 1.3)
|
|
507
|
+
dnfr = getattr(node, "dnfr", 0.0)
|
|
508
|
+
if bool(node.graph.get("OZ_NOISE_MODE", False)):
|
|
509
|
+
sigma = float(node.graph.get("OZ_SIGMA", 0.1))
|
|
510
|
+
if sigma <= 0:
|
|
511
|
+
node.dnfr = dnfr
|
|
512
|
+
return
|
|
513
|
+
node.dnfr = dnfr + random_jitter(node, sigma)
|
|
514
|
+
else:
|
|
515
|
+
node.dnfr = factor * dnfr if abs(dnfr) > 1e-9 else 0.1
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _um_candidate_iter(node: NodeProtocol) -> Iterator[NodeProtocol]:
|
|
519
|
+
sample_ids = node.graph.get("_node_sample")
|
|
520
|
+
if sample_ids is not None and hasattr(node, "G"):
|
|
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)
|
|
525
|
+
else:
|
|
526
|
+
base = node.all_nodes()
|
|
527
|
+
for j in base:
|
|
528
|
+
same = (j is node) or (getattr(node, "n", None) == getattr(j, "n", None))
|
|
529
|
+
if same or node.has_edge(j):
|
|
530
|
+
continue
|
|
531
|
+
yield j
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _um_select_candidates(
|
|
535
|
+
node: NodeProtocol,
|
|
536
|
+
candidates: Iterator[NodeProtocol],
|
|
537
|
+
limit: int,
|
|
538
|
+
mode: str,
|
|
539
|
+
th: float,
|
|
540
|
+
) -> list[NodeProtocol]:
|
|
541
|
+
"""Select a subset of ``candidates`` for UM coupling."""
|
|
542
|
+
rng = make_rng(int(node.graph.get("RANDOM_SEED", 0)), node.offset(), node.G)
|
|
543
|
+
|
|
544
|
+
if limit <= 0:
|
|
545
|
+
return list(candidates)
|
|
546
|
+
|
|
547
|
+
if mode == "proximity":
|
|
548
|
+
return heapq.nsmallest(
|
|
549
|
+
limit, candidates, key=lambda j: abs(angle_diff(j.theta, th))
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
reservoir = list(islice(candidates, limit))
|
|
553
|
+
for i, cand in enumerate(candidates, start=limit):
|
|
554
|
+
j = rng.randint(0, i)
|
|
555
|
+
if j < limit:
|
|
556
|
+
reservoir[j] = cand
|
|
557
|
+
|
|
558
|
+
if mode == "sample":
|
|
559
|
+
rng.shuffle(reservoir)
|
|
560
|
+
|
|
561
|
+
return reservoir
|
|
562
|
+
|
|
563
|
+
|
|
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
|
+
"""
|
|
670
|
+
k = get_factor(gf, "UM_theta_push", 0.25)
|
|
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]
|
|
691
|
+
|
|
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)):
|
|
760
|
+
thr = float(
|
|
761
|
+
node.graph.get(
|
|
762
|
+
"UM_COMPAT_THRESHOLD",
|
|
763
|
+
DEFAULTS.get("UM_COMPAT_THRESHOLD", 0.75),
|
|
764
|
+
)
|
|
765
|
+
)
|
|
766
|
+
epi_i = node.EPI
|
|
767
|
+
si_i = node.Si
|
|
768
|
+
|
|
769
|
+
limit = int(node.graph.get("UM_CANDIDATE_COUNT", 0))
|
|
770
|
+
mode = str(node.graph.get("UM_CANDIDATE_MODE", "sample")).lower()
|
|
771
|
+
candidates = _um_select_candidates(
|
|
772
|
+
node, _um_candidate_iter(node), limit, mode, th_i
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
for j in candidates:
|
|
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
|
+
|
|
781
|
+
epi_j = j.EPI
|
|
782
|
+
si_j = j.Si
|
|
783
|
+
epi_sim = 1.0 - abs(epi_i - epi_j) / (abs(epi_i) + abs(epi_j) + 1e-9)
|
|
784
|
+
si_sim = 1.0 - abs(si_i - si_j)
|
|
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
|
|
787
|
+
if compat >= thr:
|
|
788
|
+
node.add_edge(j, compat)
|
|
789
|
+
|
|
790
|
+
|
|
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
|
|
858
|
+
diff = get_factor(gf, "RA_epi_diff", 0.15)
|
|
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
|
|
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
|
|
882
|
+
|
|
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
|
+
"""
|
|
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)
|
|
1024
|
+
node.vf = factor * node.vf
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
factor_val = 1.05 # Conservative scale prevents EPI overflow near boundaries
|
|
1028
|
+
factor_nul = 0.85
|
|
1029
|
+
_SCALE_FACTORS = {Glyph.VAL: factor_val, Glyph.NUL: factor_nul}
|
|
1030
|
+
|
|
1031
|
+
|
|
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
|
+
"""
|
|
1219
|
+
node.vf *= factor
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
def _make_scale_op(glyph: Glyph) -> GlyphOperation:
|
|
1223
|
+
def _op(node: NodeProtocol, gf: GlyphFactors) -> None:
|
|
1224
|
+
key = "VAL_scale" if glyph is Glyph.VAL else "NUL_scale"
|
|
1225
|
+
default = _SCALE_FACTORS[glyph]
|
|
1226
|
+
factor = get_factor(gf, key, default)
|
|
1227
|
+
|
|
1228
|
+
# Always scale νf (existing behavior)
|
|
1229
|
+
_op_scale(node, factor)
|
|
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
|
+
)
|
|
1350
|
+
return _op
|
|
1351
|
+
|
|
1352
|
+
|
|
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
|
+
"""
|
|
1377
|
+
a = get_factor(gf, "THOL_accel", 0.10)
|
|
1378
|
+
node.dnfr = node.dnfr + a * getattr(node, "d2EPI", 0.0)
|
|
1379
|
+
|
|
1380
|
+
|
|
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
|
+
"""
|
|
1499
|
+
dnfr = node.dnfr
|
|
1500
|
+
vf = node.vf
|
|
1501
|
+
eta = get_factor(gf, "NAV_eta", 0.5)
|
|
1502
|
+
strict = bool(node.graph.get("NAV_STRICT", False))
|
|
1503
|
+
if strict:
|
|
1504
|
+
base = vf
|
|
1505
|
+
else:
|
|
1506
|
+
sign = 1.0 if dnfr >= 0 else -1.0
|
|
1507
|
+
target = sign * vf
|
|
1508
|
+
base = (1.0 - eta) * dnfr + eta * target
|
|
1509
|
+
j = get_factor(gf, "NAV_jitter", 0.05)
|
|
1510
|
+
if bool(node.graph.get("NAV_RANDOM", True)):
|
|
1511
|
+
jitter = random_jitter(node, j)
|
|
1512
|
+
else:
|
|
1513
|
+
jitter = j * (1 if base >= 0 else -1)
|
|
1514
|
+
node.dnfr = base + jitter
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
def _op_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
|
+
"""
|
|
1543
|
+
step_idx = glyph_history.current_step_idx(node)
|
|
1544
|
+
last_warn = node.graph.get("_remesh_warn_step", None)
|
|
1545
|
+
if last_warn != step_idx:
|
|
1546
|
+
msg = (
|
|
1547
|
+
"REMESH operates at network scale. Use apply_remesh_if_globally_"
|
|
1548
|
+
"stable(G) or apply_network_remesh(G)."
|
|
1549
|
+
)
|
|
1550
|
+
hist = glyph_history.ensure_history(node)
|
|
1551
|
+
glyph_history.append_metric(
|
|
1552
|
+
hist,
|
|
1553
|
+
"events",
|
|
1554
|
+
("warn", {"step": step_idx, "node": None, "msg": msg}),
|
|
1555
|
+
)
|
|
1556
|
+
node.graph["_remesh_warn_step"] = step_idx
|
|
1557
|
+
return
|
|
1558
|
+
|
|
1559
|
+
|
|
1560
|
+
# -------------------------
|
|
1561
|
+
# Dispatcher
|
|
1562
|
+
# -------------------------
|
|
1563
|
+
|
|
1564
|
+
GLYPH_OPERATIONS: dict[Glyph, GlyphOperation] = {
|
|
1565
|
+
Glyph.AL: _op_AL,
|
|
1566
|
+
Glyph.EN: _op_EN,
|
|
1567
|
+
Glyph.IL: _op_IL,
|
|
1568
|
+
Glyph.OZ: _op_OZ,
|
|
1569
|
+
Glyph.UM: _op_UM,
|
|
1570
|
+
Glyph.RA: _op_RA,
|
|
1571
|
+
Glyph.SHA: _op_SHA,
|
|
1572
|
+
Glyph.VAL: _make_scale_op(Glyph.VAL),
|
|
1573
|
+
Glyph.NUL: _make_scale_op(Glyph.NUL),
|
|
1574
|
+
Glyph.THOL: _op_THOL,
|
|
1575
|
+
Glyph.ZHIR: _op_ZHIR,
|
|
1576
|
+
Glyph.NAV: _op_NAV,
|
|
1577
|
+
Glyph.REMESH: _op_REMESH,
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
def apply_glyph_obj(
|
|
1582
|
+
node: NodeProtocol, glyph: Glyph | str, *, window: int | None = None
|
|
1583
|
+
) -> None:
|
|
1584
|
+
"""Apply ``glyph`` to an object satisfying :class:`NodeProtocol`."""
|
|
1585
|
+
|
|
1586
|
+
from .grammar import function_name_to_glyph
|
|
1587
|
+
from ..validation.input_validation import ValidationError, validate_glyph
|
|
1588
|
+
|
|
1589
|
+
# Validate glyph parameter
|
|
1590
|
+
try:
|
|
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:
|
|
1601
|
+
step_idx = glyph_history.current_step_idx(node)
|
|
1602
|
+
hist = glyph_history.ensure_history(node)
|
|
1603
|
+
glyph_history.append_metric(
|
|
1604
|
+
hist,
|
|
1605
|
+
"events",
|
|
1606
|
+
(
|
|
1607
|
+
"warn",
|
|
1608
|
+
{
|
|
1609
|
+
"step": step_idx,
|
|
1610
|
+
"node": getattr(node, "n", None),
|
|
1611
|
+
"msg": f"invalid glyph: {e}",
|
|
1612
|
+
},
|
|
1613
|
+
),
|
|
1614
|
+
)
|
|
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}")
|
|
1639
|
+
|
|
1640
|
+
op = GLYPH_OPERATIONS.get(g)
|
|
1641
|
+
if op is None:
|
|
1642
|
+
raise ValueError(f"glyph has no registered operator: {g}")
|
|
1643
|
+
if window is None:
|
|
1644
|
+
window = int(get_param(node, "GLYPH_HYSTERESIS_WINDOW"))
|
|
1645
|
+
gf = get_glyph_factors(node)
|
|
1646
|
+
op(node, gf)
|
|
1647
|
+
glyph_history.push_glyph(node._glyph_storage(), g.value, window)
|
|
1648
|
+
node.epi_kind = g.value
|
|
1649
|
+
|
|
1650
|
+
|
|
1651
|
+
def apply_glyph(
|
|
1652
|
+
G: TNFRGraph, n: NodeId, glyph: Glyph | str, *, window: int | None = None
|
|
1653
|
+
) -> None:
|
|
1654
|
+
"""Adapter to operate on ``networkx`` graphs."""
|
|
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)
|
|
1672
|
+
apply_glyph_obj(node, glyph, window=window)
|