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,1203 @@
|
|
|
1
|
+
r"""Sense Index computation for TNFR networks.
|
|
2
|
+
|
|
3
|
+
The **Sense Index** (:math:`\text{Si}`) quantifies a node's capacity for stable
|
|
4
|
+
structural reorganization. It blends three structural signals: frequency :math:`\nu_f`
|
|
5
|
+
(reorganization rate), phase coupling :math:`\theta` (network synchrony), and
|
|
6
|
+
reorganization pressure :math:`\Delta\text{NFR}`.
|
|
7
|
+
|
|
8
|
+
Mathematical Foundation
|
|
9
|
+
-----------------------
|
|
10
|
+
|
|
11
|
+
The Sense Index is defined as a weighted combination:
|
|
12
|
+
|
|
13
|
+
.. math::
|
|
14
|
+
\text{Si} = \alpha \cdot \nu_{f,\text{norm}}
|
|
15
|
+
+ \beta \cdot (1 - \text{disp}_\theta)
|
|
16
|
+
+ \gamma \cdot (1 - |\Delta\text{NFR}|_{\text{norm}})
|
|
17
|
+
|
|
18
|
+
**Component definitions**:
|
|
19
|
+
|
|
20
|
+
1. **Normalized frequency** :math:`\nu_{f,\text{norm}}`:
|
|
21
|
+
|
|
22
|
+
.. math::
|
|
23
|
+
\nu_{f,\text{norm}} = \frac{|\nu_f|}{\nu_{f,\max}}
|
|
24
|
+
|
|
25
|
+
Measures how fast a node reorganizes relative to network maximum.
|
|
26
|
+
Range: [0, 1] where 1 = maximum reorganization rate.
|
|
27
|
+
|
|
28
|
+
2. **Phase dispersion** :math:`\text{disp}_\theta`:
|
|
29
|
+
|
|
30
|
+
.. math::
|
|
31
|
+
\text{disp}_\theta = \frac{|\theta - \bar{\theta}|}{\pi}
|
|
32
|
+
|
|
33
|
+
where :math:`\bar{\theta}` is the circular mean of neighbor phases:
|
|
34
|
+
|
|
35
|
+
.. math::
|
|
36
|
+
\bar{\theta} = \text{atan2}\left(\sum_{j \in N(i)} \sin\theta_j, \sum_{j \in N(i)} \cos\theta_j\right)
|
|
37
|
+
|
|
38
|
+
Measures phase misalignment with neighbors.
|
|
39
|
+
Range: [0, 1] where 0 = perfect synchrony, 1 = maximum dispersion.
|
|
40
|
+
|
|
41
|
+
3. **Normalized reorganization magnitude** :math:`|\Delta\text{NFR}|_{\text{norm}}`:
|
|
42
|
+
|
|
43
|
+
.. math::
|
|
44
|
+
|\Delta\text{NFR}|_{\text{norm}} = \frac{|\Delta\text{NFR}|}{\Delta\text{NFR}_{\max}}
|
|
45
|
+
|
|
46
|
+
Measures structural pressure relative to network maximum.
|
|
47
|
+
Range: [0, 1] where 0 = equilibrium, 1 = maximum pressure.
|
|
48
|
+
|
|
49
|
+
**Structural weights**:
|
|
50
|
+
|
|
51
|
+
- :math:`\alpha`: Frequency weight (default: 0.4) - emphasizes reorganization capacity
|
|
52
|
+
- :math:`\beta`: Phase weight (default: 0.3) - emphasizes network synchrony
|
|
53
|
+
- :math:`\gamma`: ΔNFR weight (default: 0.3) - emphasizes pressure damping
|
|
54
|
+
- Constraint: :math:`\alpha + \beta + \gamma = 1`
|
|
55
|
+
|
|
56
|
+
**Final clamping**: :math:`\text{Si}_{\text{final}} = \max(0, \min(1, \text{Si}))`
|
|
57
|
+
|
|
58
|
+
Physical Interpretation
|
|
59
|
+
------------------------
|
|
60
|
+
|
|
61
|
+
**High Si (> 0.7)**:
|
|
62
|
+
- Node reorganizes efficiently (:math:`\nu_f` high)
|
|
63
|
+
- Stays synchronized with network (:math:`\text{disp}_\theta` low)
|
|
64
|
+
- Experiences manageable pressure (:math:`|\Delta\text{NFR}|` low)
|
|
65
|
+
- **Implication**: Stable, well-integrated node
|
|
66
|
+
|
|
67
|
+
**Low Si (< 0.3)**:
|
|
68
|
+
- Slow reorganization OR high phase dispersion OR high pressure
|
|
69
|
+
- **Implication**: Risk of structural instability or network decoupling
|
|
70
|
+
|
|
71
|
+
**Moderate Si (0.3-0.7)**:
|
|
72
|
+
- Trade-offs between frequency, synchrony, and pressure
|
|
73
|
+
- **Implication**: Balanced state, monitor for bifurcation
|
|
74
|
+
|
|
75
|
+
Implementation Map
|
|
76
|
+
------------------
|
|
77
|
+
|
|
78
|
+
**Core Functions**:
|
|
79
|
+
|
|
80
|
+
- :func:`compute_Si` : Network-wide Si computation (vectorized when possible)
|
|
81
|
+
- :func:`compute_Si_node` : Single-node Si calculation
|
|
82
|
+
- :func:`get_Si_weights` : Extract or default Si weights from graph
|
|
83
|
+
|
|
84
|
+
**Helper Functions**:
|
|
85
|
+
|
|
86
|
+
- :func:`_compute_si_python_chunk` : Parallel worker for chunked computation
|
|
87
|
+
- :func:`_SiStructuralCache` : Cache for aligned :math:`\nu_f` and :math:`\Delta\text{NFR}` arrays
|
|
88
|
+
|
|
89
|
+
**Performance**:
|
|
90
|
+
|
|
91
|
+
- Uses NumPy vectorization for networks with >10 nodes
|
|
92
|
+
- Parallel computation for networks with >1000 nodes
|
|
93
|
+
- Trigonometric caching to avoid redundant phase calculations
|
|
94
|
+
|
|
95
|
+
Theoretical References
|
|
96
|
+
----------------------
|
|
97
|
+
|
|
98
|
+
See the following for complete derivation:
|
|
99
|
+
|
|
100
|
+
- **Mathematical Foundations**: `docs/source/theory/mathematical_foundations.md`
|
|
101
|
+
- **Worked Example**: `docs/source/examples/worked_examples.md` Example 1 (full walkthrough)
|
|
102
|
+
- **Style Guide**: `docs/source/style_guide.md` for notation conventions
|
|
103
|
+
|
|
104
|
+
Examples
|
|
105
|
+
--------
|
|
106
|
+
|
|
107
|
+
**Basic network-wide computation**:
|
|
108
|
+
|
|
109
|
+
>>> import networkx as nx
|
|
110
|
+
>>> from tnfr.metrics.sense_index import compute_Si
|
|
111
|
+
>>> G = nx.Graph()
|
|
112
|
+
>>> G.add_edge("sensor", "relay")
|
|
113
|
+
>>> G.nodes["sensor"].update({"nu_f": 0.9, "delta_nfr": 0.3, "phase": 0.0})
|
|
114
|
+
>>> G.nodes["relay"].update({"nu_f": 0.4, "delta_nfr": 0.05, "phase": 0.1})
|
|
115
|
+
>>> G.graph["SI_WEIGHTS"] = {"alpha": 0.5, "beta": 0.3, "gamma": 0.2}
|
|
116
|
+
>>> result = compute_Si(G, inplace=False)
|
|
117
|
+
>>> round(result["sensor"], 3), round(result["relay"], 3)
|
|
118
|
+
(0.767, 0.857)
|
|
119
|
+
|
|
120
|
+
The heavier :math:`\alpha` weight privileges the sensor's fast :math:`\nu_f` even
|
|
121
|
+
though it suffers larger :math:`\Delta\text{NFR}`. The relay keeps Si high thanks
|
|
122
|
+
to calmer :math:`\Delta\text{NFR}` despite slower frequency.
|
|
123
|
+
|
|
124
|
+
**Single-node computation**:
|
|
125
|
+
|
|
126
|
+
>>> from tnfr.metrics.sense_index import compute_Si_node
|
|
127
|
+
>>> node_attrs = {
|
|
128
|
+
... "nu_f": 0.8,
|
|
129
|
+
... "delta_nfr": 0.2,
|
|
130
|
+
... "phase": 0.5,
|
|
131
|
+
... "neighbors": [{"phase": 0.4}, {"phase": 0.6}]
|
|
132
|
+
... }
|
|
133
|
+
>>> Si = compute_Si_node(
|
|
134
|
+
... "node_id",
|
|
135
|
+
... node_attrs,
|
|
136
|
+
... alpha=0.4, beta=0.3, gamma=0.3,
|
|
137
|
+
... vfmax=1.0, dnfrmax=1.0,
|
|
138
|
+
... phase_dispersion=0.0, # Already computed
|
|
139
|
+
... inplace=False
|
|
140
|
+
... )
|
|
141
|
+
>>> 0.8 < Si < 0.9 # High stability
|
|
142
|
+
True
|
|
143
|
+
|
|
144
|
+
**In-place update**:
|
|
145
|
+
|
|
146
|
+
>>> G = nx.Graph()
|
|
147
|
+
>>> G.add_node("a", nu_f=0.8, delta_nfr=0.2, phase=0.0)
|
|
148
|
+
>>> compute_Si(G, inplace=True) # Writes to G.nodes[n]['Si']
|
|
149
|
+
>>> "Si" in G.nodes["a"]
|
|
150
|
+
True
|
|
151
|
+
|
|
152
|
+
See Also
|
|
153
|
+
--------
|
|
154
|
+
|
|
155
|
+
coherence.compute_coherence : Total network coherence :math:`C(t)`
|
|
156
|
+
coherence.coherence_matrix : Coherence operator approximation :math:`W \approx \hat{C}`
|
|
157
|
+
observers.kuramoto_order : Kuramoto order parameter for phase synchrony
|
|
158
|
+
observers.phase_sync : Phase synchronization metrics
|
|
159
|
+
|
|
160
|
+
Notes
|
|
161
|
+
-----
|
|
162
|
+
|
|
163
|
+
**Sensitivity analysis**:
|
|
164
|
+
|
|
165
|
+
The module can compute partial derivatives :math:`\frac{\partial \text{Si}}{\partial x}`
|
|
166
|
+
for :math:`x \in \{\nu_{f,\text{norm}}, \text{disp}_\theta, |\Delta\text{NFR}|_{\text{norm}}\}`
|
|
167
|
+
when `return_sensitivities=True` is passed to `compute_Si`.
|
|
168
|
+
|
|
169
|
+
**Edge cases**:
|
|
170
|
+
|
|
171
|
+
- If a node has no neighbors, :math:`\bar{\theta} = \theta` (zero dispersion)
|
|
172
|
+
- If :math:`\nu_{f,\max} = 0`, normalization defaults to 0 (frozen network)
|
|
173
|
+
- If :math:`\Delta\text{NFR}_{\max} = 0`, normalization defaults to 0 (equilibrium network)
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
from __future__ import annotations
|
|
177
|
+
|
|
178
|
+
import math
|
|
179
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
180
|
+
from functools import partial
|
|
181
|
+
from time import perf_counter
|
|
182
|
+
from typing import Any, Callable, Iterable, Iterator, Mapping, MutableMapping, cast
|
|
183
|
+
|
|
184
|
+
from ..alias import get_attr, set_attr
|
|
185
|
+
from ..constants.aliases import ALIAS_DNFR, ALIAS_SI, ALIAS_VF
|
|
186
|
+
from ..utils import angle_diff, angle_diff_array, clamp01
|
|
187
|
+
from ..types import GraphLike, NodeAttrMap
|
|
188
|
+
from ..utils import (
|
|
189
|
+
edge_version_cache,
|
|
190
|
+
get_numpy,
|
|
191
|
+
normalize_weights,
|
|
192
|
+
resolve_chunk_size,
|
|
193
|
+
stable_json,
|
|
194
|
+
)
|
|
195
|
+
from .buffer_cache import ensure_numpy_buffers
|
|
196
|
+
from .common import (
|
|
197
|
+
_coerce_jobs,
|
|
198
|
+
_get_vf_dnfr_max,
|
|
199
|
+
ensure_neighbors_map,
|
|
200
|
+
merge_graph_weights,
|
|
201
|
+
)
|
|
202
|
+
from .trig import neighbor_phase_mean_bulk, neighbor_phase_mean_list
|
|
203
|
+
from .trig_cache import get_trig_cache
|
|
204
|
+
|
|
205
|
+
PHASE_DISPERSION_KEY = "dSi_dphase_disp"
|
|
206
|
+
_SI_APPROX_BYTES_PER_NODE = 64
|
|
207
|
+
_VALID_SENSITIVITY_KEYS = frozenset(
|
|
208
|
+
{"dSi_dvf_norm", PHASE_DISPERSION_KEY, "dSi_ddnfr_norm"}
|
|
209
|
+
)
|
|
210
|
+
__all__ = ("get_Si_weights", "compute_Si_node", "compute_Si")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class _SiStructuralCache:
|
|
214
|
+
"""Cache aligned ``νf`` and ``ΔNFR`` arrays for vectorised Si."""
|
|
215
|
+
|
|
216
|
+
__slots__ = ("node_ids", "vf_values", "dnfr_values", "vf_snapshot", "dnfr_snapshot")
|
|
217
|
+
|
|
218
|
+
def __init__(self, node_ids: tuple[Any, ...]):
|
|
219
|
+
self.node_ids = node_ids
|
|
220
|
+
self.vf_values: Any | None = None
|
|
221
|
+
self.dnfr_values: Any | None = None
|
|
222
|
+
self.vf_snapshot: list[float] = []
|
|
223
|
+
self.dnfr_snapshot: list[float] = []
|
|
224
|
+
|
|
225
|
+
def rebuild(
|
|
226
|
+
self,
|
|
227
|
+
node_ids: Iterable[Any],
|
|
228
|
+
node_data: Mapping[Any, NodeAttrMap],
|
|
229
|
+
*,
|
|
230
|
+
np: Any,
|
|
231
|
+
) -> tuple[Any, Any]:
|
|
232
|
+
node_tuple = tuple(node_ids)
|
|
233
|
+
count = len(node_tuple)
|
|
234
|
+
if count == 0:
|
|
235
|
+
self.node_ids = node_tuple
|
|
236
|
+
self.vf_values = np.zeros(0, dtype=float)
|
|
237
|
+
self.dnfr_values = np.zeros(0, dtype=float)
|
|
238
|
+
self.vf_snapshot = []
|
|
239
|
+
self.dnfr_snapshot = []
|
|
240
|
+
return self.vf_values, self.dnfr_values
|
|
241
|
+
|
|
242
|
+
vf_arr = np.fromiter(
|
|
243
|
+
(float(get_attr(node_data[n], ALIAS_VF, 0.0)) for n in node_tuple),
|
|
244
|
+
dtype=float,
|
|
245
|
+
count=count,
|
|
246
|
+
)
|
|
247
|
+
dnfr_arr = np.fromiter(
|
|
248
|
+
(float(get_attr(node_data[n], ALIAS_DNFR, 0.0)) for n in node_tuple),
|
|
249
|
+
dtype=float,
|
|
250
|
+
count=count,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
self.node_ids = node_tuple
|
|
254
|
+
self.vf_values = vf_arr
|
|
255
|
+
self.dnfr_values = dnfr_arr
|
|
256
|
+
self.vf_snapshot = [float(value) for value in vf_arr]
|
|
257
|
+
self.dnfr_snapshot = [float(value) for value in dnfr_arr]
|
|
258
|
+
return self.vf_values, self.dnfr_values
|
|
259
|
+
|
|
260
|
+
def ensure_current(
|
|
261
|
+
self,
|
|
262
|
+
node_ids: Iterable[Any],
|
|
263
|
+
node_data: Mapping[Any, NodeAttrMap],
|
|
264
|
+
*,
|
|
265
|
+
np: Any,
|
|
266
|
+
) -> tuple[Any, Any]:
|
|
267
|
+
node_tuple = tuple(node_ids)
|
|
268
|
+
if node_tuple != self.node_ids:
|
|
269
|
+
return self.rebuild(node_tuple, node_data, np=np)
|
|
270
|
+
|
|
271
|
+
for idx, node in enumerate(node_tuple):
|
|
272
|
+
nd = node_data[node]
|
|
273
|
+
vf = float(get_attr(nd, ALIAS_VF, 0.0))
|
|
274
|
+
if vf != self.vf_snapshot[idx]:
|
|
275
|
+
return self.rebuild(node_tuple, node_data, np=np)
|
|
276
|
+
dnfr = float(get_attr(nd, ALIAS_DNFR, 0.0))
|
|
277
|
+
if dnfr != self.dnfr_snapshot[idx]:
|
|
278
|
+
return self.rebuild(node_tuple, node_data, np=np)
|
|
279
|
+
|
|
280
|
+
return self.vf_values, self.dnfr_values
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _build_structural_cache(
|
|
284
|
+
node_ids: Iterable[Any],
|
|
285
|
+
node_data: Mapping[Any, NodeAttrMap],
|
|
286
|
+
*,
|
|
287
|
+
np: Any,
|
|
288
|
+
) -> _SiStructuralCache:
|
|
289
|
+
cache = _SiStructuralCache(tuple(node_ids))
|
|
290
|
+
cache.rebuild(node_ids, node_data, np=np)
|
|
291
|
+
return cache
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _ensure_structural_arrays(
|
|
295
|
+
G: GraphLike,
|
|
296
|
+
node_ids: Iterable[Any],
|
|
297
|
+
node_data: Mapping[Any, NodeAttrMap],
|
|
298
|
+
*,
|
|
299
|
+
np: Any,
|
|
300
|
+
) -> tuple[Any, Any]:
|
|
301
|
+
node_key = tuple(node_ids)
|
|
302
|
+
|
|
303
|
+
def builder() -> _SiStructuralCache:
|
|
304
|
+
return _build_structural_cache(node_key, node_data, np=np)
|
|
305
|
+
|
|
306
|
+
cache = edge_version_cache(G, ("_si_structural", node_key), builder)
|
|
307
|
+
return cache.ensure_current(node_key, node_data, np=np)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _ensure_si_buffers(
|
|
311
|
+
G: GraphLike,
|
|
312
|
+
*,
|
|
313
|
+
count: int,
|
|
314
|
+
np: Any,
|
|
315
|
+
) -> tuple[Any, Any, Any]:
|
|
316
|
+
"""Return reusable NumPy buffers sized for ``count`` nodes.
|
|
317
|
+
|
|
318
|
+
Allocates three computation buffers used in Si vectorization:
|
|
319
|
+
1. phase_dispersion: Phase alignment metric per node
|
|
320
|
+
2. raw_si: Intermediate Si values before clamping
|
|
321
|
+
3. si_values: Final Si values after normalization
|
|
322
|
+
|
|
323
|
+
These buffers are reused across computation steps to minimize allocation
|
|
324
|
+
overhead in the hot path. Cache key: ``("_si_buffers", count, 3)``
|
|
325
|
+
"""
|
|
326
|
+
return ensure_numpy_buffers(
|
|
327
|
+
G, key_prefix="_si_buffers", count=count, buffer_count=3, np=np
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _ensure_chunk_workspace(
|
|
332
|
+
G: GraphLike,
|
|
333
|
+
*,
|
|
334
|
+
mask_count: int,
|
|
335
|
+
np: Any,
|
|
336
|
+
) -> tuple[Any, Any]:
|
|
337
|
+
"""Return reusable scratch buffers sized to the masked neighbours.
|
|
338
|
+
|
|
339
|
+
Allocates workspace for chunked phase dispersion computation:
|
|
340
|
+
1. chunk_theta: Theta values for current chunk
|
|
341
|
+
2. chunk_values: Intermediate values for current chunk
|
|
342
|
+
|
|
343
|
+
Used when processing large neighbor sets in chunks to manage memory.
|
|
344
|
+
Cache key: ``("_si_chunk_workspace", mask_count, 2)``
|
|
345
|
+
"""
|
|
346
|
+
return ensure_numpy_buffers(
|
|
347
|
+
G, key_prefix="_si_chunk_workspace", count=mask_count, buffer_count=2, np=np
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _ensure_neighbor_bulk_buffers(
|
|
352
|
+
G: GraphLike,
|
|
353
|
+
*,
|
|
354
|
+
count: int,
|
|
355
|
+
np: Any,
|
|
356
|
+
) -> tuple[Any, Any, Any, Any, Any]:
|
|
357
|
+
"""Return reusable buffers for bulk neighbour phase aggregation.
|
|
358
|
+
|
|
359
|
+
Allocates five buffers for neighbor accumulation in vectorized Si:
|
|
360
|
+
1. neighbor_cos_sum: Sum of cos(theta) from neighbors
|
|
361
|
+
2. neighbor_sin_sum: Sum of sin(theta) from neighbors
|
|
362
|
+
3. neighbor_counts: Number of neighbors per node
|
|
363
|
+
4. mean_cos_buf: Mean cos(theta) per node
|
|
364
|
+
5. mean_sin_buf: Mean sin(theta) per node
|
|
365
|
+
|
|
366
|
+
These enable efficient neighbor phase mean computation without Python loops.
|
|
367
|
+
Cache key: ``("_si_neighbor_buffers", count, 5)``
|
|
368
|
+
"""
|
|
369
|
+
return ensure_numpy_buffers(
|
|
370
|
+
G, key_prefix="_si_neighbor_buffers", count=count, buffer_count=5, np=np
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _normalise_si_sensitivity_mapping(
|
|
375
|
+
mapping: Mapping[str, float], *, warn: bool
|
|
376
|
+
) -> dict[str, float]:
|
|
377
|
+
"""Preserve structural sensitivities compatible with the Si operator.
|
|
378
|
+
|
|
379
|
+
Parameters
|
|
380
|
+
----------
|
|
381
|
+
mapping : Mapping[str, float]
|
|
382
|
+
Mapping of raw sensitivity weights keyed by structural derivatives.
|
|
383
|
+
warn : bool
|
|
384
|
+
Compatibility flag kept for trace helpers. It is not used directly but
|
|
385
|
+
retained so upstream logging keeps a consistent signature.
|
|
386
|
+
|
|
387
|
+
Returns
|
|
388
|
+
-------
|
|
389
|
+
dict[str, float]
|
|
390
|
+
Sanitised mapping containing only the supported sensitivity keys.
|
|
391
|
+
|
|
392
|
+
Raises
|
|
393
|
+
------
|
|
394
|
+
ValueError
|
|
395
|
+
If the mapping defines keys outside of the supported sensitivity set.
|
|
396
|
+
|
|
397
|
+
Examples
|
|
398
|
+
--------
|
|
399
|
+
>>> _normalise_si_sensitivity_mapping({"dSi_dvf_norm": 1.0}, warn=False)
|
|
400
|
+
{'dSi_dvf_norm': 1.0}
|
|
401
|
+
>>> _normalise_si_sensitivity_mapping({"unknown": 1.0}, warn=False)
|
|
402
|
+
Traceback (most recent call last):
|
|
403
|
+
...
|
|
404
|
+
ValueError: Si sensitivity mappings accept only {dSi_ddnfr_norm, dSi_dphase_disp, dSi_dvf_norm}; unexpected key(s): unknown
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
normalised = dict(mapping)
|
|
408
|
+
_ = warn # kept for API compatibility with trace helpers
|
|
409
|
+
unexpected = sorted(k for k in normalised if k not in _VALID_SENSITIVITY_KEYS)
|
|
410
|
+
if unexpected:
|
|
411
|
+
allowed = ", ".join(sorted(_VALID_SENSITIVITY_KEYS))
|
|
412
|
+
received = ", ".join(unexpected)
|
|
413
|
+
raise ValueError(
|
|
414
|
+
"Si sensitivity mappings accept only {%s}; unexpected key(s): %s"
|
|
415
|
+
% (allowed, received)
|
|
416
|
+
)
|
|
417
|
+
return normalised
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _cache_weights(G: GraphLike) -> tuple[float, float, float]:
|
|
421
|
+
"""Normalise and persist Si weights attached to the graph coherence.
|
|
422
|
+
|
|
423
|
+
Parameters
|
|
424
|
+
----------
|
|
425
|
+
G : GraphLike
|
|
426
|
+
Graph structure whose global Si sensitivities must be harmonised.
|
|
427
|
+
|
|
428
|
+
Returns
|
|
429
|
+
-------
|
|
430
|
+
tuple[float, float, float]
|
|
431
|
+
Ordered tuple ``(alpha, beta, gamma)`` with normalised Si weights.
|
|
432
|
+
|
|
433
|
+
Raises
|
|
434
|
+
------
|
|
435
|
+
ValueError
|
|
436
|
+
Propagated if the graph stores unsupported sensitivity keys.
|
|
437
|
+
|
|
438
|
+
Examples
|
|
439
|
+
--------
|
|
440
|
+
>>> import networkx as nx
|
|
441
|
+
>>> G = nx.Graph()
|
|
442
|
+
>>> G.graph["SI_WEIGHTS"] = {"alpha": 0.2, "beta": 0.5, "gamma": 0.3}
|
|
443
|
+
>>> tuple(round(v, 2) for v in _cache_weights(G))
|
|
444
|
+
(0.2, 0.5, 0.3)
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
w = merge_graph_weights(G, "SI_WEIGHTS")
|
|
448
|
+
cfg_key = stable_json(w)
|
|
449
|
+
|
|
450
|
+
existing = G.graph.get("_Si_sensitivity")
|
|
451
|
+
if isinstance(existing, Mapping):
|
|
452
|
+
migrated = _normalise_si_sensitivity_mapping(existing, warn=True)
|
|
453
|
+
if migrated != existing:
|
|
454
|
+
G.graph["_Si_sensitivity"] = migrated
|
|
455
|
+
|
|
456
|
+
def builder() -> tuple[float, float, float]:
|
|
457
|
+
weights = normalize_weights(w, ("alpha", "beta", "gamma"), default=0.0)
|
|
458
|
+
alpha = weights["alpha"]
|
|
459
|
+
beta = weights["beta"]
|
|
460
|
+
gamma = weights["gamma"]
|
|
461
|
+
G.graph["_Si_weights"] = weights
|
|
462
|
+
G.graph["_Si_weights_key"] = cfg_key
|
|
463
|
+
G.graph["_Si_sensitivity"] = {
|
|
464
|
+
"dSi_dvf_norm": alpha,
|
|
465
|
+
PHASE_DISPERSION_KEY: -beta,
|
|
466
|
+
"dSi_ddnfr_norm": -gamma,
|
|
467
|
+
}
|
|
468
|
+
return alpha, beta, gamma
|
|
469
|
+
|
|
470
|
+
return edge_version_cache(G, ("_Si_weights", cfg_key), builder)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def get_Si_weights(G: GraphLike) -> tuple[float, float, float]:
|
|
474
|
+
"""Expose the normalised Si weights associated with ``G``.
|
|
475
|
+
|
|
476
|
+
Parameters
|
|
477
|
+
----------
|
|
478
|
+
G : GraphLike
|
|
479
|
+
Graph that carries optional ``SI_WEIGHTS`` metadata.
|
|
480
|
+
|
|
481
|
+
Returns
|
|
482
|
+
-------
|
|
483
|
+
tuple[float, float, float]
|
|
484
|
+
The ``(alpha, beta, gamma)`` weights after normalisation.
|
|
485
|
+
|
|
486
|
+
Examples
|
|
487
|
+
--------
|
|
488
|
+
>>> import networkx as nx
|
|
489
|
+
>>> G = nx.Graph()
|
|
490
|
+
>>> get_Si_weights(G)
|
|
491
|
+
(0.0, 0.0, 0.0)
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
return _cache_weights(G)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def compute_Si_node(
|
|
498
|
+
n: Any,
|
|
499
|
+
nd: dict[str, Any],
|
|
500
|
+
*,
|
|
501
|
+
alpha: float,
|
|
502
|
+
beta: float,
|
|
503
|
+
gamma: float,
|
|
504
|
+
vfmax: float,
|
|
505
|
+
dnfrmax: float,
|
|
506
|
+
phase_dispersion: float | None = None,
|
|
507
|
+
inplace: bool,
|
|
508
|
+
**kwargs: Any,
|
|
509
|
+
) -> float:
|
|
510
|
+
"""Evaluate how a node's structure tilts Si within its local resonance.
|
|
511
|
+
|
|
512
|
+
Parameters
|
|
513
|
+
----------
|
|
514
|
+
n : Any
|
|
515
|
+
Node identifier whose structural perception is computed.
|
|
516
|
+
nd : dict[str, Any]
|
|
517
|
+
Mutable node attributes containing cached structural magnitudes.
|
|
518
|
+
alpha : float
|
|
519
|
+
Normalised weight applied to the node's structural frequency, boosting
|
|
520
|
+
Si when the node reorganises faster than the network baseline.
|
|
521
|
+
beta : float
|
|
522
|
+
Normalised weight applied to the phase alignment term so that tighter
|
|
523
|
+
synchrony raises the index.
|
|
524
|
+
gamma : float
|
|
525
|
+
Normalised weight applied to the ΔNFR attenuation term, rewarding nodes
|
|
526
|
+
that keep internal turbulence under control.
|
|
527
|
+
vfmax : float
|
|
528
|
+
Maximum structural frequency used for normalisation.
|
|
529
|
+
dnfrmax : float
|
|
530
|
+
Maximum |ΔNFR| used for normalisation.
|
|
531
|
+
phase_dispersion : float, optional
|
|
532
|
+
Phase dispersion ratio in ``[0, 1]`` for the node against its
|
|
533
|
+
neighbours. The value must be supplied by the caller.
|
|
534
|
+
inplace : bool
|
|
535
|
+
Whether to write the resulting Si back to ``nd``.
|
|
536
|
+
**kwargs : Any
|
|
537
|
+
Additional keyword arguments are not accepted and will raise.
|
|
538
|
+
|
|
539
|
+
Returns
|
|
540
|
+
-------
|
|
541
|
+
float
|
|
542
|
+
The clamped Si value in ``[0, 1]``.
|
|
543
|
+
|
|
544
|
+
Raises
|
|
545
|
+
------
|
|
546
|
+
TypeError
|
|
547
|
+
If ``phase_dispersion`` is missing or unsupported keyword arguments
|
|
548
|
+
are provided.
|
|
549
|
+
|
|
550
|
+
Examples
|
|
551
|
+
--------
|
|
552
|
+
>>> nd = {"nu_f": 1.0, "delta_nfr": 0.1}
|
|
553
|
+
>>> compute_Si_node(
|
|
554
|
+
... "n0",
|
|
555
|
+
... nd,
|
|
556
|
+
... alpha=0.4,
|
|
557
|
+
... beta=0.3,
|
|
558
|
+
... gamma=0.3,
|
|
559
|
+
... vfmax=1.0,
|
|
560
|
+
... dnfrmax=1.0,
|
|
561
|
+
... phase_dispersion=0.2,
|
|
562
|
+
... inplace=False,
|
|
563
|
+
... )
|
|
564
|
+
0.91
|
|
565
|
+
"""
|
|
566
|
+
|
|
567
|
+
if kwargs:
|
|
568
|
+
unexpected = ", ".join(sorted(kwargs))
|
|
569
|
+
raise TypeError(f"Unexpected keyword argument(s): {unexpected}")
|
|
570
|
+
|
|
571
|
+
if phase_dispersion is None:
|
|
572
|
+
raise TypeError("Missing required keyword-only argument: 'phase_dispersion'")
|
|
573
|
+
|
|
574
|
+
vf = get_attr(nd, ALIAS_VF, 0.0)
|
|
575
|
+
vf_norm = clamp01(abs(vf) / vfmax)
|
|
576
|
+
|
|
577
|
+
dnfr = get_attr(nd, ALIAS_DNFR, 0.0)
|
|
578
|
+
dnfr_norm = clamp01(abs(dnfr) / dnfrmax)
|
|
579
|
+
|
|
580
|
+
Si = alpha * vf_norm + beta * (1.0 - phase_dispersion) + gamma * (1.0 - dnfr_norm)
|
|
581
|
+
Si = clamp01(Si)
|
|
582
|
+
if inplace:
|
|
583
|
+
set_attr(nd, ALIAS_SI, Si)
|
|
584
|
+
return Si
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _compute_si_python_chunk(
|
|
588
|
+
chunk: Iterable[tuple[Any, tuple[Any, ...], float, float, float]],
|
|
589
|
+
*,
|
|
590
|
+
cos_th: dict[Any, float],
|
|
591
|
+
sin_th: dict[Any, float],
|
|
592
|
+
alpha: float,
|
|
593
|
+
beta: float,
|
|
594
|
+
gamma: float,
|
|
595
|
+
vfmax: float,
|
|
596
|
+
dnfrmax: float,
|
|
597
|
+
) -> dict[Any, float]:
|
|
598
|
+
"""Propagate Si contributions for a node chunk using pure Python.
|
|
599
|
+
|
|
600
|
+
The fallback keeps the νf/phase/ΔNFR balance explicit so that structural
|
|
601
|
+
effects remain traceable even without vectorised support.
|
|
602
|
+
|
|
603
|
+
Parameters
|
|
604
|
+
----------
|
|
605
|
+
chunk : Iterable[tuple[Any, tuple[Any, ...], float, float, float]]
|
|
606
|
+
Iterable of node payloads ``(node, neighbors, theta, vf, dnfr)``.
|
|
607
|
+
cos_th : dict[Any, float]
|
|
608
|
+
Cached cosine values keyed by node identifiers.
|
|
609
|
+
sin_th : dict[Any, float]
|
|
610
|
+
Cached sine values keyed by node identifiers.
|
|
611
|
+
alpha : float
|
|
612
|
+
Normalised weight for structural frequency.
|
|
613
|
+
beta : float
|
|
614
|
+
Normalised weight for phase dispersion.
|
|
615
|
+
gamma : float
|
|
616
|
+
Normalised weight for ΔNFR dispersion.
|
|
617
|
+
vfmax : float
|
|
618
|
+
Maximum |νf| reference for normalisation.
|
|
619
|
+
dnfrmax : float
|
|
620
|
+
Maximum |ΔNFR| reference for normalisation.
|
|
621
|
+
|
|
622
|
+
Returns
|
|
623
|
+
-------
|
|
624
|
+
dict[Any, float]
|
|
625
|
+
Mapping of node identifiers to their clamped Si values.
|
|
626
|
+
|
|
627
|
+
Examples
|
|
628
|
+
--------
|
|
629
|
+
>>> _compute_si_python_chunk(
|
|
630
|
+
... [("n0", ("n1",), 0.0, 0.5, 0.1)],
|
|
631
|
+
... cos_th={"n1": 1.0},
|
|
632
|
+
... sin_th={"n1": 0.0},
|
|
633
|
+
... alpha=0.5,
|
|
634
|
+
... beta=0.3,
|
|
635
|
+
... gamma=0.2,
|
|
636
|
+
... vfmax=1.0,
|
|
637
|
+
... dnfrmax=1.0,
|
|
638
|
+
... )
|
|
639
|
+
{'n0': 0.73}
|
|
640
|
+
"""
|
|
641
|
+
|
|
642
|
+
results: dict[Any, float] = {}
|
|
643
|
+
for n, neigh, theta, vf, dnfr in chunk:
|
|
644
|
+
th_bar = neighbor_phase_mean_list(
|
|
645
|
+
neigh, cos_th=cos_th, sin_th=sin_th, np=None, fallback=theta
|
|
646
|
+
)
|
|
647
|
+
phase_dispersion = abs(angle_diff(theta, th_bar)) / math.pi
|
|
648
|
+
vf_norm = clamp01(abs(vf) / vfmax)
|
|
649
|
+
dnfr_norm = clamp01(abs(dnfr) / dnfrmax)
|
|
650
|
+
Si = (
|
|
651
|
+
alpha * vf_norm
|
|
652
|
+
+ beta * (1.0 - phase_dispersion)
|
|
653
|
+
+ gamma * (1.0 - dnfr_norm)
|
|
654
|
+
)
|
|
655
|
+
results[n] = clamp01(Si)
|
|
656
|
+
return results
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _iter_python_payload_chunks(
|
|
660
|
+
nodes_data: Iterable[tuple[Any, NodeAttrMap]],
|
|
661
|
+
*,
|
|
662
|
+
neighbors: Mapping[Any, Iterable[Any]],
|
|
663
|
+
thetas: Mapping[Any, float],
|
|
664
|
+
chunk_size: int,
|
|
665
|
+
) -> Iterator[tuple[tuple[Any, tuple[Any, ...], float, float, float], ...]]:
|
|
666
|
+
"""Yield lazily constructed Si payload chunks for the Python fallback.
|
|
667
|
+
|
|
668
|
+
Each batch keeps the structural triad explicit—θ, νf, and ΔNFR—so that the
|
|
669
|
+
downstream worker preserves the coherence balance enforced by the Si
|
|
670
|
+
operator. Streaming prevents a single monolithic buffer that would skew
|
|
671
|
+
memory pressure on dense graphs while still producing deterministic ΔNFR
|
|
672
|
+
sampling. The iterator is consumed lazily by :func:`compute_Si` so that the
|
|
673
|
+
Python fallback can submit and harvest chunk results incrementally, keeping
|
|
674
|
+
both memory usage and profiling telemetry representative of the streamed
|
|
675
|
+
execution.
|
|
676
|
+
"""
|
|
677
|
+
|
|
678
|
+
if chunk_size <= 0:
|
|
679
|
+
return
|
|
680
|
+
|
|
681
|
+
buffer: list[tuple[Any, tuple[Any, ...], float, float, float]] = []
|
|
682
|
+
for node, data in nodes_data:
|
|
683
|
+
theta = thetas.get(node, 0.0)
|
|
684
|
+
vf = float(get_attr(data, ALIAS_VF, 0.0))
|
|
685
|
+
dnfr = float(get_attr(data, ALIAS_DNFR, 0.0))
|
|
686
|
+
neigh = tuple(neighbors[node])
|
|
687
|
+
buffer.append((node, neigh, theta, vf, dnfr))
|
|
688
|
+
if len(buffer) >= chunk_size:
|
|
689
|
+
yield tuple(buffer)
|
|
690
|
+
buffer.clear()
|
|
691
|
+
|
|
692
|
+
if buffer:
|
|
693
|
+
yield tuple(buffer)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def compute_Si(
|
|
697
|
+
G: GraphLike,
|
|
698
|
+
*,
|
|
699
|
+
inplace: bool = True,
|
|
700
|
+
n_jobs: int | None = None,
|
|
701
|
+
chunk_size: int | None = None,
|
|
702
|
+
profile: MutableMapping[str, Any] | None = None,
|
|
703
|
+
) -> dict[Any, float] | Any:
|
|
704
|
+
"""Compute the Si metric for each node by integrating structural drivers.
|
|
705
|
+
|
|
706
|
+
Si (sense index) quantifies how effectively a node sustains coherent
|
|
707
|
+
reorganisation within the TNFR triad. The metric aggregates three
|
|
708
|
+
structural contributions: the node's structural frequency (weighted by
|
|
709
|
+
``alpha``), its phase alignment with neighbours (weighted by ``beta``),
|
|
710
|
+
and the attenuation of disruptive ΔNFR (weighted by ``gamma``). The
|
|
711
|
+
weights therefore bias Si towards faster reorganisation, tighter phase
|
|
712
|
+
coupling, or reduced dissonance respectively, depending on the scenario.
|
|
713
|
+
|
|
714
|
+
Parameters
|
|
715
|
+
----------
|
|
716
|
+
G : GraphLike
|
|
717
|
+
Graph that exposes ``νf`` (structural frequency), ``ΔNFR`` and phase
|
|
718
|
+
attributes for each node.
|
|
719
|
+
inplace : bool, default: True
|
|
720
|
+
If ``True`` the resulting Si values are written back to ``G``.
|
|
721
|
+
n_jobs : int or None, optional
|
|
722
|
+
Maximum number of worker processes for the pure-Python fallback. Use
|
|
723
|
+
``None`` to auto-detect the configuration.
|
|
724
|
+
chunk_size : int or None, optional
|
|
725
|
+
Maximum number of nodes processed per batch when building the Si
|
|
726
|
+
mapping. ``None`` derives a safe value from the node count, the
|
|
727
|
+
available CPUs, and conservative memory heuristics. Non-positive values
|
|
728
|
+
fall back to the automatic mode. Graphs may also provide a default via
|
|
729
|
+
``G.graph["SI_CHUNK_SIZE"]``.
|
|
730
|
+
profile : MutableMapping[str, Any] or None, optional
|
|
731
|
+
Mutable mapping that aggregates wall-clock durations for the internal
|
|
732
|
+
stages of the computation. The mapping receives the keys
|
|
733
|
+
``"cache_rebuild"``, ``"neighbor_phase_mean_bulk"``,
|
|
734
|
+
``"normalize_clamp"`` and ``"inplace_write"`` accumulating seconds for
|
|
735
|
+
each step, plus ``"path"`` describing whether the vectorised (NumPy)
|
|
736
|
+
or fallback implementation executed the call. When the Python fallback
|
|
737
|
+
streams chunk execution, ``"fallback_chunks"`` records how many payload
|
|
738
|
+
batches completed. Reusing the mapping across invocations accumulates
|
|
739
|
+
the timings and chunk counts.
|
|
740
|
+
|
|
741
|
+
Returns
|
|
742
|
+
-------
|
|
743
|
+
dict[Any, float] | numpy.ndarray
|
|
744
|
+
Mapping from node identifiers to their Si scores when ``inplace`` is
|
|
745
|
+
``False``. When ``inplace`` is ``True`` and the NumPy accelerated path
|
|
746
|
+
is available the function updates the graph in place and returns the
|
|
747
|
+
vector of Si values as a :class:`numpy.ndarray`. The pure-Python
|
|
748
|
+
fallback always returns a mapping for compatibility.
|
|
749
|
+
|
|
750
|
+
Raises
|
|
751
|
+
------
|
|
752
|
+
ValueError
|
|
753
|
+
Propagated if graph-level sensitivity settings include unsupported
|
|
754
|
+
keys or invalid weights.
|
|
755
|
+
|
|
756
|
+
Examples
|
|
757
|
+
--------
|
|
758
|
+
Build a minimal resonance graph with two nodes sharing a phase-locked
|
|
759
|
+
edge. The structural weights bias the result towards phase coherence.
|
|
760
|
+
|
|
761
|
+
>>> import networkx as nx
|
|
762
|
+
>>> from tnfr.metrics.sense_index import compute_Si
|
|
763
|
+
>>> G = nx.Graph()
|
|
764
|
+
>>> G.add_edge("a", "b")
|
|
765
|
+
>>> G.nodes["a"].update({"nu_f": 0.8, "delta_nfr": 0.2, "phase": 0.0})
|
|
766
|
+
>>> G.nodes["b"].update({"nu_f": 0.6, "delta_nfr": 0.1, "phase": 0.1})
|
|
767
|
+
>>> G.graph["SI_WEIGHTS"] = {"alpha": 0.3, "beta": 0.5, "gamma": 0.2}
|
|
768
|
+
>>> {k: round(v, 3) for k, v in compute_Si(G, inplace=False).items()}
|
|
769
|
+
{'a': 0.784, 'b': 0.809}
|
|
770
|
+
"""
|
|
771
|
+
|
|
772
|
+
if profile is not None:
|
|
773
|
+
for key in (
|
|
774
|
+
"cache_rebuild",
|
|
775
|
+
"neighbor_phase_mean_bulk",
|
|
776
|
+
"normalize_clamp",
|
|
777
|
+
"inplace_write",
|
|
778
|
+
"fallback_chunks",
|
|
779
|
+
):
|
|
780
|
+
profile.setdefault(key, 0.0)
|
|
781
|
+
|
|
782
|
+
def _profile_start() -> float:
|
|
783
|
+
return perf_counter()
|
|
784
|
+
|
|
785
|
+
def _profile_stop(key: str, start: float) -> None:
|
|
786
|
+
profile[key] = float(profile.get(key, 0.0)) + (perf_counter() - start)
|
|
787
|
+
|
|
788
|
+
def _profile_mark_path(path: str) -> None:
|
|
789
|
+
profile["path"] = path
|
|
790
|
+
|
|
791
|
+
else:
|
|
792
|
+
|
|
793
|
+
def _profile_start() -> float:
|
|
794
|
+
return 0.0
|
|
795
|
+
|
|
796
|
+
def _profile_stop(key: str, start: float) -> None:
|
|
797
|
+
return None
|
|
798
|
+
|
|
799
|
+
def _profile_mark_path(path: str) -> None:
|
|
800
|
+
return None
|
|
801
|
+
|
|
802
|
+
neighbors = ensure_neighbors_map(G)
|
|
803
|
+
alpha, beta, gamma = get_Si_weights(G)
|
|
804
|
+
np = get_numpy()
|
|
805
|
+
trig = get_trig_cache(G, np=np)
|
|
806
|
+
cos_th, sin_th, thetas = trig.cos, trig.sin, trig.theta
|
|
807
|
+
|
|
808
|
+
pm_fn = partial(neighbor_phase_mean_list, cos_th=cos_th, sin_th=sin_th, np=np)
|
|
809
|
+
|
|
810
|
+
if n_jobs is None:
|
|
811
|
+
n_jobs = _coerce_jobs(G.graph.get("SI_N_JOBS"))
|
|
812
|
+
else:
|
|
813
|
+
n_jobs = _coerce_jobs(n_jobs)
|
|
814
|
+
|
|
815
|
+
supports_vector = (
|
|
816
|
+
np is not None
|
|
817
|
+
and hasattr(np, "ndarray")
|
|
818
|
+
and all(
|
|
819
|
+
hasattr(np, attr)
|
|
820
|
+
for attr in (
|
|
821
|
+
"fromiter",
|
|
822
|
+
"abs",
|
|
823
|
+
"clip",
|
|
824
|
+
"remainder",
|
|
825
|
+
"zeros",
|
|
826
|
+
"add",
|
|
827
|
+
"bincount",
|
|
828
|
+
"arctan2",
|
|
829
|
+
"where",
|
|
830
|
+
"divide",
|
|
831
|
+
"errstate",
|
|
832
|
+
"max",
|
|
833
|
+
)
|
|
834
|
+
)
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
nodes_view = G.nodes
|
|
838
|
+
nodes_data = list(nodes_view(data=True))
|
|
839
|
+
if not nodes_data:
|
|
840
|
+
return {}
|
|
841
|
+
|
|
842
|
+
node_mapping = cast(Mapping[Any, NodeAttrMap], nodes_view)
|
|
843
|
+
node_count = len(nodes_data)
|
|
844
|
+
|
|
845
|
+
trig_order = list(getattr(trig, "order", ()))
|
|
846
|
+
node_ids: list[Any]
|
|
847
|
+
node_idx: dict[Any, int]
|
|
848
|
+
using_cache_order = False
|
|
849
|
+
if trig_order and len(trig_order) == node_count:
|
|
850
|
+
node_ids = trig_order
|
|
851
|
+
node_idx = dict(getattr(trig, "index", {}))
|
|
852
|
+
using_cache_order = len(node_idx) == len(node_ids)
|
|
853
|
+
if not using_cache_order:
|
|
854
|
+
node_idx = {n: i for i, n in enumerate(node_ids)}
|
|
855
|
+
else:
|
|
856
|
+
node_ids = [n for n, _ in nodes_data]
|
|
857
|
+
node_idx = {n: i for i, n in enumerate(node_ids)}
|
|
858
|
+
|
|
859
|
+
chunk_pref = chunk_size if chunk_size is not None else G.graph.get("SI_CHUNK_SIZE")
|
|
860
|
+
|
|
861
|
+
if supports_vector:
|
|
862
|
+
_profile_mark_path("vectorized")
|
|
863
|
+
node_key = tuple(node_ids)
|
|
864
|
+
count = len(node_key)
|
|
865
|
+
|
|
866
|
+
cache_theta = getattr(trig, "theta_values", None)
|
|
867
|
+
cache_cos = getattr(trig, "cos_values", None)
|
|
868
|
+
cache_sin = getattr(trig, "sin_values", None)
|
|
869
|
+
|
|
870
|
+
trig_index_map = dict(getattr(trig, "index", {}) or {})
|
|
871
|
+
index_arr: Any | None = None
|
|
872
|
+
cached_mask = None
|
|
873
|
+
if trig_index_map and count:
|
|
874
|
+
index_values: list[int] = []
|
|
875
|
+
mask_values: list[bool] = []
|
|
876
|
+
for node in node_ids:
|
|
877
|
+
cached_idx = trig_index_map.get(node)
|
|
878
|
+
if cached_idx is None:
|
|
879
|
+
index_values.append(-1)
|
|
880
|
+
mask_values.append(False)
|
|
881
|
+
else:
|
|
882
|
+
index_values.append(int(cached_idx))
|
|
883
|
+
mask_values.append(True)
|
|
884
|
+
cached_mask = np.asarray(mask_values, dtype=bool)
|
|
885
|
+
if cached_mask.any():
|
|
886
|
+
index_arr = np.asarray(index_values, dtype=np.intp)
|
|
887
|
+
if cached_mask is None:
|
|
888
|
+
cached_mask = np.zeros(count, dtype=bool)
|
|
889
|
+
|
|
890
|
+
def _gather_values(
|
|
891
|
+
cache_values: Any | None, fallback_getter: Callable[[Any], float]
|
|
892
|
+
) -> Any:
|
|
893
|
+
if (
|
|
894
|
+
index_arr is not None
|
|
895
|
+
and cache_values is not None
|
|
896
|
+
and cached_mask.size
|
|
897
|
+
and cached_mask.any()
|
|
898
|
+
):
|
|
899
|
+
out = np.empty(count, dtype=float)
|
|
900
|
+
cached_indices = np.nonzero(cached_mask)[0]
|
|
901
|
+
if cached_indices.size:
|
|
902
|
+
out[cached_indices] = np.take(
|
|
903
|
+
np.asarray(cache_values, dtype=float), index_arr[cached_indices]
|
|
904
|
+
)
|
|
905
|
+
missing_indices = np.nonzero(~cached_mask)[0]
|
|
906
|
+
if missing_indices.size:
|
|
907
|
+
missing_nodes = [node_ids[i] for i in missing_indices]
|
|
908
|
+
out[missing_indices] = np.fromiter(
|
|
909
|
+
(fallback_getter(node) for node in missing_nodes),
|
|
910
|
+
dtype=float,
|
|
911
|
+
count=missing_indices.size,
|
|
912
|
+
)
|
|
913
|
+
return out
|
|
914
|
+
return np.fromiter(
|
|
915
|
+
(fallback_getter(node) for node in node_ids),
|
|
916
|
+
dtype=float,
|
|
917
|
+
count=count,
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
cache_timer = _profile_start()
|
|
921
|
+
|
|
922
|
+
if using_cache_order and cache_theta is not None:
|
|
923
|
+
theta_arr = np.asarray(cache_theta, dtype=float)
|
|
924
|
+
else:
|
|
925
|
+
theta_arr = _gather_values(cache_theta, lambda node: thetas.get(node, 0.0))
|
|
926
|
+
|
|
927
|
+
if using_cache_order and cache_cos is not None:
|
|
928
|
+
cos_arr = np.asarray(cache_cos, dtype=float)
|
|
929
|
+
else:
|
|
930
|
+
cos_arr = _gather_values(
|
|
931
|
+
cache_cos,
|
|
932
|
+
lambda node: cos_th.get(node, math.cos(thetas.get(node, 0.0))),
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
if using_cache_order and cache_sin is not None:
|
|
936
|
+
sin_arr = np.asarray(cache_sin, dtype=float)
|
|
937
|
+
else:
|
|
938
|
+
sin_arr = _gather_values(
|
|
939
|
+
cache_sin,
|
|
940
|
+
lambda node: sin_th.get(node, math.sin(thetas.get(node, 0.0))),
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
cached_edge_src = None
|
|
944
|
+
cached_edge_dst = None
|
|
945
|
+
if using_cache_order:
|
|
946
|
+
cached_edge_src = getattr(trig, "edge_src", None)
|
|
947
|
+
cached_edge_dst = getattr(trig, "edge_dst", None)
|
|
948
|
+
if cached_edge_src is not None and cached_edge_dst is not None:
|
|
949
|
+
cached_edge_src = np.asarray(cached_edge_src, dtype=np.intp)
|
|
950
|
+
cached_edge_dst = np.asarray(cached_edge_dst, dtype=np.intp)
|
|
951
|
+
if cached_edge_src.shape != cached_edge_dst.shape:
|
|
952
|
+
cached_edge_src = None
|
|
953
|
+
cached_edge_dst = None
|
|
954
|
+
|
|
955
|
+
if cached_edge_src is not None and cached_edge_dst is not None:
|
|
956
|
+
edge_src = cached_edge_src
|
|
957
|
+
edge_dst = cached_edge_dst
|
|
958
|
+
else:
|
|
959
|
+
|
|
960
|
+
def _build_edge_arrays() -> tuple[Any, Any]:
|
|
961
|
+
edge_src_list: list[int] = []
|
|
962
|
+
edge_dst_list: list[int] = []
|
|
963
|
+
for node in node_ids:
|
|
964
|
+
dst_idx = node_idx[node]
|
|
965
|
+
for neighbor in neighbors[node]:
|
|
966
|
+
src_idx = node_idx.get(neighbor)
|
|
967
|
+
if src_idx is None:
|
|
968
|
+
continue
|
|
969
|
+
edge_src_list.append(src_idx)
|
|
970
|
+
edge_dst_list.append(dst_idx)
|
|
971
|
+
src_arr = np.asarray(edge_src_list, dtype=np.intp)
|
|
972
|
+
dst_arr = np.asarray(edge_dst_list, dtype=np.intp)
|
|
973
|
+
return src_arr, dst_arr
|
|
974
|
+
|
|
975
|
+
edge_src, edge_dst = edge_version_cache(
|
|
976
|
+
G,
|
|
977
|
+
("_si_edges", node_key),
|
|
978
|
+
_build_edge_arrays,
|
|
979
|
+
)
|
|
980
|
+
if using_cache_order:
|
|
981
|
+
trig.edge_src = edge_src
|
|
982
|
+
trig.edge_dst = edge_dst
|
|
983
|
+
|
|
984
|
+
(
|
|
985
|
+
neighbor_cos_sum,
|
|
986
|
+
neighbor_sin_sum,
|
|
987
|
+
neighbor_counts,
|
|
988
|
+
mean_cos_buf,
|
|
989
|
+
mean_sin_buf,
|
|
990
|
+
) = _ensure_neighbor_bulk_buffers(
|
|
991
|
+
G,
|
|
992
|
+
count=count,
|
|
993
|
+
np=np,
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
vf_arr, dnfr_arr = _ensure_structural_arrays(
|
|
997
|
+
G,
|
|
998
|
+
node_ids,
|
|
999
|
+
node_mapping,
|
|
1000
|
+
np=np,
|
|
1001
|
+
)
|
|
1002
|
+
raw_vfmax = float(np.max(np.abs(vf_arr))) if getattr(vf_arr, "size", 0) else 0.0
|
|
1003
|
+
raw_dnfrmax = (
|
|
1004
|
+
float(np.max(np.abs(dnfr_arr))) if getattr(dnfr_arr, "size", 0) else 0.0
|
|
1005
|
+
)
|
|
1006
|
+
G.graph["_vfmax"] = raw_vfmax
|
|
1007
|
+
G.graph["_dnfrmax"] = raw_dnfrmax
|
|
1008
|
+
vfmax = 1.0 if raw_vfmax == 0.0 else raw_vfmax
|
|
1009
|
+
dnfrmax = 1.0 if raw_dnfrmax == 0.0 else raw_dnfrmax
|
|
1010
|
+
|
|
1011
|
+
(
|
|
1012
|
+
phase_dispersion,
|
|
1013
|
+
raw_si,
|
|
1014
|
+
si_values,
|
|
1015
|
+
) = _ensure_si_buffers(
|
|
1016
|
+
G,
|
|
1017
|
+
count=count,
|
|
1018
|
+
np=np,
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
_profile_stop("cache_rebuild", cache_timer)
|
|
1022
|
+
|
|
1023
|
+
neighbor_timer = _profile_start()
|
|
1024
|
+
mean_theta, has_neighbors = neighbor_phase_mean_bulk(
|
|
1025
|
+
edge_src,
|
|
1026
|
+
edge_dst,
|
|
1027
|
+
cos_values=cos_arr,
|
|
1028
|
+
sin_values=sin_arr,
|
|
1029
|
+
theta_values=theta_arr,
|
|
1030
|
+
node_count=count,
|
|
1031
|
+
np=np,
|
|
1032
|
+
neighbor_cos_sum=neighbor_cos_sum,
|
|
1033
|
+
neighbor_sin_sum=neighbor_sin_sum,
|
|
1034
|
+
neighbor_counts=neighbor_counts,
|
|
1035
|
+
mean_cos=mean_cos_buf,
|
|
1036
|
+
mean_sin=mean_sin_buf,
|
|
1037
|
+
)
|
|
1038
|
+
_profile_stop("neighbor_phase_mean_bulk", neighbor_timer)
|
|
1039
|
+
norm_timer = _profile_start()
|
|
1040
|
+
# Reuse the Si buffers as scratch space to avoid transient allocations during
|
|
1041
|
+
# the normalization pass and keep the structural buffers coherent with the
|
|
1042
|
+
# cached layout.
|
|
1043
|
+
np.abs(vf_arr, out=raw_si)
|
|
1044
|
+
np.divide(raw_si, vfmax, out=raw_si)
|
|
1045
|
+
np.clip(raw_si, 0.0, 1.0, out=raw_si)
|
|
1046
|
+
vf_norm = raw_si
|
|
1047
|
+
np.abs(dnfr_arr, out=si_values)
|
|
1048
|
+
np.divide(si_values, dnfrmax, out=si_values)
|
|
1049
|
+
np.clip(si_values, 0.0, 1.0, out=si_values)
|
|
1050
|
+
dnfr_norm = si_values
|
|
1051
|
+
phase_dispersion.fill(0.0)
|
|
1052
|
+
neighbor_mask = np.asarray(has_neighbors, dtype=bool)
|
|
1053
|
+
neighbor_count = int(neighbor_mask.sum())
|
|
1054
|
+
use_chunked = False
|
|
1055
|
+
if neighbor_count:
|
|
1056
|
+
effective_chunk = resolve_chunk_size(
|
|
1057
|
+
chunk_pref,
|
|
1058
|
+
neighbor_count,
|
|
1059
|
+
approx_bytes_per_item=_SI_APPROX_BYTES_PER_NODE,
|
|
1060
|
+
)
|
|
1061
|
+
if effective_chunk <= 0 or effective_chunk >= neighbor_count:
|
|
1062
|
+
effective_chunk = neighbor_count
|
|
1063
|
+
else:
|
|
1064
|
+
use_chunked = True
|
|
1065
|
+
|
|
1066
|
+
if neighbor_count and not use_chunked:
|
|
1067
|
+
angle_diff_array(
|
|
1068
|
+
theta_arr,
|
|
1069
|
+
mean_theta,
|
|
1070
|
+
np=np,
|
|
1071
|
+
out=phase_dispersion,
|
|
1072
|
+
where=neighbor_mask,
|
|
1073
|
+
)
|
|
1074
|
+
np.abs(phase_dispersion, out=phase_dispersion, where=neighbor_mask)
|
|
1075
|
+
np.divide(
|
|
1076
|
+
phase_dispersion,
|
|
1077
|
+
math.pi,
|
|
1078
|
+
out=phase_dispersion,
|
|
1079
|
+
where=neighbor_mask,
|
|
1080
|
+
)
|
|
1081
|
+
elif neighbor_count and use_chunked:
|
|
1082
|
+
neighbor_indices = np.nonzero(neighbor_mask)[0]
|
|
1083
|
+
chunk_theta, chunk_values = _ensure_chunk_workspace(
|
|
1084
|
+
G,
|
|
1085
|
+
mask_count=neighbor_count,
|
|
1086
|
+
np=np,
|
|
1087
|
+
)
|
|
1088
|
+
for start in range(0, neighbor_count, effective_chunk):
|
|
1089
|
+
end = min(start + effective_chunk, neighbor_count)
|
|
1090
|
+
slice_indices = neighbor_indices[start:end]
|
|
1091
|
+
chunk_len = end - start
|
|
1092
|
+
theta_view = chunk_theta[:chunk_len]
|
|
1093
|
+
values_view = chunk_values[:chunk_len]
|
|
1094
|
+
np.take(theta_arr, slice_indices, out=theta_view)
|
|
1095
|
+
np.take(mean_theta, slice_indices, out=values_view)
|
|
1096
|
+
angle_diff_array(theta_view, values_view, np=np, out=values_view)
|
|
1097
|
+
np.abs(values_view, out=values_view)
|
|
1098
|
+
np.divide(values_view, math.pi, out=values_view)
|
|
1099
|
+
phase_dispersion[slice_indices] = values_view
|
|
1100
|
+
else:
|
|
1101
|
+
np.abs(phase_dispersion, out=phase_dispersion)
|
|
1102
|
+
np.divide(
|
|
1103
|
+
phase_dispersion,
|
|
1104
|
+
math.pi,
|
|
1105
|
+
out=phase_dispersion,
|
|
1106
|
+
where=neighbor_mask,
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
np.multiply(vf_norm, alpha, out=raw_si)
|
|
1110
|
+
np.subtract(1.0, phase_dispersion, out=phase_dispersion)
|
|
1111
|
+
np.multiply(phase_dispersion, beta, out=phase_dispersion)
|
|
1112
|
+
np.add(raw_si, phase_dispersion, out=raw_si)
|
|
1113
|
+
np.subtract(1.0, dnfr_norm, out=si_values)
|
|
1114
|
+
np.multiply(si_values, gamma, out=si_values)
|
|
1115
|
+
np.add(raw_si, si_values, out=raw_si)
|
|
1116
|
+
np.clip(raw_si, 0.0, 1.0, out=si_values)
|
|
1117
|
+
|
|
1118
|
+
_profile_stop("normalize_clamp", norm_timer)
|
|
1119
|
+
|
|
1120
|
+
if inplace:
|
|
1121
|
+
write_timer = _profile_start()
|
|
1122
|
+
for idx, node in enumerate(node_ids):
|
|
1123
|
+
set_attr(G.nodes[node], ALIAS_SI, float(si_values[idx]))
|
|
1124
|
+
_profile_stop("inplace_write", write_timer)
|
|
1125
|
+
return np.copy(si_values)
|
|
1126
|
+
|
|
1127
|
+
return {node: float(value) for node, value in zip(node_ids, si_values)}
|
|
1128
|
+
|
|
1129
|
+
vfmax, dnfrmax = _get_vf_dnfr_max(G)
|
|
1130
|
+
|
|
1131
|
+
out: dict[Any, float] = {}
|
|
1132
|
+
_profile_mark_path("fallback")
|
|
1133
|
+
if n_jobs is not None and n_jobs > 1:
|
|
1134
|
+
node_count = len(nodes_data)
|
|
1135
|
+
if node_count:
|
|
1136
|
+
effective_chunk = resolve_chunk_size(
|
|
1137
|
+
chunk_pref,
|
|
1138
|
+
node_count,
|
|
1139
|
+
approx_bytes_per_item=_SI_APPROX_BYTES_PER_NODE,
|
|
1140
|
+
)
|
|
1141
|
+
if effective_chunk <= 0:
|
|
1142
|
+
effective_chunk = node_count
|
|
1143
|
+
payload_chunks = _iter_python_payload_chunks(
|
|
1144
|
+
nodes_data,
|
|
1145
|
+
neighbors=neighbors,
|
|
1146
|
+
thetas=thetas,
|
|
1147
|
+
chunk_size=effective_chunk,
|
|
1148
|
+
)
|
|
1149
|
+
chunk_count = 0
|
|
1150
|
+
with ProcessPoolExecutor(max_workers=n_jobs) as executor:
|
|
1151
|
+
worker = partial(
|
|
1152
|
+
_compute_si_python_chunk,
|
|
1153
|
+
cos_th=cos_th,
|
|
1154
|
+
sin_th=sin_th,
|
|
1155
|
+
alpha=alpha,
|
|
1156
|
+
beta=beta,
|
|
1157
|
+
gamma=gamma,
|
|
1158
|
+
vfmax=vfmax,
|
|
1159
|
+
dnfrmax=dnfrmax,
|
|
1160
|
+
)
|
|
1161
|
+
payload_iter = iter(payload_chunks)
|
|
1162
|
+
futures: list[Any] = []
|
|
1163
|
+
for chunk in payload_iter:
|
|
1164
|
+
futures.append(executor.submit(worker, chunk))
|
|
1165
|
+
if len(futures) >= n_jobs:
|
|
1166
|
+
future = futures.pop(0)
|
|
1167
|
+
chunk_result = future.result()
|
|
1168
|
+
chunk_count += 1
|
|
1169
|
+
out.update(chunk_result)
|
|
1170
|
+
for future in futures:
|
|
1171
|
+
chunk_result = future.result()
|
|
1172
|
+
chunk_count += 1
|
|
1173
|
+
out.update(chunk_result)
|
|
1174
|
+
if profile is not None:
|
|
1175
|
+
profile["fallback_chunks"] = float(
|
|
1176
|
+
profile.get("fallback_chunks", 0.0)
|
|
1177
|
+
) + float(chunk_count)
|
|
1178
|
+
else:
|
|
1179
|
+
for n, nd in nodes_data:
|
|
1180
|
+
theta = thetas.get(n, 0.0)
|
|
1181
|
+
neigh = neighbors[n]
|
|
1182
|
+
th_bar = pm_fn(neigh, fallback=theta)
|
|
1183
|
+
phase_dispersion = abs(angle_diff(theta, th_bar)) / math.pi
|
|
1184
|
+
norm_timer = _profile_start()
|
|
1185
|
+
out[n] = compute_Si_node(
|
|
1186
|
+
n,
|
|
1187
|
+
nd,
|
|
1188
|
+
alpha=alpha,
|
|
1189
|
+
beta=beta,
|
|
1190
|
+
gamma=gamma,
|
|
1191
|
+
vfmax=vfmax,
|
|
1192
|
+
dnfrmax=dnfrmax,
|
|
1193
|
+
phase_dispersion=phase_dispersion,
|
|
1194
|
+
inplace=False,
|
|
1195
|
+
)
|
|
1196
|
+
_profile_stop("normalize_clamp", norm_timer)
|
|
1197
|
+
|
|
1198
|
+
if inplace:
|
|
1199
|
+
write_timer = _profile_start()
|
|
1200
|
+
for n, value in out.items():
|
|
1201
|
+
set_attr(G.nodes[n], ALIAS_SI, value)
|
|
1202
|
+
_profile_stop("inplace_write", write_timer)
|
|
1203
|
+
return out
|