tnfr 4.5.2__py3-none-any.whl → 8.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tnfr might be problematic. Click here for more details.
- tnfr/__init__.py +334 -50
- tnfr/__init__.pyi +33 -0
- tnfr/_compat.py +10 -0
- tnfr/_generated_version.py +34 -0
- tnfr/_version.py +49 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +214 -37
- tnfr/alias.pyi +108 -0
- tnfr/backends/__init__.py +354 -0
- tnfr/backends/jax_backend.py +173 -0
- tnfr/backends/numpy_backend.py +238 -0
- tnfr/backends/optimized_numpy.py +420 -0
- tnfr/backends/torch_backend.py +408 -0
- tnfr/cache.py +149 -556
- tnfr/cache.pyi +13 -0
- tnfr/cli/__init__.py +51 -16
- tnfr/cli/__init__.pyi +26 -0
- tnfr/cli/arguments.py +344 -32
- tnfr/cli/arguments.pyi +29 -0
- tnfr/cli/execution.py +676 -50
- tnfr/cli/execution.pyi +70 -0
- tnfr/cli/interactive_validator.py +614 -0
- tnfr/cli/utils.py +18 -3
- tnfr/cli/utils.pyi +7 -0
- tnfr/cli/validate.py +236 -0
- tnfr/compat/__init__.py +85 -0
- tnfr/compat/dataclass.py +136 -0
- tnfr/compat/jsonschema_stub.py +61 -0
- tnfr/compat/matplotlib_stub.py +73 -0
- tnfr/compat/numpy_stub.py +155 -0
- tnfr/config/__init__.py +224 -0
- tnfr/config/__init__.pyi +10 -0
- tnfr/{constants_glyphs.py → config/constants.py} +26 -20
- tnfr/config/constants.pyi +12 -0
- tnfr/config/defaults.py +54 -0
- tnfr/{constants/core.py → config/defaults_core.py} +59 -6
- tnfr/config/defaults_init.py +33 -0
- tnfr/config/defaults_metric.py +104 -0
- tnfr/config/feature_flags.py +81 -0
- tnfr/config/feature_flags.pyi +16 -0
- tnfr/config/glyph_constants.py +31 -0
- tnfr/config/init.py +77 -0
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +254 -0
- tnfr/config/operator_names.pyi +36 -0
- tnfr/config/physics_derivation.py +354 -0
- tnfr/config/presets.py +83 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/config/security.py +927 -0
- tnfr/config/thresholds.py +114 -0
- tnfr/config/tnfr_config.py +498 -0
- tnfr/constants/__init__.py +51 -133
- tnfr/constants/__init__.pyi +92 -0
- tnfr/constants/aliases.py +33 -0
- tnfr/constants/aliases.pyi +27 -0
- tnfr/constants/init.py +3 -1
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +9 -15
- tnfr/constants/metric.pyi +19 -0
- tnfr/core/__init__.py +33 -0
- tnfr/core/container.py +226 -0
- tnfr/core/default_implementations.py +329 -0
- tnfr/core/interfaces.py +279 -0
- tnfr/dynamics/__init__.py +213 -633
- tnfr/dynamics/__init__.pyi +83 -0
- tnfr/dynamics/adaptation.py +267 -0
- tnfr/dynamics/adaptation.pyi +7 -0
- tnfr/dynamics/adaptive_sequences.py +189 -0
- tnfr/dynamics/adaptive_sequences.pyi +14 -0
- tnfr/dynamics/aliases.py +23 -0
- tnfr/dynamics/aliases.pyi +19 -0
- tnfr/dynamics/bifurcation.py +232 -0
- tnfr/dynamics/canonical.py +229 -0
- tnfr/dynamics/canonical.pyi +48 -0
- tnfr/dynamics/coordination.py +385 -0
- tnfr/dynamics/coordination.pyi +25 -0
- tnfr/dynamics/dnfr.py +2699 -398
- tnfr/dynamics/dnfr.pyi +26 -0
- tnfr/dynamics/dynamic_limits.py +225 -0
- tnfr/dynamics/feedback.py +252 -0
- tnfr/dynamics/feedback.pyi +24 -0
- tnfr/dynamics/fused_dnfr.py +454 -0
- tnfr/dynamics/homeostasis.py +157 -0
- tnfr/dynamics/homeostasis.pyi +14 -0
- tnfr/dynamics/integrators.py +496 -102
- tnfr/dynamics/integrators.pyi +36 -0
- tnfr/dynamics/learning.py +310 -0
- tnfr/dynamics/learning.pyi +33 -0
- tnfr/dynamics/metabolism.py +254 -0
- tnfr/dynamics/nbody.py +796 -0
- tnfr/dynamics/nbody_tnfr.py +783 -0
- tnfr/dynamics/propagation.py +326 -0
- tnfr/dynamics/runtime.py +908 -0
- tnfr/dynamics/runtime.pyi +77 -0
- tnfr/dynamics/sampling.py +10 -5
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +711 -0
- tnfr/dynamics/selectors.pyi +85 -0
- tnfr/dynamics/structural_clip.py +207 -0
- tnfr/errors/__init__.py +37 -0
- tnfr/errors/contextual.py +492 -0
- tnfr/execution.py +77 -55
- tnfr/execution.pyi +45 -0
- tnfr/extensions/__init__.py +205 -0
- tnfr/extensions/__init__.pyi +18 -0
- tnfr/extensions/base.py +173 -0
- tnfr/extensions/base.pyi +35 -0
- tnfr/extensions/business/__init__.py +71 -0
- tnfr/extensions/business/__init__.pyi +11 -0
- tnfr/extensions/business/cookbook.py +88 -0
- tnfr/extensions/business/cookbook.pyi +8 -0
- tnfr/extensions/business/health_analyzers.py +202 -0
- tnfr/extensions/business/health_analyzers.pyi +9 -0
- tnfr/extensions/business/patterns.py +183 -0
- tnfr/extensions/business/patterns.pyi +8 -0
- tnfr/extensions/medical/__init__.py +73 -0
- tnfr/extensions/medical/__init__.pyi +11 -0
- tnfr/extensions/medical/cookbook.py +88 -0
- tnfr/extensions/medical/cookbook.pyi +8 -0
- tnfr/extensions/medical/health_analyzers.py +181 -0
- tnfr/extensions/medical/health_analyzers.pyi +9 -0
- tnfr/extensions/medical/patterns.py +163 -0
- tnfr/extensions/medical/patterns.pyi +8 -0
- tnfr/flatten.py +29 -50
- tnfr/flatten.pyi +21 -0
- tnfr/gamma.py +66 -53
- tnfr/gamma.pyi +36 -0
- tnfr/glyph_history.py +144 -57
- tnfr/glyph_history.pyi +35 -0
- tnfr/glyph_runtime.py +19 -0
- tnfr/glyph_runtime.pyi +8 -0
- tnfr/immutable.py +70 -30
- tnfr/immutable.pyi +36 -0
- tnfr/initialization.py +22 -16
- tnfr/initialization.pyi +65 -0
- tnfr/io.py +5 -241
- tnfr/io.pyi +13 -0
- tnfr/locking.pyi +7 -0
- tnfr/mathematics/__init__.py +79 -0
- tnfr/mathematics/backend.py +453 -0
- tnfr/mathematics/backend.pyi +99 -0
- tnfr/mathematics/dynamics.py +408 -0
- tnfr/mathematics/dynamics.pyi +90 -0
- tnfr/mathematics/epi.py +391 -0
- tnfr/mathematics/epi.pyi +65 -0
- tnfr/mathematics/generators.py +242 -0
- tnfr/mathematics/generators.pyi +29 -0
- tnfr/mathematics/metrics.py +119 -0
- tnfr/mathematics/metrics.pyi +16 -0
- tnfr/mathematics/operators.py +239 -0
- tnfr/mathematics/operators.pyi +59 -0
- tnfr/mathematics/operators_factory.py +124 -0
- tnfr/mathematics/operators_factory.pyi +11 -0
- tnfr/mathematics/projection.py +87 -0
- tnfr/mathematics/projection.pyi +33 -0
- tnfr/mathematics/runtime.py +182 -0
- tnfr/mathematics/runtime.pyi +64 -0
- tnfr/mathematics/spaces.py +256 -0
- tnfr/mathematics/spaces.pyi +83 -0
- tnfr/mathematics/transforms.py +305 -0
- tnfr/mathematics/transforms.pyi +62 -0
- tnfr/metrics/__init__.py +47 -9
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/buffer_cache.py +163 -0
- tnfr/metrics/buffer_cache.pyi +24 -0
- tnfr/metrics/cache_utils.py +214 -0
- tnfr/metrics/coherence.py +1510 -330
- tnfr/metrics/coherence.pyi +129 -0
- tnfr/metrics/common.py +23 -16
- tnfr/metrics/common.pyi +35 -0
- tnfr/metrics/core.py +251 -36
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +709 -110
- tnfr/metrics/diagnosis.pyi +86 -0
- tnfr/metrics/emergence.py +245 -0
- tnfr/metrics/export.py +60 -18
- tnfr/metrics/export.pyi +7 -0
- tnfr/metrics/glyph_timing.py +233 -43
- tnfr/metrics/glyph_timing.pyi +81 -0
- tnfr/metrics/learning_metrics.py +280 -0
- tnfr/metrics/learning_metrics.pyi +21 -0
- tnfr/metrics/phase_coherence.py +351 -0
- tnfr/metrics/phase_compatibility.py +349 -0
- tnfr/metrics/reporting.py +63 -28
- tnfr/metrics/reporting.pyi +25 -0
- tnfr/metrics/sense_index.py +1126 -43
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +215 -23
- tnfr/metrics/trig.pyi +13 -0
- tnfr/metrics/trig_cache.py +148 -24
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/multiscale/__init__.py +32 -0
- tnfr/multiscale/hierarchical.py +517 -0
- tnfr/node.py +646 -140
- tnfr/node.pyi +139 -0
- tnfr/observers.py +160 -45
- tnfr/observers.pyi +31 -0
- tnfr/ontosim.py +23 -19
- tnfr/ontosim.pyi +28 -0
- tnfr/operators/__init__.py +1358 -106
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/algebra.py +277 -0
- tnfr/operators/canonical_patterns.py +420 -0
- tnfr/operators/cascade.py +267 -0
- tnfr/operators/cycle_detection.py +358 -0
- tnfr/operators/definitions.py +4108 -0
- tnfr/operators/definitions.pyi +78 -0
- tnfr/operators/grammar.py +1164 -0
- tnfr/operators/grammar.pyi +140 -0
- tnfr/operators/hamiltonian.py +710 -0
- tnfr/operators/health_analyzer.py +809 -0
- tnfr/operators/jitter.py +107 -38
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/lifecycle.py +314 -0
- tnfr/operators/metabolism.py +618 -0
- tnfr/operators/metrics.py +2138 -0
- tnfr/operators/network_analysis/__init__.py +27 -0
- tnfr/operators/network_analysis/source_detection.py +186 -0
- tnfr/operators/nodal_equation.py +395 -0
- tnfr/operators/pattern_detection.py +660 -0
- tnfr/operators/patterns.py +669 -0
- tnfr/operators/postconditions/__init__.py +38 -0
- tnfr/operators/postconditions/mutation.py +236 -0
- tnfr/operators/preconditions/__init__.py +1226 -0
- tnfr/operators/preconditions/coherence.py +305 -0
- tnfr/operators/preconditions/dissonance.py +236 -0
- tnfr/operators/preconditions/emission.py +128 -0
- tnfr/operators/preconditions/mutation.py +580 -0
- tnfr/operators/preconditions/reception.py +125 -0
- tnfr/operators/preconditions/resonance.py +364 -0
- tnfr/operators/registry.py +74 -0
- tnfr/operators/registry.pyi +9 -0
- tnfr/operators/remesh.py +1415 -91
- tnfr/operators/remesh.pyi +26 -0
- tnfr/operators/structural_units.py +268 -0
- tnfr/operators/unified_grammar.py +105 -0
- tnfr/parallel/__init__.py +54 -0
- tnfr/parallel/auto_scaler.py +234 -0
- tnfr/parallel/distributed.py +384 -0
- tnfr/parallel/engine.py +238 -0
- tnfr/parallel/gpu_engine.py +420 -0
- tnfr/parallel/monitoring.py +248 -0
- tnfr/parallel/partitioner.py +459 -0
- tnfr/py.typed +0 -0
- tnfr/recipes/__init__.py +22 -0
- tnfr/recipes/cookbook.py +743 -0
- tnfr/rng.py +75 -151
- tnfr/rng.pyi +26 -0
- tnfr/schemas/__init__.py +8 -0
- tnfr/schemas/grammar.json +94 -0
- tnfr/sdk/__init__.py +107 -0
- tnfr/sdk/__init__.pyi +19 -0
- tnfr/sdk/adaptive_system.py +173 -0
- tnfr/sdk/adaptive_system.pyi +21 -0
- tnfr/sdk/builders.py +370 -0
- tnfr/sdk/builders.pyi +51 -0
- tnfr/sdk/fluent.py +1121 -0
- tnfr/sdk/fluent.pyi +74 -0
- tnfr/sdk/templates.py +342 -0
- tnfr/sdk/templates.pyi +41 -0
- tnfr/sdk/utils.py +341 -0
- tnfr/secure_config.py +46 -0
- tnfr/security/__init__.py +70 -0
- tnfr/security/database.py +514 -0
- tnfr/security/subprocess.py +503 -0
- tnfr/security/validation.py +290 -0
- tnfr/selector.py +59 -22
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +92 -67
- tnfr/sense.pyi +23 -0
- tnfr/services/__init__.py +17 -0
- tnfr/services/orchestrator.py +325 -0
- tnfr/sparse/__init__.py +39 -0
- tnfr/sparse/representations.py +492 -0
- tnfr/structural.py +639 -263
- tnfr/structural.pyi +83 -0
- tnfr/telemetry/__init__.py +35 -0
- tnfr/telemetry/cache_metrics.py +226 -0
- tnfr/telemetry/cache_metrics.pyi +64 -0
- tnfr/telemetry/nu_f.py +422 -0
- tnfr/telemetry/nu_f.pyi +108 -0
- tnfr/telemetry/verbosity.py +36 -0
- tnfr/telemetry/verbosity.pyi +15 -0
- tnfr/tokens.py +2 -4
- tnfr/tokens.pyi +36 -0
- tnfr/tools/__init__.py +20 -0
- tnfr/tools/domain_templates.py +478 -0
- tnfr/tools/sequence_generator.py +846 -0
- tnfr/topology/__init__.py +13 -0
- tnfr/topology/asymmetry.py +151 -0
- tnfr/trace.py +300 -126
- tnfr/trace.pyi +42 -0
- tnfr/tutorials/__init__.py +38 -0
- tnfr/tutorials/autonomous_evolution.py +285 -0
- tnfr/tutorials/interactive.py +1576 -0
- tnfr/tutorials/structural_metabolism.py +238 -0
- tnfr/types.py +743 -12
- tnfr/types.pyi +357 -0
- tnfr/units.py +68 -0
- tnfr/units.pyi +13 -0
- tnfr/utils/__init__.py +282 -0
- tnfr/utils/__init__.pyi +215 -0
- tnfr/utils/cache.py +4223 -0
- tnfr/utils/cache.pyi +470 -0
- tnfr/{callback_utils.py → utils/callbacks.py} +26 -39
- tnfr/utils/callbacks.pyi +49 -0
- tnfr/utils/chunks.py +108 -0
- tnfr/utils/chunks.pyi +22 -0
- tnfr/utils/data.py +428 -0
- tnfr/utils/data.pyi +74 -0
- tnfr/utils/graph.py +85 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +821 -0
- tnfr/utils/init.pyi +80 -0
- tnfr/utils/io.py +559 -0
- tnfr/utils/io.pyi +66 -0
- tnfr/{helpers → utils}/numeric.py +51 -24
- tnfr/utils/numeric.pyi +21 -0
- tnfr/validation/__init__.py +257 -0
- tnfr/validation/__init__.pyi +85 -0
- tnfr/validation/compatibility.py +460 -0
- tnfr/validation/compatibility.pyi +6 -0
- tnfr/validation/config.py +73 -0
- tnfr/validation/graph.py +139 -0
- tnfr/validation/graph.pyi +18 -0
- tnfr/validation/input_validation.py +755 -0
- tnfr/validation/invariants.py +712 -0
- tnfr/validation/rules.py +253 -0
- tnfr/validation/rules.pyi +44 -0
- tnfr/validation/runtime.py +279 -0
- tnfr/validation/runtime.pyi +28 -0
- tnfr/validation/sequence_validator.py +162 -0
- tnfr/validation/soft_filters.py +170 -0
- tnfr/validation/soft_filters.pyi +32 -0
- tnfr/validation/spectral.py +164 -0
- tnfr/validation/spectral.pyi +42 -0
- tnfr/validation/validator.py +1266 -0
- tnfr/validation/window.py +39 -0
- tnfr/validation/window.pyi +1 -0
- tnfr/visualization/__init__.py +98 -0
- tnfr/visualization/cascade_viz.py +256 -0
- tnfr/visualization/hierarchy.py +284 -0
- tnfr/visualization/sequence_plotter.py +784 -0
- tnfr/viz/__init__.py +60 -0
- tnfr/viz/matplotlib.py +278 -0
- tnfr/viz/matplotlib.pyi +35 -0
- tnfr-8.5.0.dist-info/METADATA +573 -0
- tnfr-8.5.0.dist-info/RECORD +353 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/entry_points.txt +1 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/licenses/LICENSE.md +1 -1
- tnfr/collections_utils.py +0 -300
- tnfr/config.py +0 -32
- tnfr/grammar.py +0 -344
- tnfr/graph_utils.py +0 -84
- tnfr/helpers/__init__.py +0 -71
- tnfr/import_utils.py +0 -228
- tnfr/json_utils.py +0 -162
- tnfr/logging_utils.py +0 -116
- tnfr/presets.py +0 -60
- tnfr/validators.py +0 -84
- tnfr/value_utils.py +0 -59
- tnfr-4.5.2.dist-info/METADATA +0 -379
- tnfr-4.5.2.dist-info/RECORD +0 -67
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
tnfr/dynamics/integrators.py
CHANGED
|
@@ -1,42 +1,189 @@
|
|
|
1
|
-
|
|
1
|
+
"""Canonical ΔNFR integrators driving TNFR runtime evolution.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This module implements numerical integration of the canonical TNFR nodal equation:
|
|
4
|
+
|
|
5
|
+
∂EPI/∂t = νf · ΔNFR(t) + Γi(R)
|
|
6
|
+
|
|
7
|
+
The extended equation includes:
|
|
8
|
+
- Base term: νf · ΔNFR(t) - canonical structural evolution
|
|
9
|
+
- Network term: Γi(R) - optional Kuramoto coupling
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
Integration respects TNFR invariants:
|
|
12
|
+
- Structural units (Hz_str for νf)
|
|
13
|
+
- Operator closure (valid ΔNFR semantics)
|
|
14
|
+
- Phase coherence (network synchronization)
|
|
15
|
+
- Reproducibility (deterministic with seeds)
|
|
8
16
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
The canonical base term is computed explicitly in _collect_nodal_increments()
|
|
18
|
+
at line 321 and 342 as: base = vf * dnfr, implementing ∂EPI/∂t = νf·ΔNFR(t).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import math
|
|
24
|
+
from abc import ABC, abstractmethod
|
|
25
|
+
from collections.abc import Iterable, Mapping
|
|
26
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
27
|
+
from multiprocessing import get_context
|
|
28
|
+
from typing import Any, Literal, cast
|
|
29
|
+
|
|
30
|
+
import networkx as nx
|
|
31
|
+
|
|
32
|
+
from .._compat import TypeAlias
|
|
33
|
+
from ..alias import collect_attr, get_attr, get_attr_str, set_attr, set_attr_str
|
|
34
|
+
from ..constants import DEFAULTS
|
|
35
|
+
from ..constants.aliases import (
|
|
36
|
+
ALIAS_D2EPI,
|
|
37
|
+
ALIAS_DEPI,
|
|
38
|
+
ALIAS_DNFR,
|
|
39
|
+
ALIAS_EPI,
|
|
40
|
+
ALIAS_EPI_KIND,
|
|
41
|
+
ALIAS_VF,
|
|
12
42
|
)
|
|
13
43
|
from ..gamma import _get_gamma_spec, eval_gamma
|
|
14
|
-
from ..
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
ALIAS_DEPI = get_aliases("DEPI")
|
|
19
|
-
ALIAS_EPI = get_aliases("EPI")
|
|
20
|
-
ALIAS_EPI_KIND = get_aliases("EPI_KIND")
|
|
21
|
-
ALIAS_D2EPI = get_aliases("D2EPI")
|
|
44
|
+
from ..types import NodeId, TNFRGraph
|
|
45
|
+
from ..utils import get_numpy, resolve_chunk_size
|
|
46
|
+
from .canonical import compute_canonical_nodal_derivative
|
|
47
|
+
from .structural_clip import structural_clip
|
|
22
48
|
|
|
23
49
|
__all__ = (
|
|
50
|
+
"AbstractIntegrator",
|
|
51
|
+
"DefaultIntegrator",
|
|
24
52
|
"prepare_integration_params",
|
|
25
53
|
"update_epi_via_nodal_equation",
|
|
26
54
|
)
|
|
27
55
|
|
|
56
|
+
GammaMap: TypeAlias = dict[NodeId, float]
|
|
57
|
+
"""Γ evaluation cache keyed by node identifier."""
|
|
58
|
+
|
|
59
|
+
NodeIncrements: TypeAlias = dict[NodeId, tuple[float, ...]]
|
|
60
|
+
"""Mapping of nodes to staged integration increments."""
|
|
61
|
+
|
|
62
|
+
NodalUpdate: TypeAlias = dict[NodeId, tuple[float, float, float]]
|
|
63
|
+
"""Mapping of nodes to ``(EPI, dEPI/dt, ∂²EPI/∂t²)`` tuples."""
|
|
64
|
+
|
|
65
|
+
IntegratorMethod: TypeAlias = Literal["euler", "rk4"]
|
|
66
|
+
"""Supported explicit integration schemes for nodal updates."""
|
|
67
|
+
|
|
68
|
+
_PARALLEL_GRAPH: TNFRGraph | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _gamma_worker_init(graph: TNFRGraph) -> None:
|
|
72
|
+
"""Initialise process-local graph reference for Γ evaluation."""
|
|
73
|
+
|
|
74
|
+
global _PARALLEL_GRAPH
|
|
75
|
+
_PARALLEL_GRAPH = graph
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _gamma_worker(task: tuple[list[NodeId], float]) -> list[tuple[NodeId, float]]:
|
|
79
|
+
"""Evaluate Γ for ``task`` chunk using process-local graph."""
|
|
80
|
+
|
|
81
|
+
chunk, t = task
|
|
82
|
+
if _PARALLEL_GRAPH is None:
|
|
83
|
+
raise RuntimeError("Parallel Γ worker initialised without graph reference")
|
|
84
|
+
return [(node, float(eval_gamma(_PARALLEL_GRAPH, node, t))) for node in chunk]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _normalise_jobs(n_jobs: int | None, total: int) -> int | None:
|
|
88
|
+
"""Return an effective worker count respecting serial fallbacks."""
|
|
89
|
+
|
|
90
|
+
if n_jobs is None:
|
|
91
|
+
return None
|
|
92
|
+
try:
|
|
93
|
+
workers = int(n_jobs)
|
|
94
|
+
except (TypeError, ValueError):
|
|
95
|
+
return None
|
|
96
|
+
if workers <= 1 or total <= 1:
|
|
97
|
+
return None
|
|
98
|
+
return max(1, min(workers, total))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _chunk_nodes(nodes: list[NodeId], chunk_size: int) -> Iterable[list[NodeId]]:
|
|
102
|
+
"""Yield deterministic chunks from ``nodes`` respecting insertion order."""
|
|
103
|
+
|
|
104
|
+
for idx in range(0, len(nodes), chunk_size):
|
|
105
|
+
yield nodes[idx : idx + chunk_size]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _apply_increment_chunk(
|
|
109
|
+
chunk: list[tuple[NodeId, float, float, tuple[float, ...]]],
|
|
110
|
+
dt_step: float,
|
|
111
|
+
method: str,
|
|
112
|
+
) -> list[tuple[NodeId, tuple[float, float, float]]]:
|
|
113
|
+
"""Compute updated states for ``chunk`` using scalar arithmetic."""
|
|
114
|
+
|
|
115
|
+
results: list[tuple[NodeId, tuple[float, float, float]]] = []
|
|
116
|
+
dt_nonzero = dt_step != 0
|
|
117
|
+
|
|
118
|
+
for node, epi_i, dEPI_prev, ks in chunk:
|
|
119
|
+
if method == "rk4":
|
|
120
|
+
k1, k2, k3, k4 = ks
|
|
121
|
+
epi = epi_i + (dt_step / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)
|
|
122
|
+
dEPI_dt = k4
|
|
123
|
+
else:
|
|
124
|
+
(k1,) = ks
|
|
125
|
+
epi = epi_i + dt_step * k1
|
|
126
|
+
dEPI_dt = k1
|
|
127
|
+
d2epi = (dEPI_dt - dEPI_prev) / dt_step if dt_nonzero else 0.0
|
|
128
|
+
results.append((node, (float(epi), float(dEPI_dt), float(d2epi))))
|
|
129
|
+
|
|
130
|
+
return results
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _evaluate_gamma_map(
|
|
134
|
+
G: TNFRGraph,
|
|
135
|
+
nodes: list[NodeId],
|
|
136
|
+
t: float,
|
|
137
|
+
*,
|
|
138
|
+
n_jobs: int | None = None,
|
|
139
|
+
) -> GammaMap:
|
|
140
|
+
"""Return Γ evaluations for ``nodes`` at time ``t`` respecting parallelism."""
|
|
141
|
+
|
|
142
|
+
workers = _normalise_jobs(n_jobs, len(nodes))
|
|
143
|
+
if workers is None:
|
|
144
|
+
return {n: float(eval_gamma(G, n, t)) for n in nodes}
|
|
145
|
+
|
|
146
|
+
approx_chunk = math.ceil(len(nodes) / (workers * 4)) if workers > 0 else None
|
|
147
|
+
chunk_size = resolve_chunk_size(
|
|
148
|
+
approx_chunk,
|
|
149
|
+
len(nodes),
|
|
150
|
+
minimum=1,
|
|
151
|
+
)
|
|
152
|
+
mp_ctx = get_context("spawn")
|
|
153
|
+
tasks = ((chunk, t) for chunk in _chunk_nodes(nodes, chunk_size))
|
|
154
|
+
|
|
155
|
+
results: GammaMap = {}
|
|
156
|
+
with ProcessPoolExecutor(
|
|
157
|
+
max_workers=workers,
|
|
158
|
+
mp_context=mp_ctx,
|
|
159
|
+
initializer=_gamma_worker_init,
|
|
160
|
+
initargs=(G,),
|
|
161
|
+
) as executor:
|
|
162
|
+
futures = [executor.submit(_gamma_worker, task) for task in tasks]
|
|
163
|
+
for fut in futures:
|
|
164
|
+
for node, value in fut.result():
|
|
165
|
+
results[node] = value
|
|
166
|
+
return results
|
|
167
|
+
|
|
28
168
|
|
|
29
169
|
def prepare_integration_params(
|
|
30
|
-
G,
|
|
170
|
+
G: TNFRGraph,
|
|
31
171
|
dt: float | None = None,
|
|
32
172
|
t: float | None = None,
|
|
33
173
|
method: Literal["euler", "rk4"] | None = None,
|
|
34
|
-
):
|
|
174
|
+
) -> tuple[float, int, float, Literal["euler", "rk4"]]:
|
|
35
175
|
"""Validate and normalise ``dt``, ``t`` and ``method`` for integration.
|
|
36
176
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
177
|
+
The function raises :class:`TypeError` when ``dt`` cannot be coerced to a
|
|
178
|
+
number, :class:`ValueError` if ``dt`` is negative, and another
|
|
179
|
+
:class:`ValueError` when an unsupported method is requested. When ``dt``
|
|
180
|
+
exceeds a positive ``DT_MIN`` stored on ``G`` the span is deterministically
|
|
181
|
+
subdivided into integer steps so that the resulting ``dt_step`` never falls
|
|
182
|
+
below that minimum threshold.
|
|
183
|
+
|
|
184
|
+
Returns ``(dt_step, steps, t0, method)`` where ``dt_step`` is the effective
|
|
185
|
+
step, ``steps`` the number of substeps and ``t0`` the prepared initial
|
|
186
|
+
time.
|
|
40
187
|
"""
|
|
41
188
|
if dt is None:
|
|
42
189
|
dt = float(G.graph.get("DT", DEFAULTS["DT"]))
|
|
@@ -52,99 +199,229 @@ def prepare_integration_params(
|
|
|
52
199
|
else:
|
|
53
200
|
t = float(t)
|
|
54
201
|
|
|
55
|
-
|
|
202
|
+
method_value = (
|
|
56
203
|
method
|
|
57
|
-
or G.graph.get(
|
|
58
|
-
"INTEGRATOR_METHOD", DEFAULTS.get("INTEGRATOR_METHOD", "euler")
|
|
59
|
-
)
|
|
204
|
+
or G.graph.get("INTEGRATOR_METHOD", DEFAULTS.get("INTEGRATOR_METHOD", "euler"))
|
|
60
205
|
).lower()
|
|
61
|
-
if
|
|
206
|
+
if method_value not in ("euler", "rk4"):
|
|
62
207
|
raise ValueError("method must be 'euler' or 'rk4'")
|
|
63
208
|
|
|
64
209
|
dt_min = float(G.graph.get("DT_MIN", DEFAULTS.get("DT_MIN", 0.0)))
|
|
210
|
+
steps = 1
|
|
65
211
|
if dt_min > 0 and dt > dt_min:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
steps
|
|
69
|
-
|
|
70
|
-
dt_step = dt / steps
|
|
212
|
+
ratio = dt / dt_min
|
|
213
|
+
steps = max(1, int(math.floor(ratio + 1e-12)))
|
|
214
|
+
if dt / steps < dt_min:
|
|
215
|
+
steps = int(math.ceil(ratio))
|
|
216
|
+
dt_step = dt / steps if steps else 0.0
|
|
71
217
|
|
|
72
|
-
return dt_step, steps, t,
|
|
218
|
+
return dt_step, steps, t, cast(Literal["euler", "rk4"], method_value)
|
|
73
219
|
|
|
74
220
|
|
|
75
221
|
def _apply_increments(
|
|
76
|
-
G:
|
|
222
|
+
G: TNFRGraph,
|
|
77
223
|
dt_step: float,
|
|
78
|
-
increments:
|
|
224
|
+
increments: NodeIncrements,
|
|
79
225
|
*,
|
|
80
226
|
method: str,
|
|
81
|
-
|
|
227
|
+
n_jobs: int | None = None,
|
|
228
|
+
) -> NodalUpdate:
|
|
82
229
|
"""Combine precomputed increments to update node states."""
|
|
83
230
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
231
|
+
nodes: list[NodeId] = list(G.nodes)
|
|
232
|
+
if not nodes:
|
|
233
|
+
return {}
|
|
234
|
+
|
|
235
|
+
np = get_numpy()
|
|
236
|
+
|
|
237
|
+
epi_initial: list[float] = []
|
|
238
|
+
dEPI_prev: list[float] = []
|
|
239
|
+
ordered_increments: list[tuple[float, ...]] = []
|
|
240
|
+
|
|
241
|
+
for node in nodes:
|
|
242
|
+
nd = G.nodes[node]
|
|
243
|
+
_, _, dEPI_dt_prev, epi_i = _node_state(nd)
|
|
244
|
+
epi_initial.append(float(epi_i))
|
|
245
|
+
dEPI_prev.append(float(dEPI_dt_prev))
|
|
246
|
+
ordered_increments.append(increments[node])
|
|
247
|
+
|
|
248
|
+
if np is not None:
|
|
249
|
+
epi_arr = np.asarray(epi_initial, dtype=float)
|
|
250
|
+
dEPI_prev_arr = np.asarray(dEPI_prev, dtype=float)
|
|
251
|
+
k_arr = np.asarray(ordered_increments, dtype=float)
|
|
252
|
+
|
|
88
253
|
if method == "rk4":
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
254
|
+
if k_arr.ndim != 2 or k_arr.shape[1] != 4:
|
|
255
|
+
raise ValueError("rk4 increments require four staged values")
|
|
256
|
+
dt_factor = dt_step / 6.0
|
|
257
|
+
k1 = k_arr[:, 0]
|
|
258
|
+
k2 = k_arr[:, 1]
|
|
259
|
+
k3 = k_arr[:, 2]
|
|
260
|
+
k4 = k_arr[:, 3]
|
|
261
|
+
epi = epi_arr + dt_factor * (k1 + 2 * k2 + 2 * k3 + k4)
|
|
92
262
|
dEPI_dt = k4
|
|
93
263
|
else:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
264
|
+
if k_arr.ndim == 1:
|
|
265
|
+
k1 = k_arr
|
|
266
|
+
else:
|
|
267
|
+
k1 = k_arr[:, 0]
|
|
268
|
+
epi = epi_arr + dt_step * k1
|
|
97
269
|
dEPI_dt = k1
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
270
|
+
|
|
271
|
+
if dt_step != 0:
|
|
272
|
+
d2epi = (dEPI_dt - dEPI_prev_arr) / dt_step
|
|
273
|
+
else:
|
|
274
|
+
d2epi = np.zeros_like(dEPI_dt)
|
|
275
|
+
|
|
276
|
+
results: NodalUpdate = {}
|
|
277
|
+
for idx, node in enumerate(nodes):
|
|
278
|
+
results[node] = (
|
|
279
|
+
float(epi[idx]),
|
|
280
|
+
float(dEPI_dt[idx]),
|
|
281
|
+
float(d2epi[idx]),
|
|
282
|
+
)
|
|
283
|
+
return results
|
|
284
|
+
|
|
285
|
+
payload: list[tuple[NodeId, float, float, tuple[float, ...]]] = list(
|
|
286
|
+
zip(nodes, epi_initial, dEPI_prev, ordered_increments)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
workers = _normalise_jobs(n_jobs, len(nodes))
|
|
290
|
+
if workers is None:
|
|
291
|
+
return dict(_apply_increment_chunk(payload, dt_step, method))
|
|
292
|
+
|
|
293
|
+
approx_chunk = math.ceil(len(nodes) / (workers * 4)) if workers > 0 else None
|
|
294
|
+
chunk_size = resolve_chunk_size(
|
|
295
|
+
approx_chunk,
|
|
296
|
+
len(nodes),
|
|
297
|
+
minimum=1,
|
|
298
|
+
)
|
|
299
|
+
mp_ctx = get_context("spawn")
|
|
300
|
+
|
|
301
|
+
results: NodalUpdate = {}
|
|
302
|
+
with ProcessPoolExecutor(max_workers=workers, mp_context=mp_ctx) as executor:
|
|
303
|
+
futures = [
|
|
304
|
+
executor.submit(
|
|
305
|
+
_apply_increment_chunk,
|
|
306
|
+
chunk,
|
|
307
|
+
dt_step,
|
|
308
|
+
method,
|
|
309
|
+
)
|
|
310
|
+
for chunk in _chunk_nodes(payload, chunk_size)
|
|
311
|
+
]
|
|
312
|
+
for fut in futures:
|
|
313
|
+
for node, value in fut.result():
|
|
314
|
+
results[node] = value
|
|
315
|
+
|
|
316
|
+
return {node: results[node] for node in nodes}
|
|
101
317
|
|
|
102
318
|
|
|
103
319
|
def _collect_nodal_increments(
|
|
104
|
-
G:
|
|
105
|
-
gamma_maps: tuple[
|
|
320
|
+
G: TNFRGraph,
|
|
321
|
+
gamma_maps: tuple[GammaMap, ...],
|
|
106
322
|
*,
|
|
107
323
|
method: str,
|
|
108
|
-
) ->
|
|
324
|
+
) -> NodeIncrements:
|
|
109
325
|
"""Combine node base state with staged Γ contributions.
|
|
110
326
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
327
|
+
Implements the canonical TNFR nodal equation in two parts:
|
|
328
|
+
|
|
329
|
+
1. **Base term** (canonical equation):
|
|
330
|
+
base = vf * dnfr → ∂EPI/∂t = νf · ΔNFR(t)
|
|
331
|
+
|
|
332
|
+
This is the fundamental TNFR equation where:
|
|
333
|
+
- vf (νf): structural frequency in Hz_str
|
|
334
|
+
- dnfr (ΔNFR): nodal gradient (reorganization operator)
|
|
335
|
+
- base: instantaneous rate of EPI evolution
|
|
336
|
+
|
|
337
|
+
2. **Network coupling term**:
|
|
338
|
+
Γi(R) from gamma_maps - optional Kuramoto order parameter
|
|
339
|
+
|
|
340
|
+
The full extended equation is: ∂EPI/∂t = νf·ΔNFR(t) + Γi(R)
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
G: TNFR graph with node attributes vf and dnfr
|
|
344
|
+
gamma_maps: Staged Γ evaluations (1 for Euler, 4 for RK4)
|
|
345
|
+
method: Integration method ('euler' or 'rk4')
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Mapping of nodes to staged integration increments
|
|
349
|
+
|
|
350
|
+
Notes:
|
|
351
|
+
- Line 321 implements the canonical nodal equation explicitly
|
|
352
|
+
- Units: vf in Hz_str, dnfr dimensionless, base in Hz_str
|
|
353
|
+
- Preserves TNFR operator closure and structural semantics
|
|
114
354
|
"""
|
|
115
355
|
|
|
116
|
-
|
|
117
|
-
|
|
356
|
+
nodes: list[NodeId] = list(G.nodes())
|
|
357
|
+
if not nodes:
|
|
358
|
+
return {}
|
|
359
|
+
|
|
360
|
+
if method == "rk4":
|
|
361
|
+
expected_maps = 4
|
|
362
|
+
elif method == "euler":
|
|
363
|
+
expected_maps = 1
|
|
364
|
+
else:
|
|
365
|
+
raise ValueError("method must be 'euler' or 'rk4'")
|
|
366
|
+
|
|
367
|
+
if len(gamma_maps) != expected_maps:
|
|
368
|
+
raise ValueError(f"{method} integration requires {expected_maps} gamma maps")
|
|
369
|
+
|
|
370
|
+
np = get_numpy()
|
|
371
|
+
if np is not None:
|
|
372
|
+
vf = collect_attr(G, nodes, ALIAS_VF, 0.0, np=np)
|
|
373
|
+
dnfr = collect_attr(G, nodes, ALIAS_DNFR, 0.0, np=np)
|
|
374
|
+
# CANONICAL TNFR EQUATION: ∂EPI/∂t = νf · ΔNFR(t)
|
|
375
|
+
# This implements the fundamental nodal equation explicitly
|
|
376
|
+
base = vf * dnfr
|
|
377
|
+
|
|
378
|
+
gamma_arrays = [
|
|
379
|
+
np.fromiter((gm.get(n, 0.0) for n in nodes), float, count=len(nodes))
|
|
380
|
+
for gm in gamma_maps
|
|
381
|
+
]
|
|
382
|
+
if gamma_arrays:
|
|
383
|
+
gamma_stack = np.stack(gamma_arrays, axis=1)
|
|
384
|
+
combined = base[:, None] + gamma_stack
|
|
385
|
+
else:
|
|
386
|
+
combined = base[:, None]
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
node: tuple(float(value) for value in combined[idx])
|
|
390
|
+
for idx, node in enumerate(nodes)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
increments: NodeIncrements = {}
|
|
394
|
+
for node in nodes:
|
|
395
|
+
nd = G.nodes[node]
|
|
118
396
|
vf, dnfr, *_ = _node_state(nd)
|
|
397
|
+
# CANONICAL TNFR EQUATION: ∂EPI/∂t = νf · ΔNFR(t)
|
|
398
|
+
# Scalar implementation of the fundamental nodal equation
|
|
119
399
|
base = vf * dnfr
|
|
120
|
-
gammas = [gm.get(
|
|
400
|
+
gammas = [gm.get(node, 0.0) for gm in gamma_maps]
|
|
121
401
|
|
|
122
402
|
if method == "rk4":
|
|
123
|
-
if len(gammas) != 4:
|
|
124
|
-
raise ValueError("rk4 integration requires four gamma maps")
|
|
125
403
|
k1, k2, k3, k4 = gammas
|
|
126
|
-
increments[
|
|
404
|
+
increments[node] = (
|
|
127
405
|
base + k1,
|
|
128
406
|
base + k2,
|
|
129
407
|
base + k3,
|
|
130
408
|
base + k4,
|
|
131
409
|
)
|
|
132
410
|
else:
|
|
133
|
-
if len(gammas) != 1:
|
|
134
|
-
raise ValueError("euler integration requires one gamma map")
|
|
135
411
|
(k1,) = gammas
|
|
136
|
-
increments[
|
|
412
|
+
increments[node] = (base + k1,)
|
|
137
413
|
|
|
138
414
|
return increments
|
|
139
415
|
|
|
140
416
|
|
|
141
417
|
def _build_gamma_increments(
|
|
142
|
-
G:
|
|
418
|
+
G: TNFRGraph,
|
|
143
419
|
dt_step: float,
|
|
144
420
|
t_local: float,
|
|
145
421
|
*,
|
|
146
422
|
method: str,
|
|
147
|
-
|
|
423
|
+
n_jobs: int | None = None,
|
|
424
|
+
) -> NodeIncrements:
|
|
148
425
|
"""Evaluate Γ contributions and merge them with ``νf·ΔNFR`` base terms."""
|
|
149
426
|
|
|
150
427
|
if method == "rk4":
|
|
@@ -163,50 +440,170 @@ def _build_gamma_increments(
|
|
|
163
440
|
gamma_type = str(gamma_spec.get("type", "")).lower()
|
|
164
441
|
|
|
165
442
|
if gamma_type == "none":
|
|
166
|
-
gamma_maps
|
|
443
|
+
gamma_maps: tuple[GammaMap, ...] = tuple(
|
|
444
|
+
cast(GammaMap, {}) for _ in range(gamma_count)
|
|
445
|
+
)
|
|
446
|
+
return _collect_nodal_increments(G, gamma_maps, method=method)
|
|
447
|
+
|
|
448
|
+
nodes: list[NodeId] = list(G.nodes)
|
|
449
|
+
if not nodes:
|
|
450
|
+
gamma_maps = tuple(cast(GammaMap, {}) for _ in range(gamma_count))
|
|
167
451
|
return _collect_nodal_increments(G, gamma_maps, method=method)
|
|
168
452
|
|
|
169
453
|
if method == "rk4":
|
|
170
454
|
t_mid = t_local + dt_step / 2.0
|
|
171
455
|
t_end = t_local + dt_step
|
|
172
|
-
g1_map =
|
|
173
|
-
g_mid_map =
|
|
174
|
-
g4_map =
|
|
456
|
+
g1_map = _evaluate_gamma_map(G, nodes, t_local, n_jobs=n_jobs)
|
|
457
|
+
g_mid_map = _evaluate_gamma_map(G, nodes, t_mid, n_jobs=n_jobs)
|
|
458
|
+
g4_map = _evaluate_gamma_map(G, nodes, t_end, n_jobs=n_jobs)
|
|
175
459
|
gamma_maps = (g1_map, g_mid_map, g_mid_map, g4_map)
|
|
176
460
|
else: # method == "euler"
|
|
177
|
-
gamma_maps = (
|
|
461
|
+
gamma_maps = (_evaluate_gamma_map(G, nodes, t_local, n_jobs=n_jobs),)
|
|
178
462
|
|
|
179
463
|
return _collect_nodal_increments(G, gamma_maps, method=method)
|
|
180
464
|
|
|
181
465
|
|
|
182
|
-
def _integrate_euler(
|
|
466
|
+
def _integrate_euler(
|
|
467
|
+
G: TNFRGraph,
|
|
468
|
+
dt_step: float,
|
|
469
|
+
t_local: float,
|
|
470
|
+
*,
|
|
471
|
+
n_jobs: int | None = None,
|
|
472
|
+
) -> NodalUpdate:
|
|
183
473
|
"""One explicit Euler integration step."""
|
|
184
474
|
increments = _build_gamma_increments(
|
|
185
475
|
G,
|
|
186
476
|
dt_step,
|
|
187
477
|
t_local,
|
|
188
478
|
method="euler",
|
|
479
|
+
n_jobs=n_jobs,
|
|
480
|
+
)
|
|
481
|
+
return _apply_increments(
|
|
482
|
+
G,
|
|
483
|
+
dt_step,
|
|
484
|
+
increments,
|
|
485
|
+
method="euler",
|
|
486
|
+
n_jobs=n_jobs,
|
|
189
487
|
)
|
|
190
|
-
return _apply_increments(G, dt_step, increments, method="euler")
|
|
191
488
|
|
|
192
489
|
|
|
193
|
-
def _integrate_rk4(
|
|
490
|
+
def _integrate_rk4(
|
|
491
|
+
G: TNFRGraph,
|
|
492
|
+
dt_step: float,
|
|
493
|
+
t_local: float,
|
|
494
|
+
*,
|
|
495
|
+
n_jobs: int | None = None,
|
|
496
|
+
) -> NodalUpdate:
|
|
194
497
|
"""One Runge–Kutta order-4 integration step."""
|
|
195
498
|
increments = _build_gamma_increments(
|
|
196
499
|
G,
|
|
197
500
|
dt_step,
|
|
198
501
|
t_local,
|
|
199
502
|
method="rk4",
|
|
503
|
+
n_jobs=n_jobs,
|
|
504
|
+
)
|
|
505
|
+
return _apply_increments(
|
|
506
|
+
G,
|
|
507
|
+
dt_step,
|
|
508
|
+
increments,
|
|
509
|
+
method="rk4",
|
|
510
|
+
n_jobs=n_jobs,
|
|
200
511
|
)
|
|
201
|
-
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class AbstractIntegrator(ABC):
|
|
515
|
+
"""Abstract base class encapsulating nodal equation integration."""
|
|
516
|
+
|
|
517
|
+
@abstractmethod
|
|
518
|
+
def integrate(
|
|
519
|
+
self,
|
|
520
|
+
graph: TNFRGraph,
|
|
521
|
+
*,
|
|
522
|
+
dt: float | None,
|
|
523
|
+
t: float | None,
|
|
524
|
+
method: str | None,
|
|
525
|
+
n_jobs: int | None,
|
|
526
|
+
) -> None:
|
|
527
|
+
"""Advance ``graph`` coherence states according to the nodal equation."""
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
class DefaultIntegrator(AbstractIntegrator):
|
|
531
|
+
"""Explicit integrator combining Euler and RK4 step implementations."""
|
|
532
|
+
|
|
533
|
+
def integrate(
|
|
534
|
+
self,
|
|
535
|
+
graph: TNFRGraph,
|
|
536
|
+
*,
|
|
537
|
+
dt: float | None,
|
|
538
|
+
t: float | None,
|
|
539
|
+
method: str | None,
|
|
540
|
+
n_jobs: int | None,
|
|
541
|
+
) -> None:
|
|
542
|
+
"""Integrate the nodal equation updating EPI, ΔEPI and Δ²EPI."""
|
|
543
|
+
|
|
544
|
+
if not isinstance(
|
|
545
|
+
graph, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)
|
|
546
|
+
):
|
|
547
|
+
raise TypeError("G must be a networkx graph instance")
|
|
548
|
+
|
|
549
|
+
dt_step, steps, t0, resolved_method = prepare_integration_params(
|
|
550
|
+
graph, dt, t, cast(IntegratorMethod | None, method)
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
t_local = t0
|
|
554
|
+
for _ in range(steps):
|
|
555
|
+
if resolved_method == "rk4":
|
|
556
|
+
updates: NodalUpdate = _integrate_rk4(
|
|
557
|
+
graph, dt_step, t_local, n_jobs=n_jobs
|
|
558
|
+
)
|
|
559
|
+
else:
|
|
560
|
+
updates = _integrate_euler(graph, dt_step, t_local, n_jobs=n_jobs)
|
|
561
|
+
|
|
562
|
+
for n, (epi, dEPI_dt, d2epi) in updates.items():
|
|
563
|
+
nd = graph.nodes[n]
|
|
564
|
+
epi_kind = get_attr_str(nd, ALIAS_EPI_KIND, "")
|
|
565
|
+
|
|
566
|
+
# Apply structural boundary preservation
|
|
567
|
+
epi_min = float(
|
|
568
|
+
graph.graph.get("EPI_MIN", DEFAULTS.get("EPI_MIN", -1.0))
|
|
569
|
+
)
|
|
570
|
+
epi_max = float(
|
|
571
|
+
graph.graph.get("EPI_MAX", DEFAULTS.get("EPI_MAX", 1.0))
|
|
572
|
+
)
|
|
573
|
+
clip_mode_str = str(graph.graph.get("CLIP_MODE", "hard"))
|
|
574
|
+
# Validate clip mode and cast to proper type
|
|
575
|
+
if clip_mode_str not in ("hard", "soft"):
|
|
576
|
+
clip_mode_str = "hard"
|
|
577
|
+
clip_mode: Literal["hard", "soft"] = clip_mode_str # type: ignore[assignment]
|
|
578
|
+
clip_k = float(graph.graph.get("CLIP_SOFT_K", 3.0))
|
|
579
|
+
|
|
580
|
+
epi_clipped = structural_clip(
|
|
581
|
+
epi,
|
|
582
|
+
lo=epi_min,
|
|
583
|
+
hi=epi_max,
|
|
584
|
+
mode=clip_mode,
|
|
585
|
+
k=clip_k,
|
|
586
|
+
record_stats=False,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
set_attr(nd, ALIAS_EPI, epi_clipped)
|
|
590
|
+
if epi_kind:
|
|
591
|
+
set_attr_str(nd, ALIAS_EPI_KIND, epi_kind)
|
|
592
|
+
set_attr(nd, ALIAS_DEPI, dEPI_dt)
|
|
593
|
+
set_attr(nd, ALIAS_D2EPI, d2epi)
|
|
594
|
+
|
|
595
|
+
t_local += dt_step
|
|
596
|
+
|
|
597
|
+
graph.graph["_t"] = t_local
|
|
202
598
|
|
|
203
599
|
|
|
204
600
|
def update_epi_via_nodal_equation(
|
|
205
|
-
G,
|
|
601
|
+
G: TNFRGraph,
|
|
206
602
|
*,
|
|
207
603
|
dt: float | None = None,
|
|
208
604
|
t: float | None = None,
|
|
209
605
|
method: Literal["euler", "rk4"] | None = None,
|
|
606
|
+
n_jobs: int | None = None,
|
|
210
607
|
) -> None:
|
|
211
608
|
"""TNFR nodal equation.
|
|
212
609
|
|
|
@@ -224,40 +621,37 @@ def update_epi_via_nodal_equation(
|
|
|
224
621
|
TNFR references: nodal equation (manual), νf/ΔNFR/EPI glossary, Γ operator.
|
|
225
622
|
Side effects: caches dEPI and updates EPI via explicit integration.
|
|
226
623
|
"""
|
|
227
|
-
|
|
228
|
-
G,
|
|
229
|
-
|
|
230
|
-
|
|
624
|
+
DefaultIntegrator().integrate(
|
|
625
|
+
G,
|
|
626
|
+
dt=dt,
|
|
627
|
+
t=t,
|
|
628
|
+
method=method,
|
|
629
|
+
n_jobs=n_jobs,
|
|
630
|
+
)
|
|
231
631
|
|
|
232
|
-
dt_step, steps, t0, method = prepare_integration_params(G, dt, t, method)
|
|
233
632
|
|
|
234
|
-
|
|
235
|
-
for
|
|
236
|
-
if method == "rk4":
|
|
237
|
-
updates = _integrate_rk4(G, dt_step, t_local)
|
|
238
|
-
else:
|
|
239
|
-
updates = _integrate_euler(G, dt_step, t_local)
|
|
240
|
-
|
|
241
|
-
for n, (epi, dEPI_dt, d2epi) in updates.items():
|
|
242
|
-
nd = G.nodes[n]
|
|
243
|
-
epi_kind = get_attr_str(nd, ALIAS_EPI_KIND, "")
|
|
244
|
-
set_attr(nd, ALIAS_EPI, epi)
|
|
245
|
-
if epi_kind:
|
|
246
|
-
set_attr_str(nd, ALIAS_EPI_KIND, epi_kind)
|
|
247
|
-
set_attr(nd, ALIAS_DEPI, dEPI_dt)
|
|
248
|
-
set_attr(nd, ALIAS_D2EPI, d2epi)
|
|
633
|
+
def _node_state(nd: dict[str, Any]) -> tuple[float, float, float, float]:
|
|
634
|
+
"""Return common node state attributes for canonical equation evaluation.
|
|
249
635
|
|
|
250
|
-
|
|
636
|
+
Extracts the fundamental TNFR variables from node data:
|
|
637
|
+
- νf (vf): Structural frequency in Hz_str
|
|
638
|
+
- ΔNFR (dnfr): Nodal gradient (reorganization operator)
|
|
639
|
+
- dEPI/dt (previous): Last computed EPI derivative
|
|
640
|
+
- EPI (current): Current Primary Information Structure
|
|
251
641
|
|
|
252
|
-
|
|
642
|
+
These variables are used in the canonical nodal equation:
|
|
643
|
+
∂EPI/∂t = νf · ΔNFR(t)
|
|
253
644
|
|
|
645
|
+
Args:
|
|
646
|
+
nd: Node data dictionary containing TNFR attributes
|
|
254
647
|
|
|
255
|
-
|
|
256
|
-
|
|
648
|
+
Returns:
|
|
649
|
+
Tuple of (vf, dnfr, dEPI_dt_prev, epi_i) with 0.0 defaults
|
|
257
650
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
651
|
+
Notes:
|
|
652
|
+
- vf alias maps to VF, frequency, or structural_frequency
|
|
653
|
+
- dnfr alias maps to DNFR, delta_nfr, or reorganization_gradient
|
|
654
|
+
- All values are coerced to float for numerical stability
|
|
261
655
|
"""
|
|
262
656
|
|
|
263
657
|
vf = get_attr(nd, ALIAS_VF, 0.0)
|