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/nbody.py
ADDED
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
"""Classical N-body problem implementation in TNFR structural framework.
|
|
2
|
+
|
|
3
|
+
⚠️ **IMPORTANT LIMITATION**: This module ASSUMES Newtonian gravitational potential:
|
|
4
|
+
U(q) = -Σ_{i<j} G * m_i * m_j / |r_i - r_j|
|
|
5
|
+
|
|
6
|
+
This is an **external assumption**, NOT derived from TNFR first principles!
|
|
7
|
+
|
|
8
|
+
For a PURE TNFR implementation (no gravitational assumption), see:
|
|
9
|
+
tnfr.dynamics.nbody_tnfr
|
|
10
|
+
|
|
11
|
+
That module derives dynamics from coherence potential and Hamiltonian
|
|
12
|
+
commutator, with NO classical force law assumptions.
|
|
13
|
+
|
|
14
|
+
Purpose of This Module
|
|
15
|
+
-----------------------
|
|
16
|
+
|
|
17
|
+
This module demonstrates how TNFR can **reproduce** classical mechanics
|
|
18
|
+
when we explicitly map classical potentials into the TNFR framework.
|
|
19
|
+
It shows the correspondence:
|
|
20
|
+
|
|
21
|
+
Classical Mechanics ←→ TNFR Framework
|
|
22
|
+
------------------- ---------------
|
|
23
|
+
Position q ←→ EPI spatial component
|
|
24
|
+
Velocity v ←→ EPI velocity component
|
|
25
|
+
Mass m ←→ 1/νf (structural inertia)
|
|
26
|
+
Force F = -∇U ←→ ΔNFR (ASSUMED from classical U)
|
|
27
|
+
Newton's 2nd law ←→ Nodal equation ∂EPI/∂t = νf·ΔNFR
|
|
28
|
+
|
|
29
|
+
Comparison:
|
|
30
|
+
-----------
|
|
31
|
+
|
|
32
|
+
**This module** (nbody.py):
|
|
33
|
+
```python
|
|
34
|
+
# Assumes gravitational potential
|
|
35
|
+
U = -Σ G*m_i*m_j/r_ij
|
|
36
|
+
F = -∇U # Classical force
|
|
37
|
+
ΔNFR = F/m # External assumption
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Pure TNFR** (nbody_tnfr.py):
|
|
41
|
+
```python
|
|
42
|
+
# NO assumed potential
|
|
43
|
+
H_int = H_coh + H_freq + H_coupling
|
|
44
|
+
ΔNFR = i[H_int, ·]/ℏ_str # From Hamiltonian
|
|
45
|
+
# Forces emerge from coherence/phase sync
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Theoretical Foundation
|
|
49
|
+
----------------------
|
|
50
|
+
|
|
51
|
+
The classical N-body problem emerges from TNFR as the **low-dissonance
|
|
52
|
+
coherence regime** where:
|
|
53
|
+
|
|
54
|
+
1. **Mass as inverse frequency**: m_i = 1/νf_i
|
|
55
|
+
High mass → low structural reorganization rate (inertia)
|
|
56
|
+
Low mass → high structural reorganization rate (responsiveness)
|
|
57
|
+
|
|
58
|
+
2. **Gravitational potential as coherence potential** (ASSUMED):
|
|
59
|
+
U(q) = -Σ_{i<j} G * m_i * m_j / |r_i - r_j|
|
|
60
|
+
|
|
61
|
+
This potential encodes structural stability landscape. Nodes
|
|
62
|
+
naturally evolve toward configurations of higher coherence
|
|
63
|
+
(lower potential energy).
|
|
64
|
+
|
|
65
|
+
3. **Nodal equation integration**:
|
|
66
|
+
∂EPI/∂t = νf · ΔNFR(t)
|
|
67
|
+
|
|
68
|
+
Where EPI encodes position and velocity, and ΔNFR is computed
|
|
69
|
+
from the gravitational coherence gradient (ASSUMED).
|
|
70
|
+
|
|
71
|
+
Mathematical Correspondence
|
|
72
|
+
---------------------------
|
|
73
|
+
|
|
74
|
+
Classical mechanics: TNFR structural dynamics:
|
|
75
|
+
- Position q_i → EPI spatial component
|
|
76
|
+
- Velocity v_i → EPI velocity component
|
|
77
|
+
- Mass m_i → 1/νf_i (structural inertia)
|
|
78
|
+
- Force F_i = -∇U → ΔNFR (coherence gradient, ASSUMED)
|
|
79
|
+
- Newton's 2nd law → Nodal equation ∂EPI/∂t = νf·ΔNFR
|
|
80
|
+
|
|
81
|
+
Conservation Laws
|
|
82
|
+
-----------------
|
|
83
|
+
|
|
84
|
+
The implementation preserves:
|
|
85
|
+
- Total energy (H_int = T + U)
|
|
86
|
+
- Linear momentum (Σ m_i * v_i)
|
|
87
|
+
- Angular momentum (Σ r_i × m_i * v_i)
|
|
88
|
+
|
|
89
|
+
These emerge naturally from the Hamiltonian structure and
|
|
90
|
+
translational/rotational symmetry of the coherence potential.
|
|
91
|
+
|
|
92
|
+
References
|
|
93
|
+
----------
|
|
94
|
+
- tnfr.dynamics.nbody_tnfr: Pure TNFR n-body (no assumptions)
|
|
95
|
+
- docs/source/theory/07_emergence_classical_mechanics.md
|
|
96
|
+
- docs/source/theory/08_classical_mechanics_euler_lagrange.md
|
|
97
|
+
- TNFR.pdf: Canonical nodal equation (§2.3)
|
|
98
|
+
- AGENTS.md: Canonical invariants (§3)
|
|
99
|
+
|
|
100
|
+
Examples
|
|
101
|
+
--------
|
|
102
|
+
Two-body orbit (Earth-Moon system) with ASSUMED gravity:
|
|
103
|
+
|
|
104
|
+
>>> from tnfr.dynamics.nbody import NBodySystem
|
|
105
|
+
>>> import numpy as np
|
|
106
|
+
>>>
|
|
107
|
+
>>> # Create 2-body system (dimensionless units)
|
|
108
|
+
>>> system = NBodySystem(
|
|
109
|
+
... n_bodies=2,
|
|
110
|
+
... masses=[1.0, 0.012], # Mass ratio ~ Earth/Moon
|
|
111
|
+
... G=1.0 # Gravitational constant (ASSUMED)
|
|
112
|
+
... )
|
|
113
|
+
>>>
|
|
114
|
+
>>> # Initialize circular orbit
|
|
115
|
+
>>> positions = np.array([
|
|
116
|
+
... [0.0, 0.0, 0.0], # Earth at origin
|
|
117
|
+
... [1.0, 0.0, 0.0] # Moon at distance 1
|
|
118
|
+
... ])
|
|
119
|
+
>>> velocities = np.array([
|
|
120
|
+
... [0.0, 0.0, 0.0], # Earth at rest (CM frame)
|
|
121
|
+
... [0.0, 1.0, 0.0] # Moon with tangential velocity
|
|
122
|
+
... ])
|
|
123
|
+
>>>
|
|
124
|
+
>>> system.set_state(positions, velocities)
|
|
125
|
+
>>>
|
|
126
|
+
>>> # Evolve system (structural time)
|
|
127
|
+
>>> history = system.evolve(t_final=10.0, dt=0.01)
|
|
128
|
+
>>>
|
|
129
|
+
>>> # Check energy conservation
|
|
130
|
+
>>> E0 = history['energy'][0]
|
|
131
|
+
>>> E_final = history['energy'][-1]
|
|
132
|
+
>>> print(f"Energy drift: {abs(E_final - E0) / abs(E0):.2e}")
|
|
133
|
+
|
|
134
|
+
Three-body system (Figure-8 orbit):
|
|
135
|
+
|
|
136
|
+
>>> system = NBodySystem(n_bodies=3, masses=[1.0, 1.0, 1.0], G=1.0)
|
|
137
|
+
>>> # Use known figure-8 initial conditions
|
|
138
|
+
>>> # (See Chenciner & Montgomery, 2000)
|
|
139
|
+
>>> history = system.evolve(t_final=6.3, dt=0.001)
|
|
140
|
+
>>> system.plot_trajectories(history)
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
from __future__ import annotations
|
|
144
|
+
|
|
145
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Optional
|
|
146
|
+
|
|
147
|
+
import numpy as np
|
|
148
|
+
from numpy.typing import NDArray
|
|
149
|
+
|
|
150
|
+
from ..alias import get_attr
|
|
151
|
+
from ..structural import create_nfr
|
|
152
|
+
from ..types import TNFRGraph
|
|
153
|
+
|
|
154
|
+
if TYPE_CHECKING:
|
|
155
|
+
from matplotlib.figure import Figure
|
|
156
|
+
|
|
157
|
+
__all__ = (
|
|
158
|
+
"NBodySystem",
|
|
159
|
+
"gravitational_potential",
|
|
160
|
+
"gravitational_force",
|
|
161
|
+
"compute_gravitational_dnfr",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def gravitational_potential(
|
|
166
|
+
positions: NDArray[np.floating],
|
|
167
|
+
masses: NDArray[np.floating],
|
|
168
|
+
G: float = 1.0,
|
|
169
|
+
softening: float = 0.0,
|
|
170
|
+
) -> float:
|
|
171
|
+
"""Compute total Newtonian gravitational potential energy.
|
|
172
|
+
|
|
173
|
+
U(q) = -Σ_{i<j} G * m_i * m_j / |r_i - r_j|
|
|
174
|
+
|
|
175
|
+
This is the coherence potential in TNFR language: lower U means
|
|
176
|
+
higher structural stability (attractive gravitational well).
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
positions : ndarray, shape (N, 3)
|
|
181
|
+
Positions of N bodies in 3D space
|
|
182
|
+
masses : ndarray, shape (N,)
|
|
183
|
+
Masses of N bodies
|
|
184
|
+
G : float, default=1.0
|
|
185
|
+
Gravitational constant (in appropriate units)
|
|
186
|
+
softening : float, default=0.0
|
|
187
|
+
Softening length to avoid singularities at r=0.
|
|
188
|
+
Effective distance: r_eff = sqrt(r² + ε²)
|
|
189
|
+
|
|
190
|
+
Returns
|
|
191
|
+
-------
|
|
192
|
+
U : float
|
|
193
|
+
Total gravitational potential energy (negative)
|
|
194
|
+
|
|
195
|
+
Notes
|
|
196
|
+
-----
|
|
197
|
+
The negative sign ensures bound states have U < 0, matching
|
|
198
|
+
the TNFR convention that coherent states minimize the potential.
|
|
199
|
+
"""
|
|
200
|
+
N = len(positions)
|
|
201
|
+
U = 0.0
|
|
202
|
+
|
|
203
|
+
for i in range(N):
|
|
204
|
+
for j in range(i + 1, N):
|
|
205
|
+
r_ij = positions[j] - positions[i]
|
|
206
|
+
dist = np.sqrt(np.sum(r_ij**2) + softening**2)
|
|
207
|
+
U -= G * masses[i] * masses[j] / dist
|
|
208
|
+
|
|
209
|
+
return U
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def gravitational_force(
|
|
213
|
+
positions: NDArray[np.floating],
|
|
214
|
+
masses: NDArray[np.floating],
|
|
215
|
+
G: float = 1.0,
|
|
216
|
+
softening: float = 0.0,
|
|
217
|
+
) -> NDArray[np.floating]:
|
|
218
|
+
"""Compute gravitational forces on all bodies.
|
|
219
|
+
|
|
220
|
+
F_i = -∇_i U = Σ_{j≠i} G * m_i * m_j * (r_j - r_i) / |r_j - r_i|³
|
|
221
|
+
|
|
222
|
+
In TNFR language: force is the coherence gradient pointing toward
|
|
223
|
+
higher stability (lower potential).
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
positions : ndarray, shape (N, 3)
|
|
228
|
+
Positions of N bodies
|
|
229
|
+
masses : ndarray, shape (N,)
|
|
230
|
+
Masses of N bodies
|
|
231
|
+
G : float, default=1.0
|
|
232
|
+
Gravitational constant
|
|
233
|
+
softening : float, default=0.0
|
|
234
|
+
Softening length for numerical stability
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
forces : ndarray, shape (N, 3)
|
|
239
|
+
Gravitational forces on each body
|
|
240
|
+
|
|
241
|
+
Notes
|
|
242
|
+
-----
|
|
243
|
+
Force points from lower to higher coherence (lower potential).
|
|
244
|
+
Newton's 3rd law (F_ij = -F_ji) emerges from symmetry of U(q).
|
|
245
|
+
"""
|
|
246
|
+
N = len(positions)
|
|
247
|
+
forces = np.zeros_like(positions)
|
|
248
|
+
|
|
249
|
+
for i in range(N):
|
|
250
|
+
for j in range(N):
|
|
251
|
+
if i == j:
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
r_ij = positions[j] - positions[i]
|
|
255
|
+
dist_sq = np.sum(r_ij**2) + softening**2
|
|
256
|
+
dist = np.sqrt(dist_sq)
|
|
257
|
+
dist_cubed = dist_sq * dist
|
|
258
|
+
|
|
259
|
+
# F_i = G * m_i * m_j * (r_j - r_i) / |r_j - r_i|³
|
|
260
|
+
forces[i] += G * masses[i] * masses[j] * r_ij / dist_cubed
|
|
261
|
+
|
|
262
|
+
return forces
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def compute_gravitational_dnfr(
|
|
266
|
+
positions: NDArray[np.floating],
|
|
267
|
+
masses: NDArray[np.floating],
|
|
268
|
+
G: float = 1.0,
|
|
269
|
+
softening: float = 0.0,
|
|
270
|
+
) -> NDArray[np.floating]:
|
|
271
|
+
"""Compute ΔNFR from gravitational coherence gradient.
|
|
272
|
+
|
|
273
|
+
ΔNFR_i = F_i / m_i = a_i (acceleration)
|
|
274
|
+
|
|
275
|
+
This is the structural reorganization operator that drives evolution
|
|
276
|
+
via the nodal equation: ∂EPI/∂t = νf · ΔNFR
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
positions : ndarray, shape (N, 3)
|
|
281
|
+
Positions of N bodies
|
|
282
|
+
masses : ndarray, shape (N,)
|
|
283
|
+
Masses (or inverse frequencies: m = 1/νf)
|
|
284
|
+
G : float, default=1.0
|
|
285
|
+
Gravitational constant
|
|
286
|
+
softening : float, default=0.0
|
|
287
|
+
Softening length
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
-------
|
|
291
|
+
dnfr : ndarray, shape (N, 3)
|
|
292
|
+
ΔNFR values (accelerations) for each body
|
|
293
|
+
|
|
294
|
+
Notes
|
|
295
|
+
-----
|
|
296
|
+
ΔNFR = a = F/m is independent of mass by equivalence principle.
|
|
297
|
+
This is the reorganization "pressure" that drives structural change.
|
|
298
|
+
"""
|
|
299
|
+
forces = gravitational_force(positions, masses, G, softening)
|
|
300
|
+
|
|
301
|
+
# ΔNFR = F/m (acceleration)
|
|
302
|
+
# Broadcast division: (N, 3) / (N, 1) -> (N, 3)
|
|
303
|
+
dnfr = forces / masses[:, np.newaxis]
|
|
304
|
+
|
|
305
|
+
return dnfr
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class NBodySystem:
|
|
309
|
+
"""Classical N-body gravitational system in TNFR framework.
|
|
310
|
+
|
|
311
|
+
Implements N particles (resonant nodes) coupled through Newtonian
|
|
312
|
+
gravitational potential. Positions and velocities are encoded as
|
|
313
|
+
EPI components, masses as inverse frequencies (m = 1/νf), and
|
|
314
|
+
evolution follows the canonical nodal equation.
|
|
315
|
+
|
|
316
|
+
Attributes
|
|
317
|
+
----------
|
|
318
|
+
n_bodies : int
|
|
319
|
+
Number of bodies in the system
|
|
320
|
+
masses : ndarray, shape (N,)
|
|
321
|
+
Masses of bodies (m_i = 1/νf_i)
|
|
322
|
+
G : float
|
|
323
|
+
Gravitational constant
|
|
324
|
+
softening : float
|
|
325
|
+
Softening length for numerical stability
|
|
326
|
+
positions : ndarray, shape (N, 3)
|
|
327
|
+
Current positions
|
|
328
|
+
velocities : ndarray, shape (N, 3)
|
|
329
|
+
Current velocities
|
|
330
|
+
time : float
|
|
331
|
+
Current structural time
|
|
332
|
+
graph : TNFRGraph
|
|
333
|
+
NetworkX graph storing nodes as NFRs
|
|
334
|
+
|
|
335
|
+
Notes
|
|
336
|
+
-----
|
|
337
|
+
The system maintains TNFR canonical invariants:
|
|
338
|
+
- EPI encodes (position, velocity) pairs
|
|
339
|
+
- νf = 1/m (structural frequency from mass)
|
|
340
|
+
- ΔNFR computed from gravitational gradient
|
|
341
|
+
- Evolution via ∂EPI/∂t = νf · ΔNFR
|
|
342
|
+
|
|
343
|
+
Conservation laws emerge naturally from Hamiltonian structure.
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
def __init__(
|
|
347
|
+
self,
|
|
348
|
+
n_bodies: int,
|
|
349
|
+
masses: List[float] | NDArray[np.floating],
|
|
350
|
+
G: float = 1.0,
|
|
351
|
+
softening: float = 0.0,
|
|
352
|
+
):
|
|
353
|
+
"""Initialize N-body system.
|
|
354
|
+
|
|
355
|
+
Parameters
|
|
356
|
+
----------
|
|
357
|
+
n_bodies : int
|
|
358
|
+
Number of bodies
|
|
359
|
+
masses : array_like, shape (N,)
|
|
360
|
+
Masses of bodies (must be positive)
|
|
361
|
+
G : float, default=1.0
|
|
362
|
+
Gravitational constant
|
|
363
|
+
softening : float, default=0.0
|
|
364
|
+
Softening length (ε) for numerical stability.
|
|
365
|
+
Prevents singularities at r=0.
|
|
366
|
+
|
|
367
|
+
Raises
|
|
368
|
+
------
|
|
369
|
+
ValueError
|
|
370
|
+
If masses are non-positive or dimensions mismatch
|
|
371
|
+
"""
|
|
372
|
+
if n_bodies < 1:
|
|
373
|
+
raise ValueError(f"n_bodies must be >= 1, got {n_bodies}")
|
|
374
|
+
|
|
375
|
+
self.n_bodies = n_bodies
|
|
376
|
+
self.masses = np.array(masses, dtype=float)
|
|
377
|
+
|
|
378
|
+
if len(self.masses) != n_bodies:
|
|
379
|
+
raise ValueError(f"masses length {len(self.masses)} != n_bodies {n_bodies}")
|
|
380
|
+
|
|
381
|
+
if np.any(self.masses <= 0):
|
|
382
|
+
raise ValueError("All masses must be positive")
|
|
383
|
+
|
|
384
|
+
self.G = float(G)
|
|
385
|
+
self.softening = float(softening)
|
|
386
|
+
|
|
387
|
+
# State vectors
|
|
388
|
+
self.positions = np.zeros((n_bodies, 3), dtype=float)
|
|
389
|
+
self.velocities = np.zeros((n_bodies, 3), dtype=float)
|
|
390
|
+
self.time = 0.0
|
|
391
|
+
|
|
392
|
+
# Create TNFR graph representation
|
|
393
|
+
self._build_graph()
|
|
394
|
+
|
|
395
|
+
def _build_graph(self) -> None:
|
|
396
|
+
"""Build TNFR graph representation of the N-body system.
|
|
397
|
+
|
|
398
|
+
Each body becomes a resonant node with:
|
|
399
|
+
- νf = 1/m (structural frequency)
|
|
400
|
+
- EPI encoding (position, velocity)
|
|
401
|
+
- Phase initialized to 0 (can be set for rotation)
|
|
402
|
+
- Fully connected topology (all-to-all gravitational coupling)
|
|
403
|
+
"""
|
|
404
|
+
# Create empty graph (will add nodes manually)
|
|
405
|
+
import networkx as nx
|
|
406
|
+
|
|
407
|
+
self.graph: TNFRGraph = nx.Graph()
|
|
408
|
+
self.graph.graph["name"] = "nbody_system"
|
|
409
|
+
|
|
410
|
+
# Add nodes with TNFR attributes
|
|
411
|
+
for i in range(self.n_bodies):
|
|
412
|
+
node_id = f"body_{i}"
|
|
413
|
+
|
|
414
|
+
# Structural frequency: νf = 1/m
|
|
415
|
+
nu_f = 1.0 / self.masses[i]
|
|
416
|
+
|
|
417
|
+
# Create NFR node
|
|
418
|
+
_, _ = create_nfr(
|
|
419
|
+
node_id,
|
|
420
|
+
epi=1.0, # Will be overwritten by set_state
|
|
421
|
+
vf=nu_f,
|
|
422
|
+
theta=0.0, # Phase (for rotating systems)
|
|
423
|
+
graph=self.graph,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Add edges (all-to-all coupling for gravitational interaction)
|
|
427
|
+
# Edge weight represents gravitational coupling strength
|
|
428
|
+
for i in range(self.n_bodies):
|
|
429
|
+
for j in range(i + 1, self.n_bodies):
|
|
430
|
+
node_i = f"body_{i}"
|
|
431
|
+
node_j = f"body_{j}"
|
|
432
|
+
# Coupling weight: G * m_i * m_j
|
|
433
|
+
weight = self.G * self.masses[i] * self.masses[j]
|
|
434
|
+
self.graph.add_edge(node_i, node_j, weight=weight)
|
|
435
|
+
|
|
436
|
+
def set_state(
|
|
437
|
+
self,
|
|
438
|
+
positions: NDArray[np.floating],
|
|
439
|
+
velocities: NDArray[np.floating],
|
|
440
|
+
) -> None:
|
|
441
|
+
"""Set system state (positions and velocities).
|
|
442
|
+
|
|
443
|
+
Parameters
|
|
444
|
+
----------
|
|
445
|
+
positions : ndarray, shape (N, 3)
|
|
446
|
+
Positions of N bodies
|
|
447
|
+
velocities : ndarray, shape (N, 3)
|
|
448
|
+
Velocities of N bodies
|
|
449
|
+
|
|
450
|
+
Raises
|
|
451
|
+
------
|
|
452
|
+
ValueError
|
|
453
|
+
If shapes don't match (N, 3)
|
|
454
|
+
"""
|
|
455
|
+
positions = np.asarray(positions, dtype=float)
|
|
456
|
+
velocities = np.asarray(velocities, dtype=float)
|
|
457
|
+
|
|
458
|
+
expected_shape = (self.n_bodies, 3)
|
|
459
|
+
if positions.shape != expected_shape:
|
|
460
|
+
raise ValueError(f"positions shape {positions.shape} != {expected_shape}")
|
|
461
|
+
if velocities.shape != expected_shape:
|
|
462
|
+
raise ValueError(f"velocities shape {velocities.shape} != {expected_shape}")
|
|
463
|
+
|
|
464
|
+
self.positions = positions.copy()
|
|
465
|
+
self.velocities = velocities.copy()
|
|
466
|
+
|
|
467
|
+
# Update EPI in graph nodes
|
|
468
|
+
# EPI encodes state as dictionary with position/velocity
|
|
469
|
+
for i in range(self.n_bodies):
|
|
470
|
+
node_id = f"body_{i}"
|
|
471
|
+
# Store as structured EPI
|
|
472
|
+
epi_state = {
|
|
473
|
+
"position": self.positions[i].copy(),
|
|
474
|
+
"velocity": self.velocities[i].copy(),
|
|
475
|
+
}
|
|
476
|
+
self.graph.nodes[node_id]["epi"] = epi_state
|
|
477
|
+
|
|
478
|
+
def get_state(self) -> Tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
479
|
+
"""Get current state (positions and velocities).
|
|
480
|
+
|
|
481
|
+
Returns
|
|
482
|
+
-------
|
|
483
|
+
positions : ndarray, shape (N, 3)
|
|
484
|
+
Current positions
|
|
485
|
+
velocities : ndarray, shape (N, 3)
|
|
486
|
+
Current velocities
|
|
487
|
+
"""
|
|
488
|
+
return self.positions.copy(), self.velocities.copy()
|
|
489
|
+
|
|
490
|
+
def compute_energy(self) -> Tuple[float, float, float]:
|
|
491
|
+
"""Compute system energy (kinetic + potential).
|
|
492
|
+
|
|
493
|
+
Returns
|
|
494
|
+
-------
|
|
495
|
+
kinetic : float
|
|
496
|
+
Total kinetic energy T = Σ (1/2) m_i v_i²
|
|
497
|
+
potential : float
|
|
498
|
+
Total potential energy U (negative for bound systems)
|
|
499
|
+
total : float
|
|
500
|
+
Total energy H = T + U
|
|
501
|
+
|
|
502
|
+
Notes
|
|
503
|
+
-----
|
|
504
|
+
Energy conservation is a fundamental check of integrator accuracy.
|
|
505
|
+
For Hamiltonian systems, H should be constant over time.
|
|
506
|
+
"""
|
|
507
|
+
# Kinetic energy: T = Σ (1/2) m_i v_i²
|
|
508
|
+
v_squared = np.sum(self.velocities**2, axis=1)
|
|
509
|
+
kinetic = 0.5 * np.sum(self.masses * v_squared)
|
|
510
|
+
|
|
511
|
+
# Potential energy: U = -Σ_{i<j} G m_i m_j / r_ij
|
|
512
|
+
potential = gravitational_potential(
|
|
513
|
+
self.positions, self.masses, self.G, self.softening
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
total = kinetic + potential
|
|
517
|
+
|
|
518
|
+
return kinetic, potential, total
|
|
519
|
+
|
|
520
|
+
def compute_momentum(self) -> NDArray[np.floating]:
|
|
521
|
+
"""Compute total linear momentum.
|
|
522
|
+
|
|
523
|
+
Returns
|
|
524
|
+
-------
|
|
525
|
+
momentum : ndarray, shape (3,)
|
|
526
|
+
Total momentum P = Σ m_i v_i
|
|
527
|
+
|
|
528
|
+
Notes
|
|
529
|
+
-----
|
|
530
|
+
For isolated systems, momentum should be conserved (constant).
|
|
531
|
+
"""
|
|
532
|
+
momentum = np.sum(self.masses[:, np.newaxis] * self.velocities, axis=0)
|
|
533
|
+
return momentum
|
|
534
|
+
|
|
535
|
+
def compute_angular_momentum(self) -> NDArray[np.floating]:
|
|
536
|
+
"""Compute total angular momentum about origin.
|
|
537
|
+
|
|
538
|
+
Returns
|
|
539
|
+
-------
|
|
540
|
+
angular_momentum : ndarray, shape (3,)
|
|
541
|
+
Total angular momentum L = Σ r_i × m_i v_i
|
|
542
|
+
|
|
543
|
+
Notes
|
|
544
|
+
-----
|
|
545
|
+
For central force systems, angular momentum is conserved.
|
|
546
|
+
"""
|
|
547
|
+
L = np.zeros(3)
|
|
548
|
+
for i in range(self.n_bodies):
|
|
549
|
+
L += self.masses[i] * np.cross(self.positions[i], self.velocities[i])
|
|
550
|
+
return L
|
|
551
|
+
|
|
552
|
+
def step(self, dt: float) -> None:
|
|
553
|
+
"""Advance system by one time step using velocity Verlet.
|
|
554
|
+
|
|
555
|
+
The velocity Verlet integrator is symplectic (preserves phase space
|
|
556
|
+
volume) and provides excellent long-term energy conservation.
|
|
557
|
+
|
|
558
|
+
Algorithm:
|
|
559
|
+
1. r(t+dt) = r(t) + v(t)*dt + (1/2)*a(t)*dt²
|
|
560
|
+
2. a(t+dt) = compute acceleration at new positions
|
|
561
|
+
3. v(t+dt) = v(t) + (1/2)*(a(t) + a(t+dt))*dt
|
|
562
|
+
|
|
563
|
+
Parameters
|
|
564
|
+
----------
|
|
565
|
+
dt : float
|
|
566
|
+
Time step (in structural time units)
|
|
567
|
+
|
|
568
|
+
Notes
|
|
569
|
+
-----
|
|
570
|
+
This integrator is equivalent to applying the nodal equation:
|
|
571
|
+
∂EPI/∂t = νf · ΔNFR with νf = 1/m and ΔNFR = acceleration.
|
|
572
|
+
"""
|
|
573
|
+
# Compute acceleration at current time: a(t) = ΔNFR
|
|
574
|
+
accel_t = compute_gravitational_dnfr(
|
|
575
|
+
self.positions, self.masses, self.G, self.softening
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# Update positions: r(t+dt) = r(t) + v(t)*dt + (1/2)*a(t)*dt²
|
|
579
|
+
self.positions += self.velocities * dt + 0.5 * accel_t * dt**2
|
|
580
|
+
|
|
581
|
+
# Compute acceleration at new time: a(t+dt)
|
|
582
|
+
accel_t_plus_dt = compute_gravitational_dnfr(
|
|
583
|
+
self.positions, self.masses, self.G, self.softening
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# Update velocities: v(t+dt) = v(t) + (1/2)*(a(t) + a(t+dt))*dt
|
|
587
|
+
self.velocities += 0.5 * (accel_t + accel_t_plus_dt) * dt
|
|
588
|
+
|
|
589
|
+
# Update structural time
|
|
590
|
+
self.time += dt
|
|
591
|
+
|
|
592
|
+
# Update graph representation
|
|
593
|
+
for i in range(self.n_bodies):
|
|
594
|
+
node_id = f"body_{i}"
|
|
595
|
+
epi_state = {
|
|
596
|
+
"position": self.positions[i].copy(),
|
|
597
|
+
"velocity": self.velocities[i].copy(),
|
|
598
|
+
}
|
|
599
|
+
self.graph.nodes[node_id]["epi"] = epi_state
|
|
600
|
+
|
|
601
|
+
def evolve(
|
|
602
|
+
self,
|
|
603
|
+
t_final: float,
|
|
604
|
+
dt: float,
|
|
605
|
+
store_interval: int = 1,
|
|
606
|
+
) -> Dict[str, Any]:
|
|
607
|
+
"""Evolve system from current time to t_final.
|
|
608
|
+
|
|
609
|
+
Parameters
|
|
610
|
+
----------
|
|
611
|
+
t_final : float
|
|
612
|
+
Final structural time
|
|
613
|
+
dt : float
|
|
614
|
+
Time step for integration
|
|
615
|
+
store_interval : int, default=1
|
|
616
|
+
Store state every N steps (for memory efficiency)
|
|
617
|
+
|
|
618
|
+
Returns
|
|
619
|
+
-------
|
|
620
|
+
history : dict
|
|
621
|
+
Dictionary containing:
|
|
622
|
+
- 'time': array of time points
|
|
623
|
+
- 'positions': array of positions (n_steps, N, 3)
|
|
624
|
+
- 'velocities': array of velocities (n_steps, N, 3)
|
|
625
|
+
- 'energy': array of total energies
|
|
626
|
+
- 'kinetic': array of kinetic energies
|
|
627
|
+
- 'potential': array of potential energies
|
|
628
|
+
- 'momentum': array of momentum vectors (n_steps, 3)
|
|
629
|
+
- 'angular_momentum': array of L vectors (n_steps, 3)
|
|
630
|
+
|
|
631
|
+
Notes
|
|
632
|
+
-----
|
|
633
|
+
The evolution implements the nodal equation iteratively.
|
|
634
|
+
Conservation laws are tracked for validation.
|
|
635
|
+
"""
|
|
636
|
+
n_steps = int((t_final - self.time) / dt)
|
|
637
|
+
|
|
638
|
+
if n_steps < 1:
|
|
639
|
+
raise ValueError(f"t_final {t_final} <= current time {self.time}")
|
|
640
|
+
|
|
641
|
+
# Pre-allocate storage
|
|
642
|
+
n_stored = (n_steps // store_interval) + 1
|
|
643
|
+
times = np.zeros(n_stored)
|
|
644
|
+
positions_hist = np.zeros((n_stored, self.n_bodies, 3))
|
|
645
|
+
velocities_hist = np.zeros((n_stored, self.n_bodies, 3))
|
|
646
|
+
energies = np.zeros(n_stored)
|
|
647
|
+
kinetic_energies = np.zeros(n_stored)
|
|
648
|
+
potential_energies = np.zeros(n_stored)
|
|
649
|
+
momenta = np.zeros((n_stored, 3))
|
|
650
|
+
angular_momenta = np.zeros((n_stored, 3))
|
|
651
|
+
|
|
652
|
+
# Store initial state
|
|
653
|
+
store_idx = 0
|
|
654
|
+
times[store_idx] = self.time
|
|
655
|
+
positions_hist[store_idx] = self.positions.copy()
|
|
656
|
+
velocities_hist[store_idx] = self.velocities.copy()
|
|
657
|
+
K, U, E = self.compute_energy()
|
|
658
|
+
kinetic_energies[store_idx] = K
|
|
659
|
+
potential_energies[store_idx] = U
|
|
660
|
+
energies[store_idx] = E
|
|
661
|
+
momenta[store_idx] = self.compute_momentum()
|
|
662
|
+
angular_momenta[store_idx] = self.compute_angular_momentum()
|
|
663
|
+
store_idx += 1
|
|
664
|
+
|
|
665
|
+
# Evolution loop
|
|
666
|
+
for step in range(n_steps):
|
|
667
|
+
self.step(dt)
|
|
668
|
+
|
|
669
|
+
# Store state if needed
|
|
670
|
+
if (step + 1) % store_interval == 0 and store_idx < n_stored:
|
|
671
|
+
times[store_idx] = self.time
|
|
672
|
+
positions_hist[store_idx] = self.positions.copy()
|
|
673
|
+
velocities_hist[store_idx] = self.velocities.copy()
|
|
674
|
+
K, U, E = self.compute_energy()
|
|
675
|
+
kinetic_energies[store_idx] = K
|
|
676
|
+
potential_energies[store_idx] = U
|
|
677
|
+
energies[store_idx] = E
|
|
678
|
+
momenta[store_idx] = self.compute_momentum()
|
|
679
|
+
angular_momenta[store_idx] = self.compute_angular_momentum()
|
|
680
|
+
store_idx += 1
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
"time": times[:store_idx],
|
|
684
|
+
"positions": positions_hist[:store_idx],
|
|
685
|
+
"velocities": velocities_hist[:store_idx],
|
|
686
|
+
"energy": energies[:store_idx],
|
|
687
|
+
"kinetic": kinetic_energies[:store_idx],
|
|
688
|
+
"potential": potential_energies[:store_idx],
|
|
689
|
+
"momentum": momenta[:store_idx],
|
|
690
|
+
"angular_momentum": angular_momenta[:store_idx],
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
def plot_trajectories(
|
|
694
|
+
self,
|
|
695
|
+
history: Dict[str, Any],
|
|
696
|
+
ax: Optional[Any] = None,
|
|
697
|
+
show_energy: bool = True,
|
|
698
|
+
) -> Figure:
|
|
699
|
+
"""Plot trajectories and energy evolution.
|
|
700
|
+
|
|
701
|
+
Parameters
|
|
702
|
+
----------
|
|
703
|
+
history : dict
|
|
704
|
+
Result from evolve() method
|
|
705
|
+
ax : matplotlib axis, optional
|
|
706
|
+
Axis to plot on. If None, creates new figure.
|
|
707
|
+
show_energy : bool, default=True
|
|
708
|
+
If True, also plot energy conservation
|
|
709
|
+
|
|
710
|
+
Returns
|
|
711
|
+
-------
|
|
712
|
+
fig : matplotlib Figure
|
|
713
|
+
Figure object containing plots
|
|
714
|
+
|
|
715
|
+
Raises
|
|
716
|
+
------
|
|
717
|
+
ImportError
|
|
718
|
+
If matplotlib is not available
|
|
719
|
+
"""
|
|
720
|
+
try:
|
|
721
|
+
import matplotlib.pyplot as plt
|
|
722
|
+
from mpl_toolkits.mplot3d import Axes3D
|
|
723
|
+
except ImportError as exc:
|
|
724
|
+
raise ImportError(
|
|
725
|
+
"matplotlib is required for plotting. "
|
|
726
|
+
"Install with: pip install 'tnfr[viz-basic]'"
|
|
727
|
+
) from exc
|
|
728
|
+
|
|
729
|
+
if show_energy:
|
|
730
|
+
fig = plt.figure(figsize=(14, 6))
|
|
731
|
+
ax_3d = fig.add_subplot(121, projection="3d")
|
|
732
|
+
ax_energy = fig.add_subplot(122)
|
|
733
|
+
else:
|
|
734
|
+
fig = plt.figure(figsize=(10, 8))
|
|
735
|
+
ax_3d = fig.add_subplot(111, projection="3d")
|
|
736
|
+
|
|
737
|
+
# Plot 3D trajectories
|
|
738
|
+
positions = history["positions"]
|
|
739
|
+
colors = plt.cm.rainbow(np.linspace(0, 1, self.n_bodies))
|
|
740
|
+
|
|
741
|
+
for i in range(self.n_bodies):
|
|
742
|
+
traj = positions[:, i, :]
|
|
743
|
+
ax_3d.plot(
|
|
744
|
+
traj[:, 0],
|
|
745
|
+
traj[:, 1],
|
|
746
|
+
traj[:, 2],
|
|
747
|
+
color=colors[i],
|
|
748
|
+
label=f"Body {i+1} (m={self.masses[i]:.2f})",
|
|
749
|
+
alpha=0.7,
|
|
750
|
+
)
|
|
751
|
+
# Mark initial position
|
|
752
|
+
ax_3d.scatter(
|
|
753
|
+
traj[0, 0],
|
|
754
|
+
traj[0, 1],
|
|
755
|
+
traj[0, 2],
|
|
756
|
+
color=colors[i],
|
|
757
|
+
s=100,
|
|
758
|
+
marker="o",
|
|
759
|
+
)
|
|
760
|
+
# Mark final position
|
|
761
|
+
ax_3d.scatter(
|
|
762
|
+
traj[-1, 0],
|
|
763
|
+
traj[-1, 1],
|
|
764
|
+
traj[-1, 2],
|
|
765
|
+
color=colors[i],
|
|
766
|
+
s=50,
|
|
767
|
+
marker="x",
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
ax_3d.set_xlabel("X")
|
|
771
|
+
ax_3d.set_ylabel("Y")
|
|
772
|
+
ax_3d.set_zlabel("Z")
|
|
773
|
+
ax_3d.set_title("N-Body Trajectories (TNFR Framework)")
|
|
774
|
+
ax_3d.legend()
|
|
775
|
+
|
|
776
|
+
if show_energy:
|
|
777
|
+
# Plot energy conservation
|
|
778
|
+
time = history["time"]
|
|
779
|
+
E = history["energy"]
|
|
780
|
+
E0 = E[0]
|
|
781
|
+
|
|
782
|
+
ax_energy.plot(
|
|
783
|
+
time,
|
|
784
|
+
(E - E0) / abs(E0) * 100,
|
|
785
|
+
label="Relative energy error (%)",
|
|
786
|
+
color="red",
|
|
787
|
+
)
|
|
788
|
+
ax_energy.axhline(0, color="black", linestyle="--", alpha=0.3)
|
|
789
|
+
ax_energy.set_xlabel("Structural Time")
|
|
790
|
+
ax_energy.set_ylabel("ΔE/E₀ (%)")
|
|
791
|
+
ax_energy.set_title("Energy Conservation Check")
|
|
792
|
+
ax_energy.legend()
|
|
793
|
+
ax_energy.grid(True, alpha=0.3)
|
|
794
|
+
|
|
795
|
+
plt.tight_layout()
|
|
796
|
+
return fig
|