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,229 @@
|
|
|
1
|
+
"""Canonical TNFR nodal equation implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the explicit, canonical implementation of the fundamental
|
|
4
|
+
TNFR nodal equation as specified in the theory:
|
|
5
|
+
|
|
6
|
+
∂EPI/∂t = νf · ΔNFR(t)
|
|
7
|
+
|
|
8
|
+
Where:
|
|
9
|
+
- EPI: Primary Information Structure (coherent form)
|
|
10
|
+
- νf: Structural frequency in Hz_str (structural hertz)
|
|
11
|
+
- ΔNFR: Nodal gradient (reorganization operator)
|
|
12
|
+
- t: Structural time (not chronological time)
|
|
13
|
+
|
|
14
|
+
This implementation ensures theoretical fidelity to the TNFR paradigm by:
|
|
15
|
+
1. Making the canonical equation explicit in code
|
|
16
|
+
2. Validating dimensional consistency (Hz_str units)
|
|
17
|
+
3. Providing clear mapping between theory and implementation
|
|
18
|
+
4. Maintaining reproducibility and traceability
|
|
19
|
+
|
|
20
|
+
TNFR Invariants (from AGENTS.md):
|
|
21
|
+
- EPI as coherent form: changes only via structural operators
|
|
22
|
+
- Structural units: νf expressed in Hz_str (structural hertz)
|
|
23
|
+
- ΔNFR semantics: sign and magnitude modulate reorganization rate
|
|
24
|
+
- Operator closure: composition yields valid TNFR states
|
|
25
|
+
|
|
26
|
+
References:
|
|
27
|
+
- TNFR.pdf: Canonical nodal equation specification
|
|
28
|
+
- AGENTS.md: Section 3 (Canonical invariants)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import math
|
|
34
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from ..types import GraphLike
|
|
38
|
+
|
|
39
|
+
__all__ = (
|
|
40
|
+
"NodalEquationResult",
|
|
41
|
+
"compute_canonical_nodal_derivative",
|
|
42
|
+
"validate_structural_frequency",
|
|
43
|
+
"validate_nodal_gradient",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NodalEquationResult(NamedTuple):
|
|
48
|
+
"""Result of canonical nodal equation evaluation.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
derivative: ∂EPI/∂t computed from νf · ΔNFR(t)
|
|
52
|
+
nu_f: Structural frequency (Hz_str) used in computation
|
|
53
|
+
delta_nfr: Nodal gradient (ΔNFR) used in computation
|
|
54
|
+
validated: Whether units and bounds were validated
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
derivative: float
|
|
58
|
+
nu_f: float
|
|
59
|
+
delta_nfr: float
|
|
60
|
+
validated: bool
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def compute_canonical_nodal_derivative(
|
|
64
|
+
nu_f: float,
|
|
65
|
+
delta_nfr: float,
|
|
66
|
+
*,
|
|
67
|
+
validate_units: bool = True,
|
|
68
|
+
graph: GraphLike | None = None,
|
|
69
|
+
) -> NodalEquationResult:
|
|
70
|
+
"""Compute ∂EPI/∂t using the canonical TNFR nodal equation.
|
|
71
|
+
|
|
72
|
+
This is the explicit implementation of the fundamental equation:
|
|
73
|
+
∂EPI/∂t = νf · ΔNFR(t)
|
|
74
|
+
|
|
75
|
+
The function computes the time derivative of the Primary Information
|
|
76
|
+
Structure (EPI) as the product of:
|
|
77
|
+
- νf: structural frequency (reorganization rate in Hz_str)
|
|
78
|
+
- ΔNFR: nodal gradient (reorganization need/operator)
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
nu_f: Structural frequency in Hz_str (must be non-negative)
|
|
82
|
+
delta_nfr: Nodal gradient (reorganization operator)
|
|
83
|
+
validate_units: If True, validates that inputs are in valid ranges
|
|
84
|
+
graph: Optional graph for context-aware validation
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
NodalEquationResult containing the computed derivative and metadata
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ValueError: If validation is enabled and inputs are invalid
|
|
91
|
+
|
|
92
|
+
Notes:
|
|
93
|
+
- This function is the canonical reference implementation
|
|
94
|
+
- The result represents the instantaneous rate of EPI evolution
|
|
95
|
+
- Units: [∂EPI/∂t] = Hz_str (structural reorganization rate)
|
|
96
|
+
- The product νf·ΔNFR must preserve TNFR operator closure
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
>>> # Basic computation
|
|
100
|
+
>>> result = compute_canonical_nodal_derivative(1.0, 0.5)
|
|
101
|
+
>>> result.derivative
|
|
102
|
+
0.5
|
|
103
|
+
|
|
104
|
+
>>> # With explicit validation
|
|
105
|
+
>>> result = compute_canonical_nodal_derivative(
|
|
106
|
+
... nu_f=1.2,
|
|
107
|
+
... delta_nfr=-0.3,
|
|
108
|
+
... validate_units=True
|
|
109
|
+
... )
|
|
110
|
+
>>> result.validated
|
|
111
|
+
True
|
|
112
|
+
"""
|
|
113
|
+
validated = False
|
|
114
|
+
|
|
115
|
+
if validate_units:
|
|
116
|
+
nu_f = validate_structural_frequency(nu_f, graph=graph)
|
|
117
|
+
delta_nfr = validate_nodal_gradient(delta_nfr, graph=graph)
|
|
118
|
+
validated = True
|
|
119
|
+
|
|
120
|
+
# Canonical TNFR nodal equation: ∂EPI/∂t = νf · ΔNFR(t)
|
|
121
|
+
derivative = float(nu_f) * float(delta_nfr)
|
|
122
|
+
|
|
123
|
+
return NodalEquationResult(
|
|
124
|
+
derivative=derivative,
|
|
125
|
+
nu_f=nu_f,
|
|
126
|
+
delta_nfr=delta_nfr,
|
|
127
|
+
validated=validated,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def validate_structural_frequency(
|
|
132
|
+
nu_f: float,
|
|
133
|
+
*,
|
|
134
|
+
graph: GraphLike | None = None,
|
|
135
|
+
) -> float:
|
|
136
|
+
"""Validate that structural frequency is in valid range.
|
|
137
|
+
|
|
138
|
+
Structural frequency (νf) must satisfy TNFR constraints:
|
|
139
|
+
- Non-negative (νf ≥ 0)
|
|
140
|
+
- Expressed in Hz_str (structural hertz)
|
|
141
|
+
- Finite and well-defined
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
nu_f: Structural frequency to validate
|
|
145
|
+
graph: Optional graph for context-aware bounds checking
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Validated structural frequency value
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
ValueError: If nu_f is negative, infinite, or NaN
|
|
152
|
+
TypeError: If nu_f cannot be converted to float
|
|
153
|
+
|
|
154
|
+
Notes:
|
|
155
|
+
- νf = 0 is valid and represents structural silence
|
|
156
|
+
- Units must be Hz_str (not classical Hz)
|
|
157
|
+
- For Hz↔Hz_str conversion, use tnfr.units module
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
value = float(nu_f)
|
|
161
|
+
except TypeError as exc:
|
|
162
|
+
# Non-convertible type (e.g., None, object())
|
|
163
|
+
raise TypeError(
|
|
164
|
+
f"Structural frequency must be numeric, got {type(nu_f).__name__}"
|
|
165
|
+
) from exc
|
|
166
|
+
except ValueError as exc:
|
|
167
|
+
# Invalid string value (e.g., "invalid")
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"Structural frequency must be a valid number, got {nu_f!r}"
|
|
170
|
+
) from exc
|
|
171
|
+
|
|
172
|
+
# Check for NaN or infinity using math.isfinite
|
|
173
|
+
if not math.isfinite(value):
|
|
174
|
+
raise ValueError(f"Structural frequency must be finite, got νf={value}")
|
|
175
|
+
|
|
176
|
+
if value < 0:
|
|
177
|
+
raise ValueError(f"Structural frequency must be non-negative, got νf={value}")
|
|
178
|
+
|
|
179
|
+
return value
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def validate_nodal_gradient(
|
|
183
|
+
delta_nfr: float,
|
|
184
|
+
*,
|
|
185
|
+
graph: GraphLike | None = None,
|
|
186
|
+
) -> float:
|
|
187
|
+
"""Validate that nodal gradient is well-defined.
|
|
188
|
+
|
|
189
|
+
The nodal gradient (ΔNFR) represents the internal reorganization
|
|
190
|
+
operator and must be:
|
|
191
|
+
- Finite and well-defined
|
|
192
|
+
- Sign indicates reorganization direction
|
|
193
|
+
- Magnitude indicates reorganization intensity
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
delta_nfr: Nodal gradient to validate
|
|
197
|
+
graph: Optional graph for context-aware validation
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Validated nodal gradient value
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
ValueError: If delta_nfr is infinite or NaN
|
|
204
|
+
TypeError: If delta_nfr cannot be converted to float
|
|
205
|
+
|
|
206
|
+
Notes:
|
|
207
|
+
- ΔNFR can be positive (expansion) or negative (contraction)
|
|
208
|
+
- ΔNFR = 0 indicates equilibrium (no reorganization)
|
|
209
|
+
- Do NOT reinterpret as classical "error gradient"
|
|
210
|
+
- Semantics: operator over EPI, not optimization target
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
value = float(delta_nfr)
|
|
214
|
+
except TypeError as exc:
|
|
215
|
+
# Non-convertible type (e.g., None, object())
|
|
216
|
+
raise TypeError(
|
|
217
|
+
f"Nodal gradient must be numeric, got {type(delta_nfr).__name__}"
|
|
218
|
+
) from exc
|
|
219
|
+
except ValueError as exc:
|
|
220
|
+
# Invalid string value (e.g., "invalid")
|
|
221
|
+
raise ValueError(
|
|
222
|
+
f"Nodal gradient must be a valid number, got {delta_nfr!r}"
|
|
223
|
+
) from exc
|
|
224
|
+
|
|
225
|
+
# Check for NaN or infinity using math.isfinite
|
|
226
|
+
if not math.isfinite(value):
|
|
227
|
+
raise ValueError(f"Nodal gradient must be finite, got ΔNFR={value}")
|
|
228
|
+
|
|
229
|
+
return value
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Type stubs for canonical TNFR nodal equation implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import NamedTuple
|
|
6
|
+
|
|
7
|
+
from ..types import GraphLike
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
"NodalEquationResult",
|
|
11
|
+
"compute_canonical_nodal_derivative",
|
|
12
|
+
"validate_structural_frequency",
|
|
13
|
+
"validate_nodal_gradient",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
class NodalEquationResult(NamedTuple):
|
|
17
|
+
"""Result of canonical nodal equation evaluation."""
|
|
18
|
+
|
|
19
|
+
derivative: float
|
|
20
|
+
nu_f: float
|
|
21
|
+
delta_nfr: float
|
|
22
|
+
validated: bool
|
|
23
|
+
|
|
24
|
+
def compute_canonical_nodal_derivative(
|
|
25
|
+
nu_f: float,
|
|
26
|
+
delta_nfr: float,
|
|
27
|
+
*,
|
|
28
|
+
validate_units: bool = True,
|
|
29
|
+
graph: GraphLike | None = None,
|
|
30
|
+
) -> NodalEquationResult:
|
|
31
|
+
"""Compute ∂EPI/∂t using the canonical TNFR nodal equation."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
def validate_structural_frequency(
|
|
35
|
+
nu_f: float,
|
|
36
|
+
*,
|
|
37
|
+
graph: GraphLike | None = None,
|
|
38
|
+
) -> float:
|
|
39
|
+
"""Validate that structural frequency is in valid range."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
def validate_nodal_gradient(
|
|
43
|
+
delta_nfr: float,
|
|
44
|
+
*,
|
|
45
|
+
graph: GraphLike | None = None,
|
|
46
|
+
) -> float:
|
|
47
|
+
"""Validate that nodal gradient is well-defined."""
|
|
48
|
+
...
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Phase coordination helpers for TNFR dynamics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections import deque
|
|
7
|
+
from collections.abc import Mapping, MutableMapping, Sequence
|
|
8
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
9
|
+
from typing import Any, TypeVar, cast
|
|
10
|
+
from ..alias import get_theta_attr, set_theta
|
|
11
|
+
from ..constants import (
|
|
12
|
+
DEFAULTS,
|
|
13
|
+
METRIC_DEFAULTS,
|
|
14
|
+
STATE_DISSONANT,
|
|
15
|
+
STATE_STABLE,
|
|
16
|
+
STATE_TRANSITION,
|
|
17
|
+
normalise_state_token,
|
|
18
|
+
)
|
|
19
|
+
from ..glyph_history import append_metric
|
|
20
|
+
from ..utils import angle_diff, resolve_chunk_size
|
|
21
|
+
from ..metrics.common import ensure_neighbors_map
|
|
22
|
+
from ..metrics.trig import neighbor_phase_mean_list
|
|
23
|
+
from ..metrics.trig_cache import get_trig_cache
|
|
24
|
+
from ..observers import DEFAULT_GLYPH_LOAD_SPAN, glyph_load, kuramoto_order
|
|
25
|
+
from ..types import FloatArray, NodeId, Phase, TNFRGraph
|
|
26
|
+
from ..utils import get_numpy
|
|
27
|
+
|
|
28
|
+
_DequeT = TypeVar("_DequeT")
|
|
29
|
+
|
|
30
|
+
ChunkArgs = tuple[
|
|
31
|
+
Sequence[NodeId],
|
|
32
|
+
Mapping[NodeId, Phase],
|
|
33
|
+
Mapping[NodeId, float],
|
|
34
|
+
Mapping[NodeId, float],
|
|
35
|
+
Mapping[NodeId, Sequence[NodeId]],
|
|
36
|
+
float,
|
|
37
|
+
float,
|
|
38
|
+
float,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
__all__ = ("coordinate_global_local_phase",)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _ensure_hist_deque(
|
|
45
|
+
hist: MutableMapping[str, Any], key: str, maxlen: int
|
|
46
|
+
) -> deque[_DequeT]:
|
|
47
|
+
"""Ensure history entry ``key`` is a deque with ``maxlen``."""
|
|
48
|
+
|
|
49
|
+
dq = hist.setdefault(key, deque(maxlen=maxlen))
|
|
50
|
+
if not isinstance(dq, deque):
|
|
51
|
+
dq = deque(dq, maxlen=maxlen)
|
|
52
|
+
hist[key] = dq
|
|
53
|
+
return cast("deque[_DequeT]", dq)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_adaptive_params(
|
|
57
|
+
g: Mapping[str, Any],
|
|
58
|
+
) -> tuple[Mapping[str, Any], float, float]:
|
|
59
|
+
"""Obtain configuration and current values for phase adaptation."""
|
|
60
|
+
|
|
61
|
+
cfg = g.get("PHASE_ADAPT", DEFAULTS.get("PHASE_ADAPT", {}))
|
|
62
|
+
kG = float(g.get("PHASE_K_GLOBAL", DEFAULTS["PHASE_K_GLOBAL"]))
|
|
63
|
+
kL = float(g.get("PHASE_K_LOCAL", DEFAULTS["PHASE_K_LOCAL"]))
|
|
64
|
+
return cast(Mapping[str, Any], cfg), kG, kL
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _compute_state(G: TNFRGraph, cfg: Mapping[str, Any]) -> tuple[str, float, float]:
|
|
68
|
+
"""Return the canonical network state and supporting metrics."""
|
|
69
|
+
|
|
70
|
+
R = kuramoto_order(G)
|
|
71
|
+
dist = glyph_load(G, window=DEFAULT_GLYPH_LOAD_SPAN)
|
|
72
|
+
disr = float(dist.get("_disruptors", 0.0)) if dist else 0.0
|
|
73
|
+
|
|
74
|
+
R_hi = float(cfg.get("R_hi", 0.90))
|
|
75
|
+
R_lo = float(cfg.get("R_lo", 0.60))
|
|
76
|
+
disr_hi = float(cfg.get("disr_hi", 0.50))
|
|
77
|
+
disr_lo = float(cfg.get("disr_lo", 0.25))
|
|
78
|
+
if (R >= R_hi) and (disr <= disr_lo):
|
|
79
|
+
state = STATE_STABLE
|
|
80
|
+
elif (R <= R_lo) or (disr >= disr_hi):
|
|
81
|
+
state = STATE_DISSONANT
|
|
82
|
+
else:
|
|
83
|
+
state = STATE_TRANSITION
|
|
84
|
+
return state, float(R), disr
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _smooth_adjust_k(
|
|
88
|
+
kG: float, kL: float, state: str, cfg: Mapping[str, Any]
|
|
89
|
+
) -> tuple[float, float]:
|
|
90
|
+
"""Smoothly update kG/kL toward targets according to state."""
|
|
91
|
+
|
|
92
|
+
kG_min = float(cfg.get("kG_min", 0.01))
|
|
93
|
+
kG_max = float(cfg.get("kG_max", 0.20))
|
|
94
|
+
kL_min = float(cfg.get("kL_min", 0.05))
|
|
95
|
+
kL_max = float(cfg.get("kL_max", 0.25))
|
|
96
|
+
|
|
97
|
+
state = normalise_state_token(state)
|
|
98
|
+
|
|
99
|
+
if state == STATE_DISSONANT:
|
|
100
|
+
kG_t = kG_max
|
|
101
|
+
kL_t = 0.5 * (kL_min + kL_max) # keep kL mid-range to preserve local plasticity
|
|
102
|
+
elif state == STATE_STABLE:
|
|
103
|
+
kG_t = kG_min
|
|
104
|
+
kL_t = kL_min
|
|
105
|
+
else:
|
|
106
|
+
kG_t = 0.5 * (kG_min + kG_max)
|
|
107
|
+
kL_t = 0.5 * (kL_min + kL_max)
|
|
108
|
+
|
|
109
|
+
up = float(cfg.get("up", 0.10))
|
|
110
|
+
down = float(cfg.get("down", 0.07))
|
|
111
|
+
|
|
112
|
+
def _step(curr: float, target: float, mn: float, mx: float) -> float:
|
|
113
|
+
gain = up if target > curr else down
|
|
114
|
+
nxt = curr + gain * (target - curr)
|
|
115
|
+
return max(mn, min(mx, nxt))
|
|
116
|
+
|
|
117
|
+
return _step(kG, kG_t, kG_min, kG_max), _step(kL, kL_t, kL_min, kL_max)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _phase_adjust_chunk(args: ChunkArgs) -> list[tuple[NodeId, Phase]]:
|
|
121
|
+
"""Return coordinated phase updates for the provided chunk."""
|
|
122
|
+
|
|
123
|
+
(
|
|
124
|
+
nodes,
|
|
125
|
+
theta_map,
|
|
126
|
+
cos_map,
|
|
127
|
+
sin_map,
|
|
128
|
+
neighbors_map,
|
|
129
|
+
thG,
|
|
130
|
+
kG,
|
|
131
|
+
kL,
|
|
132
|
+
) = args
|
|
133
|
+
updates: list[tuple[NodeId, Phase]] = []
|
|
134
|
+
for node in nodes:
|
|
135
|
+
th = float(theta_map.get(node, 0.0))
|
|
136
|
+
neigh = neighbors_map.get(node, ())
|
|
137
|
+
if neigh:
|
|
138
|
+
thL = neighbor_phase_mean_list(
|
|
139
|
+
neigh,
|
|
140
|
+
cos_map,
|
|
141
|
+
sin_map,
|
|
142
|
+
np=None,
|
|
143
|
+
fallback=th,
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
thL = th
|
|
147
|
+
dG = angle_diff(thG, th)
|
|
148
|
+
dL = angle_diff(thL, th)
|
|
149
|
+
updates.append((node, cast(Phase, th + kG * dG + kL * dL)))
|
|
150
|
+
return updates
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def coordinate_global_local_phase(
|
|
154
|
+
G: TNFRGraph,
|
|
155
|
+
global_force: float | None = None,
|
|
156
|
+
local_force: float | None = None,
|
|
157
|
+
*,
|
|
158
|
+
n_jobs: int | None = None,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Coordinate phase using a blend of global and neighbour coupling.
|
|
161
|
+
|
|
162
|
+
This operator harmonises a TNFR graph by iteratively nudging each node's
|
|
163
|
+
phase toward the global Kuramoto mean while respecting the local
|
|
164
|
+
neighbourhood attractor. The global (``kG``) and local (``kL``) coupling
|
|
165
|
+
gains reshape phase coherence by modulating how strongly nodes follow the
|
|
166
|
+
network-wide synchrony versus immediate neighbours. When explicit coupling
|
|
167
|
+
overrides are not supplied, the gains adapt based on current ΔNFR telemetry
|
|
168
|
+
and the structural state recorded in the graph history. Adaptive updates
|
|
169
|
+
mutate the ``history`` buffers for phase state, order parameter, disruptor
|
|
170
|
+
load, and the stored coupling gains.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
G : TNFRGraph
|
|
175
|
+
Graph whose nodes expose TNFR phase attributes and ΔNFR telemetry. The
|
|
176
|
+
graph's ``history`` mapping is updated in-place when adaptive gain
|
|
177
|
+
smoothing is active.
|
|
178
|
+
global_force : float, optional
|
|
179
|
+
Override for the global coupling gain ``kG``. When provided, adaptive
|
|
180
|
+
gain estimation is skipped and the global history buffers are left
|
|
181
|
+
untouched.
|
|
182
|
+
local_force : float, optional
|
|
183
|
+
Override for the local coupling gain ``kL``. Analogous to
|
|
184
|
+
``global_force``, the adaptive pathway is bypassed when supplied.
|
|
185
|
+
n_jobs : int, optional
|
|
186
|
+
Maximum number of worker processes for distributing local updates.
|
|
187
|
+
Values of ``None`` or ``<=1`` perform updates sequentially. NumPy
|
|
188
|
+
availability forces sequential execution because vectorised updates are
|
|
189
|
+
faster than multiprocess handoffs.
|
|
190
|
+
|
|
191
|
+
Returns
|
|
192
|
+
-------
|
|
193
|
+
None
|
|
194
|
+
This operator updates node phases in-place and does not allocate a new
|
|
195
|
+
graph structure.
|
|
196
|
+
|
|
197
|
+
Examples
|
|
198
|
+
--------
|
|
199
|
+
Coordinate phase on a minimal TNFR network while inspecting ΔNFR telemetry
|
|
200
|
+
and history traces::
|
|
201
|
+
|
|
202
|
+
>>> import networkx as nx
|
|
203
|
+
>>> from tnfr.dynamics.coordination import coordinate_global_local_phase
|
|
204
|
+
>>> G = nx.Graph()
|
|
205
|
+
>>> G.add_nodes_from(("a", {"theta": 0.0, "ΔNFR": 0.08}),
|
|
206
|
+
... ("b", {"theta": 1.2, "ΔNFR": -0.05}))
|
|
207
|
+
>>> G.add_edge("a", "b")
|
|
208
|
+
>>> G.graph["history"] = {}
|
|
209
|
+
>>> coordinate_global_local_phase(G)
|
|
210
|
+
>>> list(round(G.nodes[n]["theta"], 3) for n in G)
|
|
211
|
+
[0.578, 0.622]
|
|
212
|
+
>>> history = G.graph["history"]
|
|
213
|
+
>>> sorted(history)
|
|
214
|
+
['phase_R', 'phase_disr', 'phase_kG', 'phase_kL', 'phase_state']
|
|
215
|
+
>>> history["phase_kG"][-1] <= history["phase_kL"][-1]
|
|
216
|
+
True
|
|
217
|
+
|
|
218
|
+
The resulting history buffers allow downstream observers to correlate
|
|
219
|
+
ΔNFR adjustments with phase telemetry snapshots.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
g = cast(dict[str, Any], G.graph)
|
|
223
|
+
hist = cast(dict[str, Any], g.setdefault("history", {}))
|
|
224
|
+
maxlen = int(g.get("PHASE_HISTORY_MAXLEN", METRIC_DEFAULTS["PHASE_HISTORY_MAXLEN"]))
|
|
225
|
+
hist_state = cast(deque[str], _ensure_hist_deque(hist, "phase_state", maxlen))
|
|
226
|
+
if hist_state:
|
|
227
|
+
normalised_states = [normalise_state_token(item) for item in hist_state]
|
|
228
|
+
if normalised_states != list(hist_state):
|
|
229
|
+
hist_state.clear()
|
|
230
|
+
hist_state.extend(normalised_states)
|
|
231
|
+
hist_R = cast(deque[float], _ensure_hist_deque(hist, "phase_R", maxlen))
|
|
232
|
+
hist_disr = cast(deque[float], _ensure_hist_deque(hist, "phase_disr", maxlen))
|
|
233
|
+
|
|
234
|
+
if (global_force is not None) or (local_force is not None):
|
|
235
|
+
kG = float(
|
|
236
|
+
global_force
|
|
237
|
+
if global_force is not None
|
|
238
|
+
else g.get("PHASE_K_GLOBAL", DEFAULTS["PHASE_K_GLOBAL"])
|
|
239
|
+
)
|
|
240
|
+
kL = float(
|
|
241
|
+
local_force
|
|
242
|
+
if local_force is not None
|
|
243
|
+
else g.get("PHASE_K_LOCAL", DEFAULTS["PHASE_K_LOCAL"])
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
cfg, kG, kL = _read_adaptive_params(g)
|
|
247
|
+
|
|
248
|
+
if bool(cfg.get("enabled", False)):
|
|
249
|
+
state, R, disr = _compute_state(G, cfg)
|
|
250
|
+
kG, kL = _smooth_adjust_k(kG, kL, state, cfg)
|
|
251
|
+
|
|
252
|
+
hist_state.append(state)
|
|
253
|
+
hist_R.append(float(R))
|
|
254
|
+
hist_disr.append(float(disr))
|
|
255
|
+
|
|
256
|
+
g["PHASE_K_GLOBAL"] = kG
|
|
257
|
+
g["PHASE_K_LOCAL"] = kL
|
|
258
|
+
append_metric(hist, "phase_kG", float(kG))
|
|
259
|
+
append_metric(hist, "phase_kL", float(kL))
|
|
260
|
+
|
|
261
|
+
jobs: int | None
|
|
262
|
+
try:
|
|
263
|
+
jobs = None if n_jobs is None else int(n_jobs)
|
|
264
|
+
except (TypeError, ValueError):
|
|
265
|
+
jobs = None
|
|
266
|
+
if jobs is not None and jobs <= 1:
|
|
267
|
+
jobs = None
|
|
268
|
+
|
|
269
|
+
np = get_numpy()
|
|
270
|
+
if np is not None:
|
|
271
|
+
jobs = None
|
|
272
|
+
|
|
273
|
+
nodes: list[NodeId] = [cast(NodeId, node) for node in G.nodes()]
|
|
274
|
+
num_nodes = len(nodes)
|
|
275
|
+
if not num_nodes:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
trig = get_trig_cache(G, np=np)
|
|
279
|
+
theta_map = cast(dict[NodeId, Phase], trig.theta)
|
|
280
|
+
cos_map = cast(dict[NodeId, float], trig.cos)
|
|
281
|
+
sin_map = cast(dict[NodeId, float], trig.sin)
|
|
282
|
+
|
|
283
|
+
neighbors_proxy = ensure_neighbors_map(G)
|
|
284
|
+
neighbors_map: dict[NodeId, tuple[NodeId, ...]] = {}
|
|
285
|
+
for n in nodes:
|
|
286
|
+
try:
|
|
287
|
+
neighbors_map[n] = tuple(cast(Sequence[NodeId], neighbors_proxy[n]))
|
|
288
|
+
except KeyError:
|
|
289
|
+
neighbors_map[n] = ()
|
|
290
|
+
|
|
291
|
+
def _theta_value(node: NodeId) -> float:
|
|
292
|
+
cached = theta_map.get(node)
|
|
293
|
+
if cached is not None:
|
|
294
|
+
return float(cached)
|
|
295
|
+
attr_val = get_theta_attr(G.nodes[node], 0.0)
|
|
296
|
+
return float(attr_val if attr_val is not None else 0.0)
|
|
297
|
+
|
|
298
|
+
theta_vals = [_theta_value(n) for n in nodes]
|
|
299
|
+
cos_vals = [
|
|
300
|
+
float(cos_map.get(n, math.cos(theta_vals[idx]))) for idx, n in enumerate(nodes)
|
|
301
|
+
]
|
|
302
|
+
sin_vals = [
|
|
303
|
+
float(sin_map.get(n, math.sin(theta_vals[idx]))) for idx, n in enumerate(nodes)
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
if np is not None:
|
|
307
|
+
theta_arr = cast(FloatArray, np.fromiter(theta_vals, dtype=float))
|
|
308
|
+
cos_arr = cast(FloatArray, np.fromiter(cos_vals, dtype=float))
|
|
309
|
+
sin_arr = cast(FloatArray, np.fromiter(sin_vals, dtype=float))
|
|
310
|
+
if cos_arr.size:
|
|
311
|
+
mean_cos = float(np.mean(cos_arr))
|
|
312
|
+
mean_sin = float(np.mean(sin_arr))
|
|
313
|
+
thG = float(np.arctan2(mean_sin, mean_cos))
|
|
314
|
+
else:
|
|
315
|
+
thG = 0.0
|
|
316
|
+
neighbor_means = [
|
|
317
|
+
neighbor_phase_mean_list(
|
|
318
|
+
neighbors_map.get(n, ()),
|
|
319
|
+
cos_map,
|
|
320
|
+
sin_map,
|
|
321
|
+
np=np,
|
|
322
|
+
fallback=theta_vals[idx],
|
|
323
|
+
)
|
|
324
|
+
for idx, n in enumerate(nodes)
|
|
325
|
+
]
|
|
326
|
+
neighbor_arr = cast(FloatArray, np.fromiter(neighbor_means, dtype=float))
|
|
327
|
+
theta_updates = (
|
|
328
|
+
theta_arr + kG * (thG - theta_arr) + kL * (neighbor_arr - theta_arr)
|
|
329
|
+
)
|
|
330
|
+
for idx, node in enumerate(nodes):
|
|
331
|
+
set_theta(G, node, float(theta_updates[int(idx)]))
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
mean_cos = math.fsum(cos_vals) / num_nodes
|
|
335
|
+
mean_sin = math.fsum(sin_vals) / num_nodes
|
|
336
|
+
thG = math.atan2(mean_sin, mean_cos)
|
|
337
|
+
|
|
338
|
+
if jobs is None:
|
|
339
|
+
for node in nodes:
|
|
340
|
+
th = float(theta_map.get(node, 0.0))
|
|
341
|
+
neigh = neighbors_map.get(node, ())
|
|
342
|
+
if neigh:
|
|
343
|
+
thL = neighbor_phase_mean_list(
|
|
344
|
+
neigh,
|
|
345
|
+
cos_map,
|
|
346
|
+
sin_map,
|
|
347
|
+
np=None,
|
|
348
|
+
fallback=th,
|
|
349
|
+
)
|
|
350
|
+
else:
|
|
351
|
+
thL = th
|
|
352
|
+
dG = angle_diff(thG, th)
|
|
353
|
+
dL = angle_diff(thL, th)
|
|
354
|
+
set_theta(G, node, float(th + kG * dG + kL * dL))
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
approx_chunk = math.ceil(len(nodes) / jobs) if jobs else None
|
|
358
|
+
chunk_size = resolve_chunk_size(
|
|
359
|
+
approx_chunk,
|
|
360
|
+
len(nodes),
|
|
361
|
+
minimum=1,
|
|
362
|
+
)
|
|
363
|
+
chunks = [nodes[idx : idx + chunk_size] for idx in range(0, len(nodes), chunk_size)]
|
|
364
|
+
args: list[ChunkArgs] = [
|
|
365
|
+
(
|
|
366
|
+
chunk,
|
|
367
|
+
theta_map,
|
|
368
|
+
cos_map,
|
|
369
|
+
sin_map,
|
|
370
|
+
neighbors_map,
|
|
371
|
+
thG,
|
|
372
|
+
kG,
|
|
373
|
+
kL,
|
|
374
|
+
)
|
|
375
|
+
for chunk in chunks
|
|
376
|
+
]
|
|
377
|
+
results: dict[NodeId, Phase] = {}
|
|
378
|
+
with ProcessPoolExecutor(max_workers=jobs) as executor:
|
|
379
|
+
for res in executor.map(_phase_adjust_chunk, args):
|
|
380
|
+
for node, value in res:
|
|
381
|
+
results[node] = value
|
|
382
|
+
for node in nodes:
|
|
383
|
+
new_theta = results.get(node)
|
|
384
|
+
base_theta = theta_map.get(node, 0.0)
|
|
385
|
+
set_theta(G, node, float(new_theta if new_theta is not None else base_theta))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..types import NodeId, Phase, TNFRGraph
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
|
|
6
|
+
__all__ = ["coordinate_global_local_phase"]
|
|
7
|
+
|
|
8
|
+
ChunkArgs = tuple[
|
|
9
|
+
Sequence[NodeId],
|
|
10
|
+
Mapping[NodeId, Phase],
|
|
11
|
+
Mapping[NodeId, float],
|
|
12
|
+
Mapping[NodeId, float],
|
|
13
|
+
Mapping[NodeId, Sequence[NodeId]],
|
|
14
|
+
float,
|
|
15
|
+
float,
|
|
16
|
+
float,
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
def coordinate_global_local_phase(
|
|
20
|
+
G: TNFRGraph,
|
|
21
|
+
global_force: float | None = None,
|
|
22
|
+
local_force: float | None = None,
|
|
23
|
+
*,
|
|
24
|
+
n_jobs: int | None = None,
|
|
25
|
+
) -> None: ...
|