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,13 @@
|
|
|
1
|
+
"""Topological analysis utilities for TNFR networks.
|
|
2
|
+
|
|
3
|
+
This module provides tools for analyzing the topological structure of TNFR
|
|
4
|
+
networks, including asymmetry measures and structural disruption detection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .asymmetry import compute_topological_asymmetry
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"compute_topological_asymmetry",
|
|
13
|
+
]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Topological asymmetry analysis for TNFR networks.
|
|
2
|
+
|
|
3
|
+
This module provides functions to measure local topological asymmetry around
|
|
4
|
+
nodes in a TNFR network. According to TNFR canonical theory, the OZ (Dissonance)
|
|
5
|
+
operator introduces topological disruption that breaks structural symmetry.
|
|
6
|
+
|
|
7
|
+
The asymmetry measure quantifies this symmetry breaking by analyzing the
|
|
8
|
+
heterogeneity of the node's ego-network (the node and its immediate neighbors).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..types import NodeId, TNFRGraph
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"compute_topological_asymmetry",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def compute_topological_asymmetry(G: "TNFRGraph", node: "NodeId") -> float:
|
|
24
|
+
"""Measure local topological asymmetry around node.
|
|
25
|
+
|
|
26
|
+
Uses ego-graph analysis to detect structural symmetry breaking introduced
|
|
27
|
+
by dissonance operators. According to TNFR canonical theory (§2.3.3, R4),
|
|
28
|
+
OZ (Dissonance) introduces **topological disruption**, not just numerical
|
|
29
|
+
instability.
|
|
30
|
+
|
|
31
|
+
The asymmetry is computed by analyzing the heterogeneity of degree and
|
|
32
|
+
clustering distributions in the node's 1-hop neighborhood (ego-graph).
|
|
33
|
+
Higher asymmetry indicates successful structural disruption.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
G : TNFRGraph
|
|
38
|
+
Graph containing the node
|
|
39
|
+
node : NodeId
|
|
40
|
+
Node identifier to analyze
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
float
|
|
45
|
+
Topological asymmetry measure, range [0.0, 1.0]:
|
|
46
|
+
- 0.0 = perfect local symmetry (homogeneous neighborhood)
|
|
47
|
+
- 1.0 = maximal asymmetry (heterogeneous structure)
|
|
48
|
+
|
|
49
|
+
Notes
|
|
50
|
+
-----
|
|
51
|
+
The asymmetry measure combines two components:
|
|
52
|
+
|
|
53
|
+
1. **Degree heterogeneity**: Coefficient of variation (CV) of node degrees
|
|
54
|
+
in the ego-graph. Measures structural connectivity variation.
|
|
55
|
+
|
|
56
|
+
2. **Clustering heterogeneity**: CV of local clustering coefficients.
|
|
57
|
+
Measures variation in local cohesion patterns.
|
|
58
|
+
|
|
59
|
+
The final asymmetry score is a weighted combination:
|
|
60
|
+
asymmetry = 0.6 * degree_cv + 0.4 * clustering_cv
|
|
61
|
+
|
|
62
|
+
For isolated nodes or very small neighborhoods (≤2 nodes), returns 0.0
|
|
63
|
+
as there is insufficient structure for meaningful asymmetry measurement.
|
|
64
|
+
|
|
65
|
+
Examples
|
|
66
|
+
--------
|
|
67
|
+
>>> from tnfr.structural import create_nfr
|
|
68
|
+
>>> from tnfr.operators.definitions import Dissonance
|
|
69
|
+
>>> from tnfr.topology import compute_topological_asymmetry
|
|
70
|
+
>>>
|
|
71
|
+
>>> G, node = create_nfr("test", epi=0.5, vf=1.0)
|
|
72
|
+
>>> # Add neighbors to create network structure
|
|
73
|
+
>>> for i in range(4):
|
|
74
|
+
... neighbor = f"n{i}"
|
|
75
|
+
... G.add_node(neighbor)
|
|
76
|
+
... G.add_edge(node, neighbor)
|
|
77
|
+
>>>
|
|
78
|
+
>>> # Measure asymmetry before dissonance
|
|
79
|
+
>>> asym_before = compute_topological_asymmetry(G, node)
|
|
80
|
+
>>>
|
|
81
|
+
>>> # Apply dissonance operator
|
|
82
|
+
>>> Dissonance()(G, node)
|
|
83
|
+
>>>
|
|
84
|
+
>>> # Measure asymmetry after dissonance
|
|
85
|
+
>>> asym_after = compute_topological_asymmetry(G, node)
|
|
86
|
+
>>>
|
|
87
|
+
>>> # Dissonance should increase asymmetry (topological disruption)
|
|
88
|
+
>>> assert asym_after >= asym_before
|
|
89
|
+
|
|
90
|
+
See Also
|
|
91
|
+
--------
|
|
92
|
+
tnfr.operators.definitions.Dissonance : OZ operator that introduces dissonance
|
|
93
|
+
tnfr.operators.metrics.dissonance_metrics : Collects asymmetry in metrics
|
|
94
|
+
"""
|
|
95
|
+
import networkx as nx
|
|
96
|
+
|
|
97
|
+
from ..utils import get_numpy
|
|
98
|
+
|
|
99
|
+
np = get_numpy()
|
|
100
|
+
|
|
101
|
+
# Extract 1-hop ego graph (node + immediate neighbors)
|
|
102
|
+
try:
|
|
103
|
+
ego = nx.ego_graph(G, node, radius=1)
|
|
104
|
+
except nx.NetworkXError:
|
|
105
|
+
return 0.0
|
|
106
|
+
|
|
107
|
+
n_nodes = ego.number_of_nodes()
|
|
108
|
+
|
|
109
|
+
if n_nodes <= 2:
|
|
110
|
+
# Too small for meaningful asymmetry
|
|
111
|
+
# Isolated node (n=1) or single connection (n=2) are trivially symmetric
|
|
112
|
+
return 0.0
|
|
113
|
+
|
|
114
|
+
# Compute degree heterogeneity in ego-graph
|
|
115
|
+
degrees = [ego.degree(n) for n in ego.nodes()]
|
|
116
|
+
|
|
117
|
+
if not degrees or all(d == 0 for d in degrees):
|
|
118
|
+
# No edges in ego graph - symmetric by definition
|
|
119
|
+
return 0.0
|
|
120
|
+
|
|
121
|
+
degrees_arr = np.array(degrees, dtype=float)
|
|
122
|
+
mean_degree = np.mean(degrees_arr)
|
|
123
|
+
|
|
124
|
+
if mean_degree < 1e-10:
|
|
125
|
+
degree_cv = 0.0
|
|
126
|
+
else:
|
|
127
|
+
std_degree = np.std(degrees_arr)
|
|
128
|
+
degree_cv = std_degree / mean_degree
|
|
129
|
+
|
|
130
|
+
# Compute clustering heterogeneity in ego-graph
|
|
131
|
+
try:
|
|
132
|
+
clustering = [nx.clustering(ego, n) for n in ego.nodes()]
|
|
133
|
+
except (ZeroDivisionError, nx.NetworkXError):
|
|
134
|
+
# If clustering computation fails, use only degree asymmetry
|
|
135
|
+
clustering = [0.0] * n_nodes
|
|
136
|
+
|
|
137
|
+
clustering_arr = np.array(clustering, dtype=float)
|
|
138
|
+
mean_clustering = np.mean(clustering_arr)
|
|
139
|
+
|
|
140
|
+
if mean_clustering < 1e-10:
|
|
141
|
+
clustering_cv = 0.0
|
|
142
|
+
else:
|
|
143
|
+
std_clustering = np.std(clustering_arr)
|
|
144
|
+
clustering_cv = std_clustering / mean_clustering
|
|
145
|
+
|
|
146
|
+
# Combined asymmetry score (weighted)
|
|
147
|
+
# Degree asymmetry is primary (60%), clustering is secondary (40%)
|
|
148
|
+
asymmetry = 0.6 * degree_cv + 0.4 * clustering_cv
|
|
149
|
+
|
|
150
|
+
# Clip to [0, 1] range
|
|
151
|
+
return float(np.clip(asymmetry, 0.0, 1.0))
|
tnfr/trace.py
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
"""Trace logging.
|
|
2
|
+
|
|
3
|
+
Field helpers avoid unnecessary copying by reusing dictionaries stored on
|
|
4
|
+
the graph whenever possible. Callers are expected to treat returned
|
|
5
|
+
structures as immutable snapshots.
|
|
6
|
+
|
|
7
|
+
Immutability Guarantees
|
|
8
|
+
-----------------------
|
|
9
|
+
Trace field producers return mappings wrapped in ``MappingProxyType`` to
|
|
10
|
+
prevent accidental mutation. These proxies enforce immutability while avoiding
|
|
11
|
+
unnecessary data copying. Consumers that need to modify trace data should
|
|
12
|
+
create mutable copies using ``dict(proxy)`` or merge patterns like
|
|
13
|
+
``{**proxy1, **proxy2, "new_key": value}``.
|
|
14
|
+
|
|
15
|
+
Example safe mutation patterns::
|
|
16
|
+
|
|
17
|
+
# Get immutable trace data
|
|
18
|
+
result = gamma_field(G)
|
|
19
|
+
gamma_proxy = result["gamma"]
|
|
20
|
+
|
|
21
|
+
# Cannot mutate directly (TypeError will be raised)
|
|
22
|
+
# gamma_proxy["new_key"] = value # ❌ Error!
|
|
23
|
+
|
|
24
|
+
# Safe pattern: create mutable copy
|
|
25
|
+
mutable = dict(gamma_proxy)
|
|
26
|
+
mutable["new_key"] = value # ✓ OK
|
|
27
|
+
|
|
28
|
+
# Safe pattern: merge with new data
|
|
29
|
+
combined = {**gamma_proxy, "new_key": value} # ✓ OK
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import warnings
|
|
35
|
+
from collections.abc import Iterable, Mapping
|
|
36
|
+
from types import MappingProxyType
|
|
37
|
+
from typing import Any, NamedTuple, Protocol, cast
|
|
38
|
+
|
|
39
|
+
from .constants import TRACE
|
|
40
|
+
from .glyph_history import append_metric, count_glyphs, ensure_history
|
|
41
|
+
from .metrics.sense_index import _normalise_si_sensitivity_mapping
|
|
42
|
+
from .telemetry.verbosity import (
|
|
43
|
+
TELEMETRY_VERBOSITY_DEFAULT,
|
|
44
|
+
TelemetryVerbosity,
|
|
45
|
+
)
|
|
46
|
+
from .types import (
|
|
47
|
+
SigmaVector,
|
|
48
|
+
TNFRGraph,
|
|
49
|
+
TraceCallback,
|
|
50
|
+
TraceFieldFn,
|
|
51
|
+
TraceFieldMap,
|
|
52
|
+
TraceFieldRegistry,
|
|
53
|
+
TraceMetadata,
|
|
54
|
+
TraceSnapshot,
|
|
55
|
+
)
|
|
56
|
+
from .utils import cached_import, get_graph_mapping, is_non_string_sequence
|
|
57
|
+
from .utils.callbacks import CallbackSpec
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _KuramotoFn(Protocol):
|
|
61
|
+
def __call__(self, G: TNFRGraph) -> tuple[float, float]: ...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _SigmaVectorFn(Protocol):
|
|
65
|
+
def __call__(self, G: TNFRGraph, weight_mode: str | None = None) -> SigmaVector: ...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TraceFieldSpec(NamedTuple):
|
|
69
|
+
"""Declarative specification for a trace field producer."""
|
|
70
|
+
|
|
71
|
+
name: str
|
|
72
|
+
phase: str
|
|
73
|
+
producer: TraceFieldFn
|
|
74
|
+
tiers: tuple[TelemetryVerbosity, ...]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
TRACE_VERBOSITY_DEFAULT = TELEMETRY_VERBOSITY_DEFAULT
|
|
78
|
+
TRACE_VERBOSITY_PRESETS: dict[str, tuple[str, ...]] = {}
|
|
79
|
+
_TRACE_CAPTURE_ALIASES: Mapping[str, str] = MappingProxyType(
|
|
80
|
+
{
|
|
81
|
+
"glyphs": "glyph_counts",
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _canonical_capture_name(name: str) -> str:
|
|
87
|
+
"""Return the canonical capture field name for ``name``."""
|
|
88
|
+
|
|
89
|
+
stripped = name.strip()
|
|
90
|
+
alias = _TRACE_CAPTURE_ALIASES.get(stripped)
|
|
91
|
+
if alias is not None:
|
|
92
|
+
return alias
|
|
93
|
+
|
|
94
|
+
lowered = stripped.lower()
|
|
95
|
+
alias = _TRACE_CAPTURE_ALIASES.get(lowered)
|
|
96
|
+
if alias is not None:
|
|
97
|
+
return alias
|
|
98
|
+
|
|
99
|
+
return stripped
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _normalise_capture_spec(raw: Any) -> set[str]:
|
|
103
|
+
"""Coerce custom capture payloads to a ``set`` of field names."""
|
|
104
|
+
|
|
105
|
+
if raw is None:
|
|
106
|
+
return set()
|
|
107
|
+
if isinstance(raw, Mapping):
|
|
108
|
+
return {_canonical_capture_name(str(name)) for name in raw.keys()}
|
|
109
|
+
if isinstance(raw, str):
|
|
110
|
+
return {_canonical_capture_name(raw)}
|
|
111
|
+
if isinstance(raw, Iterable):
|
|
112
|
+
return {_canonical_capture_name(str(name)) for name in raw}
|
|
113
|
+
return {_canonical_capture_name(str(raw))}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _resolve_trace_capture(cfg: Mapping[str, Any]) -> set[str]:
|
|
117
|
+
"""Return the capture set declared by ``cfg`` respecting verbosity."""
|
|
118
|
+
|
|
119
|
+
if "capture" in cfg:
|
|
120
|
+
return _normalise_capture_spec(cfg.get("capture"))
|
|
121
|
+
|
|
122
|
+
raw_verbosity = cfg.get("verbosity", TRACE_VERBOSITY_DEFAULT)
|
|
123
|
+
verbosity = str(raw_verbosity).lower()
|
|
124
|
+
fields = TRACE_VERBOSITY_PRESETS.get(verbosity)
|
|
125
|
+
if fields is None:
|
|
126
|
+
warnings.warn(
|
|
127
|
+
(
|
|
128
|
+
"Unknown TRACE verbosity %r; falling back to %s"
|
|
129
|
+
% (raw_verbosity, TRACE_VERBOSITY_DEFAULT)
|
|
130
|
+
),
|
|
131
|
+
UserWarning,
|
|
132
|
+
stacklevel=3,
|
|
133
|
+
)
|
|
134
|
+
fields = TRACE_VERBOSITY_PRESETS[TRACE_VERBOSITY_DEFAULT]
|
|
135
|
+
return set(fields)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _kuramoto_fallback(G: TNFRGraph) -> tuple[float, float]:
|
|
139
|
+
return 0.0, 0.0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
kuramoto_R_psi: _KuramotoFn = cast(
|
|
143
|
+
_KuramotoFn,
|
|
144
|
+
cached_import("tnfr.gamma", "kuramoto_R_psi", fallback=_kuramoto_fallback),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _sigma_fallback(G: TNFRGraph, _weight_mode: str | None = None) -> SigmaVector:
|
|
149
|
+
"""Return a null sigma vector regardless of ``_weight_mode``."""
|
|
150
|
+
|
|
151
|
+
return {"x": 0.0, "y": 0.0, "mag": 0.0, "angle": 0.0, "n": 0}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# Public exports for this module
|
|
155
|
+
__all__ = (
|
|
156
|
+
"CallbackSpec",
|
|
157
|
+
"TraceFieldSpec",
|
|
158
|
+
"TraceMetadata",
|
|
159
|
+
"TraceSnapshot",
|
|
160
|
+
"register_trace",
|
|
161
|
+
"register_trace_field",
|
|
162
|
+
"_callback_names",
|
|
163
|
+
"gamma_field",
|
|
164
|
+
"grammar_field",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# -------------------------
|
|
168
|
+
# Helpers
|
|
169
|
+
# -------------------------
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _trace_setup(
|
|
173
|
+
G: TNFRGraph,
|
|
174
|
+
) -> tuple[
|
|
175
|
+
Mapping[str, Any] | None,
|
|
176
|
+
set[str],
|
|
177
|
+
dict[str, Any] | None,
|
|
178
|
+
str | None,
|
|
179
|
+
]:
|
|
180
|
+
"""Prepare common configuration for trace snapshots.
|
|
181
|
+
|
|
182
|
+
Returns the active configuration, capture set, history and key under
|
|
183
|
+
which metadata will be stored. If tracing is disabled returns
|
|
184
|
+
``(None, set(), None, None)``.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
cfg_raw = G.graph.get("TRACE", TRACE)
|
|
188
|
+
cfg = cfg_raw if isinstance(cfg_raw, Mapping) else TRACE
|
|
189
|
+
if not cfg.get("enabled", True):
|
|
190
|
+
return None, set(), None, None
|
|
191
|
+
|
|
192
|
+
capture = _resolve_trace_capture(cfg)
|
|
193
|
+
hist = ensure_history(G)
|
|
194
|
+
key = cast(str | None, cfg.get("history_key", "trace_meta"))
|
|
195
|
+
return cfg, capture, hist, key
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _callback_names(
|
|
199
|
+
callbacks: Mapping[str, CallbackSpec] | Iterable[CallbackSpec],
|
|
200
|
+
) -> list[str]:
|
|
201
|
+
"""Return callback names from ``callbacks``."""
|
|
202
|
+
if isinstance(callbacks, Mapping):
|
|
203
|
+
callbacks = callbacks.values()
|
|
204
|
+
return [
|
|
205
|
+
cb.name if cb.name is not None else str(getattr(cb.func, "__name__", "fn"))
|
|
206
|
+
for cb in callbacks
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
EMPTY_MAPPING: Mapping[str, Any] = MappingProxyType({})
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def mapping_field(G: TNFRGraph, graph_key: str, out_key: str) -> TraceMetadata:
|
|
214
|
+
"""Copy mappings from ``G.graph`` into trace output."""
|
|
215
|
+
mapping = get_graph_mapping(
|
|
216
|
+
G, graph_key, f"G.graph[{graph_key!r}] is not a mapping; ignoring"
|
|
217
|
+
)
|
|
218
|
+
if mapping is None:
|
|
219
|
+
return {}
|
|
220
|
+
return {out_key: mapping}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# -------------------------
|
|
224
|
+
# Builders
|
|
225
|
+
# -------------------------
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _new_trace_meta(
|
|
229
|
+
G: TNFRGraph, phase: str
|
|
230
|
+
) -> tuple[TraceSnapshot, set[str], dict[str, Any] | None, str | None] | None:
|
|
231
|
+
"""Initialise trace metadata for a ``phase``.
|
|
232
|
+
|
|
233
|
+
Wraps :func:`_trace_setup` and creates the base structure with timestamp
|
|
234
|
+
and current phase. Returns ``None`` if tracing is disabled.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
cfg, capture, hist, key = _trace_setup(G)
|
|
238
|
+
if not cfg:
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
meta: TraceSnapshot = {"t": float(G.graph.get("_t", 0.0)), "phase": phase}
|
|
242
|
+
return meta, capture, hist, key
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# -------------------------
|
|
246
|
+
# Snapshots
|
|
247
|
+
# -------------------------
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _trace_capture(G: TNFRGraph, phase: str, fields: TraceFieldMap) -> None:
|
|
251
|
+
"""Capture ``fields`` for ``phase`` and store the snapshot.
|
|
252
|
+
|
|
253
|
+
A :class:`TraceSnapshot` is appended to the configured history when
|
|
254
|
+
tracing is active. If there is no active history or storage key the
|
|
255
|
+
capture is silently ignored.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
res = _new_trace_meta(G, phase)
|
|
259
|
+
if not res:
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
meta, capture, hist, key = res
|
|
263
|
+
if not capture:
|
|
264
|
+
return
|
|
265
|
+
for name, getter in fields.items():
|
|
266
|
+
if name in capture:
|
|
267
|
+
meta.update(getter(G))
|
|
268
|
+
if hist is None or key is None:
|
|
269
|
+
return
|
|
270
|
+
append_metric(hist, key, meta)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# -------------------------
|
|
274
|
+
# Registry
|
|
275
|
+
# -------------------------
|
|
276
|
+
|
|
277
|
+
TRACE_FIELDS: TraceFieldRegistry = {}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def register_trace_field(phase: str, name: str, func: TraceFieldFn) -> None:
|
|
281
|
+
"""Register ``func`` to populate trace field ``name`` during ``phase``."""
|
|
282
|
+
|
|
283
|
+
TRACE_FIELDS.setdefault(phase, {})[name] = func
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def gamma_field(G: TNFRGraph) -> TraceMetadata:
|
|
287
|
+
"""Expose γ-field metadata stored under ``G.graph['GAMMA']``."""
|
|
288
|
+
|
|
289
|
+
return mapping_field(G, "GAMMA", "gamma")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def grammar_field(G: TNFRGraph) -> TraceMetadata:
|
|
293
|
+
"""Expose canonical grammar metadata for trace emission."""
|
|
294
|
+
|
|
295
|
+
return mapping_field(G, "GRAMMAR_CANON", "grammar")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def dnfr_weights_field(G: TNFRGraph) -> TraceMetadata:
|
|
299
|
+
return mapping_field(G, "DNFR_WEIGHTS", "dnfr_weights")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def selector_field(G: TNFRGraph) -> TraceMetadata:
|
|
303
|
+
sel = G.graph.get("glyph_selector")
|
|
304
|
+
selector_name = getattr(sel, "__name__", str(sel)) if sel else None
|
|
305
|
+
return {"selector": selector_name}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _si_weights_field(G: TNFRGraph) -> TraceMetadata:
|
|
309
|
+
weights = mapping_field(G, "_Si_weights", "si_weights")
|
|
310
|
+
if weights:
|
|
311
|
+
return weights
|
|
312
|
+
return {"si_weights": EMPTY_MAPPING}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _si_sensitivity_field(G: TNFRGraph) -> TraceMetadata:
|
|
316
|
+
mapping = get_graph_mapping(
|
|
317
|
+
G,
|
|
318
|
+
"_Si_sensitivity",
|
|
319
|
+
"G.graph['_Si_sensitivity'] is not a mapping; ignoring",
|
|
320
|
+
)
|
|
321
|
+
if mapping is None:
|
|
322
|
+
return {"si_sensitivity": EMPTY_MAPPING}
|
|
323
|
+
|
|
324
|
+
normalised = _normalise_si_sensitivity_mapping(mapping, warn=True)
|
|
325
|
+
|
|
326
|
+
if normalised != mapping:
|
|
327
|
+
G.graph["_Si_sensitivity"] = normalised
|
|
328
|
+
|
|
329
|
+
return {"si_sensitivity": MappingProxyType(normalised)}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def si_weights_field(G: TNFRGraph) -> TraceMetadata:
|
|
333
|
+
"""Return sense-plane weights and sensitivity."""
|
|
334
|
+
|
|
335
|
+
weights = _si_weights_field(G)
|
|
336
|
+
sensitivity = _si_sensitivity_field(G)
|
|
337
|
+
return {**weights, **sensitivity}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def callbacks_field(G: TNFRGraph) -> TraceMetadata:
|
|
341
|
+
cb = G.graph.get("callbacks")
|
|
342
|
+
if not isinstance(cb, Mapping):
|
|
343
|
+
return {}
|
|
344
|
+
out: dict[str, list[str] | None] = {}
|
|
345
|
+
for phase, cb_map in cb.items():
|
|
346
|
+
if isinstance(cb_map, Mapping) or is_non_string_sequence(cb_map):
|
|
347
|
+
out[phase] = _callback_names(cb_map)
|
|
348
|
+
else:
|
|
349
|
+
out[phase] = None
|
|
350
|
+
return {"callbacks": out}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def thol_state_field(G: TNFRGraph) -> TraceMetadata:
|
|
354
|
+
th_open = 0
|
|
355
|
+
for _, nd in G.nodes(data=True):
|
|
356
|
+
st = nd.get("_GRAM", {})
|
|
357
|
+
if st.get("thol_open", False):
|
|
358
|
+
th_open += 1
|
|
359
|
+
return {"thol_open_nodes": th_open}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def kuramoto_field(G: TNFRGraph) -> TraceMetadata:
|
|
363
|
+
R, psi = kuramoto_R_psi(G)
|
|
364
|
+
return {"kuramoto": {"R": float(R), "psi": float(psi)}}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def sigma_field(G: TNFRGraph) -> TraceMetadata:
|
|
368
|
+
sigma_vector_from_graph: _SigmaVectorFn = cast(
|
|
369
|
+
_SigmaVectorFn,
|
|
370
|
+
cached_import(
|
|
371
|
+
"tnfr.sense",
|
|
372
|
+
"sigma_vector_from_graph",
|
|
373
|
+
fallback=_sigma_fallback,
|
|
374
|
+
),
|
|
375
|
+
)
|
|
376
|
+
sv = sigma_vector_from_graph(G)
|
|
377
|
+
return {
|
|
378
|
+
"sigma": {
|
|
379
|
+
"x": float(sv.get("x", 0.0)),
|
|
380
|
+
"y": float(sv.get("y", 0.0)),
|
|
381
|
+
"mag": float(sv.get("mag", 0.0)),
|
|
382
|
+
"angle": float(sv.get("angle", 0.0)),
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def glyph_counts_field(G: TNFRGraph) -> TraceMetadata:
|
|
388
|
+
"""Return structural operator application count snapshot.
|
|
389
|
+
|
|
390
|
+
Provides a snapshot of which structural operator symbols (glyphs) have been
|
|
391
|
+
applied in the current step. ``count_glyphs`` already produces a fresh
|
|
392
|
+
mapping so no additional copy is taken. Treat the returned mapping as read-only.
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
cnt = count_glyphs(G, window=1)
|
|
396
|
+
return {"glyphs": cnt}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
TRACE_FIELD_SPECS: tuple[TraceFieldSpec, ...] = (
|
|
400
|
+
TraceFieldSpec(
|
|
401
|
+
name="gamma",
|
|
402
|
+
phase="before",
|
|
403
|
+
producer=gamma_field,
|
|
404
|
+
tiers=(
|
|
405
|
+
TelemetryVerbosity.BASIC,
|
|
406
|
+
TelemetryVerbosity.DETAILED,
|
|
407
|
+
TelemetryVerbosity.DEBUG,
|
|
408
|
+
),
|
|
409
|
+
),
|
|
410
|
+
TraceFieldSpec(
|
|
411
|
+
name="grammar",
|
|
412
|
+
phase="before",
|
|
413
|
+
producer=grammar_field,
|
|
414
|
+
tiers=(
|
|
415
|
+
TelemetryVerbosity.BASIC,
|
|
416
|
+
TelemetryVerbosity.DETAILED,
|
|
417
|
+
TelemetryVerbosity.DEBUG,
|
|
418
|
+
),
|
|
419
|
+
),
|
|
420
|
+
TraceFieldSpec(
|
|
421
|
+
name="selector",
|
|
422
|
+
phase="before",
|
|
423
|
+
producer=selector_field,
|
|
424
|
+
tiers=(
|
|
425
|
+
TelemetryVerbosity.BASIC,
|
|
426
|
+
TelemetryVerbosity.DETAILED,
|
|
427
|
+
TelemetryVerbosity.DEBUG,
|
|
428
|
+
),
|
|
429
|
+
),
|
|
430
|
+
TraceFieldSpec(
|
|
431
|
+
name="dnfr_weights",
|
|
432
|
+
phase="before",
|
|
433
|
+
producer=dnfr_weights_field,
|
|
434
|
+
tiers=(
|
|
435
|
+
TelemetryVerbosity.BASIC,
|
|
436
|
+
TelemetryVerbosity.DETAILED,
|
|
437
|
+
TelemetryVerbosity.DEBUG,
|
|
438
|
+
),
|
|
439
|
+
),
|
|
440
|
+
TraceFieldSpec(
|
|
441
|
+
name="si_weights",
|
|
442
|
+
phase="before",
|
|
443
|
+
producer=si_weights_field,
|
|
444
|
+
tiers=(
|
|
445
|
+
TelemetryVerbosity.BASIC,
|
|
446
|
+
TelemetryVerbosity.DETAILED,
|
|
447
|
+
TelemetryVerbosity.DEBUG,
|
|
448
|
+
),
|
|
449
|
+
),
|
|
450
|
+
TraceFieldSpec(
|
|
451
|
+
name="callbacks",
|
|
452
|
+
phase="before",
|
|
453
|
+
producer=callbacks_field,
|
|
454
|
+
tiers=(
|
|
455
|
+
TelemetryVerbosity.BASIC,
|
|
456
|
+
TelemetryVerbosity.DETAILED,
|
|
457
|
+
TelemetryVerbosity.DEBUG,
|
|
458
|
+
),
|
|
459
|
+
),
|
|
460
|
+
TraceFieldSpec(
|
|
461
|
+
name="thol_open_nodes",
|
|
462
|
+
phase="before",
|
|
463
|
+
producer=thol_state_field,
|
|
464
|
+
tiers=(
|
|
465
|
+
TelemetryVerbosity.BASIC,
|
|
466
|
+
TelemetryVerbosity.DETAILED,
|
|
467
|
+
TelemetryVerbosity.DEBUG,
|
|
468
|
+
),
|
|
469
|
+
),
|
|
470
|
+
TraceFieldSpec(
|
|
471
|
+
name="kuramoto",
|
|
472
|
+
phase="after",
|
|
473
|
+
producer=kuramoto_field,
|
|
474
|
+
tiers=(TelemetryVerbosity.DETAILED, TelemetryVerbosity.DEBUG),
|
|
475
|
+
),
|
|
476
|
+
TraceFieldSpec(
|
|
477
|
+
name="sigma",
|
|
478
|
+
phase="after",
|
|
479
|
+
producer=sigma_field,
|
|
480
|
+
tiers=(TelemetryVerbosity.DETAILED, TelemetryVerbosity.DEBUG),
|
|
481
|
+
),
|
|
482
|
+
TraceFieldSpec(
|
|
483
|
+
name="glyph_counts",
|
|
484
|
+
phase="after",
|
|
485
|
+
producer=glyph_counts_field,
|
|
486
|
+
tiers=(TelemetryVerbosity.DEBUG,),
|
|
487
|
+
),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
TRACE_VERBOSITY_PRESETS = {
|
|
491
|
+
level.value: tuple(spec.name for spec in TRACE_FIELD_SPECS if level in spec.tiers)
|
|
492
|
+
for level in TelemetryVerbosity
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
for spec in TRACE_FIELD_SPECS:
|
|
496
|
+
register_trace_field(spec.phase, spec.name, spec.producer)
|
|
497
|
+
|
|
498
|
+
# -------------------------
|
|
499
|
+
# API
|
|
500
|
+
# -------------------------
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def register_trace(G: TNFRGraph) -> None:
|
|
504
|
+
"""Enable before/after-step snapshots and dump operational metadata to history.
|
|
505
|
+
|
|
506
|
+
Trace snapshots are stored as :class:`TraceSnapshot` entries in
|
|
507
|
+
``G.graph['history'][TRACE.history_key]`` with:
|
|
508
|
+
- gamma: active Γi(R) specification
|
|
509
|
+
- grammar: canonical grammar configuration
|
|
510
|
+
- selector: glyph selector name
|
|
511
|
+
- dnfr_weights: ΔNFR mix declared in the engine
|
|
512
|
+
- si_weights: α/β/γ weights and Si sensitivity
|
|
513
|
+
- callbacks: callbacks registered per phase (if in
|
|
514
|
+
``G.graph['callbacks']``)
|
|
515
|
+
- thol_open_nodes: how many nodes have an open THOL block
|
|
516
|
+
- kuramoto: network ``(R, ψ)``
|
|
517
|
+
- sigma: global sense-plane vector
|
|
518
|
+
- glyphs: glyph counts after the step
|
|
519
|
+
|
|
520
|
+
Field helpers reuse graph dictionaries and expect them to be treated as
|
|
521
|
+
immutable snapshots by consumers.
|
|
522
|
+
"""
|
|
523
|
+
if G.graph.get("_trace_registered"):
|
|
524
|
+
return
|
|
525
|
+
|
|
526
|
+
from .utils import callback_manager
|
|
527
|
+
|
|
528
|
+
for phase in TRACE_FIELDS.keys():
|
|
529
|
+
event = f"{phase}_step"
|
|
530
|
+
|
|
531
|
+
def _make_cb(ph: str) -> TraceCallback:
|
|
532
|
+
def _cb(graph: TNFRGraph, ctx: dict[str, Any]) -> None:
|
|
533
|
+
del ctx
|
|
534
|
+
|
|
535
|
+
_trace_capture(graph, ph, TRACE_FIELDS.get(ph, {}))
|
|
536
|
+
|
|
537
|
+
return _cb
|
|
538
|
+
|
|
539
|
+
callback_manager.register_callback(
|
|
540
|
+
G, event=event, func=_make_cb(phase), name=f"trace_{phase}"
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
G.graph["_trace_registered"] = True
|