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.

Files changed (365) hide show
  1. tnfr/__init__.py +334 -50
  2. tnfr/__init__.pyi +33 -0
  3. tnfr/_compat.py +10 -0
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +49 -0
  6. tnfr/_version.pyi +7 -0
  7. tnfr/alias.py +214 -37
  8. tnfr/alias.pyi +108 -0
  9. tnfr/backends/__init__.py +354 -0
  10. tnfr/backends/jax_backend.py +173 -0
  11. tnfr/backends/numpy_backend.py +238 -0
  12. tnfr/backends/optimized_numpy.py +420 -0
  13. tnfr/backends/torch_backend.py +408 -0
  14. tnfr/cache.py +149 -556
  15. tnfr/cache.pyi +13 -0
  16. tnfr/cli/__init__.py +51 -16
  17. tnfr/cli/__init__.pyi +26 -0
  18. tnfr/cli/arguments.py +344 -32
  19. tnfr/cli/arguments.pyi +29 -0
  20. tnfr/cli/execution.py +676 -50
  21. tnfr/cli/execution.pyi +70 -0
  22. tnfr/cli/interactive_validator.py +614 -0
  23. tnfr/cli/utils.py +18 -3
  24. tnfr/cli/utils.pyi +7 -0
  25. tnfr/cli/validate.py +236 -0
  26. tnfr/compat/__init__.py +85 -0
  27. tnfr/compat/dataclass.py +136 -0
  28. tnfr/compat/jsonschema_stub.py +61 -0
  29. tnfr/compat/matplotlib_stub.py +73 -0
  30. tnfr/compat/numpy_stub.py +155 -0
  31. tnfr/config/__init__.py +224 -0
  32. tnfr/config/__init__.pyi +10 -0
  33. tnfr/{constants_glyphs.py → config/constants.py} +26 -20
  34. tnfr/config/constants.pyi +12 -0
  35. tnfr/config/defaults.py +54 -0
  36. tnfr/{constants/core.py → config/defaults_core.py} +59 -6
  37. tnfr/config/defaults_init.py +33 -0
  38. tnfr/config/defaults_metric.py +104 -0
  39. tnfr/config/feature_flags.py +81 -0
  40. tnfr/config/feature_flags.pyi +16 -0
  41. tnfr/config/glyph_constants.py +31 -0
  42. tnfr/config/init.py +77 -0
  43. tnfr/config/init.pyi +8 -0
  44. tnfr/config/operator_names.py +254 -0
  45. tnfr/config/operator_names.pyi +36 -0
  46. tnfr/config/physics_derivation.py +354 -0
  47. tnfr/config/presets.py +83 -0
  48. tnfr/config/presets.pyi +7 -0
  49. tnfr/config/security.py +927 -0
  50. tnfr/config/thresholds.py +114 -0
  51. tnfr/config/tnfr_config.py +498 -0
  52. tnfr/constants/__init__.py +51 -133
  53. tnfr/constants/__init__.pyi +92 -0
  54. tnfr/constants/aliases.py +33 -0
  55. tnfr/constants/aliases.pyi +27 -0
  56. tnfr/constants/init.py +3 -1
  57. tnfr/constants/init.pyi +12 -0
  58. tnfr/constants/metric.py +9 -15
  59. tnfr/constants/metric.pyi +19 -0
  60. tnfr/core/__init__.py +33 -0
  61. tnfr/core/container.py +226 -0
  62. tnfr/core/default_implementations.py +329 -0
  63. tnfr/core/interfaces.py +279 -0
  64. tnfr/dynamics/__init__.py +213 -633
  65. tnfr/dynamics/__init__.pyi +83 -0
  66. tnfr/dynamics/adaptation.py +267 -0
  67. tnfr/dynamics/adaptation.pyi +7 -0
  68. tnfr/dynamics/adaptive_sequences.py +189 -0
  69. tnfr/dynamics/adaptive_sequences.pyi +14 -0
  70. tnfr/dynamics/aliases.py +23 -0
  71. tnfr/dynamics/aliases.pyi +19 -0
  72. tnfr/dynamics/bifurcation.py +232 -0
  73. tnfr/dynamics/canonical.py +229 -0
  74. tnfr/dynamics/canonical.pyi +48 -0
  75. tnfr/dynamics/coordination.py +385 -0
  76. tnfr/dynamics/coordination.pyi +25 -0
  77. tnfr/dynamics/dnfr.py +2699 -398
  78. tnfr/dynamics/dnfr.pyi +26 -0
  79. tnfr/dynamics/dynamic_limits.py +225 -0
  80. tnfr/dynamics/feedback.py +252 -0
  81. tnfr/dynamics/feedback.pyi +24 -0
  82. tnfr/dynamics/fused_dnfr.py +454 -0
  83. tnfr/dynamics/homeostasis.py +157 -0
  84. tnfr/dynamics/homeostasis.pyi +14 -0
  85. tnfr/dynamics/integrators.py +496 -102
  86. tnfr/dynamics/integrators.pyi +36 -0
  87. tnfr/dynamics/learning.py +310 -0
  88. tnfr/dynamics/learning.pyi +33 -0
  89. tnfr/dynamics/metabolism.py +254 -0
  90. tnfr/dynamics/nbody.py +796 -0
  91. tnfr/dynamics/nbody_tnfr.py +783 -0
  92. tnfr/dynamics/propagation.py +326 -0
  93. tnfr/dynamics/runtime.py +908 -0
  94. tnfr/dynamics/runtime.pyi +77 -0
  95. tnfr/dynamics/sampling.py +10 -5
  96. tnfr/dynamics/sampling.pyi +7 -0
  97. tnfr/dynamics/selectors.py +711 -0
  98. tnfr/dynamics/selectors.pyi +85 -0
  99. tnfr/dynamics/structural_clip.py +207 -0
  100. tnfr/errors/__init__.py +37 -0
  101. tnfr/errors/contextual.py +492 -0
  102. tnfr/execution.py +77 -55
  103. tnfr/execution.pyi +45 -0
  104. tnfr/extensions/__init__.py +205 -0
  105. tnfr/extensions/__init__.pyi +18 -0
  106. tnfr/extensions/base.py +173 -0
  107. tnfr/extensions/base.pyi +35 -0
  108. tnfr/extensions/business/__init__.py +71 -0
  109. tnfr/extensions/business/__init__.pyi +11 -0
  110. tnfr/extensions/business/cookbook.py +88 -0
  111. tnfr/extensions/business/cookbook.pyi +8 -0
  112. tnfr/extensions/business/health_analyzers.py +202 -0
  113. tnfr/extensions/business/health_analyzers.pyi +9 -0
  114. tnfr/extensions/business/patterns.py +183 -0
  115. tnfr/extensions/business/patterns.pyi +8 -0
  116. tnfr/extensions/medical/__init__.py +73 -0
  117. tnfr/extensions/medical/__init__.pyi +11 -0
  118. tnfr/extensions/medical/cookbook.py +88 -0
  119. tnfr/extensions/medical/cookbook.pyi +8 -0
  120. tnfr/extensions/medical/health_analyzers.py +181 -0
  121. tnfr/extensions/medical/health_analyzers.pyi +9 -0
  122. tnfr/extensions/medical/patterns.py +163 -0
  123. tnfr/extensions/medical/patterns.pyi +8 -0
  124. tnfr/flatten.py +29 -50
  125. tnfr/flatten.pyi +21 -0
  126. tnfr/gamma.py +66 -53
  127. tnfr/gamma.pyi +36 -0
  128. tnfr/glyph_history.py +144 -57
  129. tnfr/glyph_history.pyi +35 -0
  130. tnfr/glyph_runtime.py +19 -0
  131. tnfr/glyph_runtime.pyi +8 -0
  132. tnfr/immutable.py +70 -30
  133. tnfr/immutable.pyi +36 -0
  134. tnfr/initialization.py +22 -16
  135. tnfr/initialization.pyi +65 -0
  136. tnfr/io.py +5 -241
  137. tnfr/io.pyi +13 -0
  138. tnfr/locking.pyi +7 -0
  139. tnfr/mathematics/__init__.py +79 -0
  140. tnfr/mathematics/backend.py +453 -0
  141. tnfr/mathematics/backend.pyi +99 -0
  142. tnfr/mathematics/dynamics.py +408 -0
  143. tnfr/mathematics/dynamics.pyi +90 -0
  144. tnfr/mathematics/epi.py +391 -0
  145. tnfr/mathematics/epi.pyi +65 -0
  146. tnfr/mathematics/generators.py +242 -0
  147. tnfr/mathematics/generators.pyi +29 -0
  148. tnfr/mathematics/metrics.py +119 -0
  149. tnfr/mathematics/metrics.pyi +16 -0
  150. tnfr/mathematics/operators.py +239 -0
  151. tnfr/mathematics/operators.pyi +59 -0
  152. tnfr/mathematics/operators_factory.py +124 -0
  153. tnfr/mathematics/operators_factory.pyi +11 -0
  154. tnfr/mathematics/projection.py +87 -0
  155. tnfr/mathematics/projection.pyi +33 -0
  156. tnfr/mathematics/runtime.py +182 -0
  157. tnfr/mathematics/runtime.pyi +64 -0
  158. tnfr/mathematics/spaces.py +256 -0
  159. tnfr/mathematics/spaces.pyi +83 -0
  160. tnfr/mathematics/transforms.py +305 -0
  161. tnfr/mathematics/transforms.pyi +62 -0
  162. tnfr/metrics/__init__.py +47 -9
  163. tnfr/metrics/__init__.pyi +20 -0
  164. tnfr/metrics/buffer_cache.py +163 -0
  165. tnfr/metrics/buffer_cache.pyi +24 -0
  166. tnfr/metrics/cache_utils.py +214 -0
  167. tnfr/metrics/coherence.py +1510 -330
  168. tnfr/metrics/coherence.pyi +129 -0
  169. tnfr/metrics/common.py +23 -16
  170. tnfr/metrics/common.pyi +35 -0
  171. tnfr/metrics/core.py +251 -36
  172. tnfr/metrics/core.pyi +13 -0
  173. tnfr/metrics/diagnosis.py +709 -110
  174. tnfr/metrics/diagnosis.pyi +86 -0
  175. tnfr/metrics/emergence.py +245 -0
  176. tnfr/metrics/export.py +60 -18
  177. tnfr/metrics/export.pyi +7 -0
  178. tnfr/metrics/glyph_timing.py +233 -43
  179. tnfr/metrics/glyph_timing.pyi +81 -0
  180. tnfr/metrics/learning_metrics.py +280 -0
  181. tnfr/metrics/learning_metrics.pyi +21 -0
  182. tnfr/metrics/phase_coherence.py +351 -0
  183. tnfr/metrics/phase_compatibility.py +349 -0
  184. tnfr/metrics/reporting.py +63 -28
  185. tnfr/metrics/reporting.pyi +25 -0
  186. tnfr/metrics/sense_index.py +1126 -43
  187. tnfr/metrics/sense_index.pyi +9 -0
  188. tnfr/metrics/trig.py +215 -23
  189. tnfr/metrics/trig.pyi +13 -0
  190. tnfr/metrics/trig_cache.py +148 -24
  191. tnfr/metrics/trig_cache.pyi +10 -0
  192. tnfr/multiscale/__init__.py +32 -0
  193. tnfr/multiscale/hierarchical.py +517 -0
  194. tnfr/node.py +646 -140
  195. tnfr/node.pyi +139 -0
  196. tnfr/observers.py +160 -45
  197. tnfr/observers.pyi +31 -0
  198. tnfr/ontosim.py +23 -19
  199. tnfr/ontosim.pyi +28 -0
  200. tnfr/operators/__init__.py +1358 -106
  201. tnfr/operators/__init__.pyi +31 -0
  202. tnfr/operators/algebra.py +277 -0
  203. tnfr/operators/canonical_patterns.py +420 -0
  204. tnfr/operators/cascade.py +267 -0
  205. tnfr/operators/cycle_detection.py +358 -0
  206. tnfr/operators/definitions.py +4108 -0
  207. tnfr/operators/definitions.pyi +78 -0
  208. tnfr/operators/grammar.py +1164 -0
  209. tnfr/operators/grammar.pyi +140 -0
  210. tnfr/operators/hamiltonian.py +710 -0
  211. tnfr/operators/health_analyzer.py +809 -0
  212. tnfr/operators/jitter.py +107 -38
  213. tnfr/operators/jitter.pyi +11 -0
  214. tnfr/operators/lifecycle.py +314 -0
  215. tnfr/operators/metabolism.py +618 -0
  216. tnfr/operators/metrics.py +2138 -0
  217. tnfr/operators/network_analysis/__init__.py +27 -0
  218. tnfr/operators/network_analysis/source_detection.py +186 -0
  219. tnfr/operators/nodal_equation.py +395 -0
  220. tnfr/operators/pattern_detection.py +660 -0
  221. tnfr/operators/patterns.py +669 -0
  222. tnfr/operators/postconditions/__init__.py +38 -0
  223. tnfr/operators/postconditions/mutation.py +236 -0
  224. tnfr/operators/preconditions/__init__.py +1226 -0
  225. tnfr/operators/preconditions/coherence.py +305 -0
  226. tnfr/operators/preconditions/dissonance.py +236 -0
  227. tnfr/operators/preconditions/emission.py +128 -0
  228. tnfr/operators/preconditions/mutation.py +580 -0
  229. tnfr/operators/preconditions/reception.py +125 -0
  230. tnfr/operators/preconditions/resonance.py +364 -0
  231. tnfr/operators/registry.py +74 -0
  232. tnfr/operators/registry.pyi +9 -0
  233. tnfr/operators/remesh.py +1415 -91
  234. tnfr/operators/remesh.pyi +26 -0
  235. tnfr/operators/structural_units.py +268 -0
  236. tnfr/operators/unified_grammar.py +105 -0
  237. tnfr/parallel/__init__.py +54 -0
  238. tnfr/parallel/auto_scaler.py +234 -0
  239. tnfr/parallel/distributed.py +384 -0
  240. tnfr/parallel/engine.py +238 -0
  241. tnfr/parallel/gpu_engine.py +420 -0
  242. tnfr/parallel/monitoring.py +248 -0
  243. tnfr/parallel/partitioner.py +459 -0
  244. tnfr/py.typed +0 -0
  245. tnfr/recipes/__init__.py +22 -0
  246. tnfr/recipes/cookbook.py +743 -0
  247. tnfr/rng.py +75 -151
  248. tnfr/rng.pyi +26 -0
  249. tnfr/schemas/__init__.py +8 -0
  250. tnfr/schemas/grammar.json +94 -0
  251. tnfr/sdk/__init__.py +107 -0
  252. tnfr/sdk/__init__.pyi +19 -0
  253. tnfr/sdk/adaptive_system.py +173 -0
  254. tnfr/sdk/adaptive_system.pyi +21 -0
  255. tnfr/sdk/builders.py +370 -0
  256. tnfr/sdk/builders.pyi +51 -0
  257. tnfr/sdk/fluent.py +1121 -0
  258. tnfr/sdk/fluent.pyi +74 -0
  259. tnfr/sdk/templates.py +342 -0
  260. tnfr/sdk/templates.pyi +41 -0
  261. tnfr/sdk/utils.py +341 -0
  262. tnfr/secure_config.py +46 -0
  263. tnfr/security/__init__.py +70 -0
  264. tnfr/security/database.py +514 -0
  265. tnfr/security/subprocess.py +503 -0
  266. tnfr/security/validation.py +290 -0
  267. tnfr/selector.py +59 -22
  268. tnfr/selector.pyi +19 -0
  269. tnfr/sense.py +92 -67
  270. tnfr/sense.pyi +23 -0
  271. tnfr/services/__init__.py +17 -0
  272. tnfr/services/orchestrator.py +325 -0
  273. tnfr/sparse/__init__.py +39 -0
  274. tnfr/sparse/representations.py +492 -0
  275. tnfr/structural.py +639 -263
  276. tnfr/structural.pyi +83 -0
  277. tnfr/telemetry/__init__.py +35 -0
  278. tnfr/telemetry/cache_metrics.py +226 -0
  279. tnfr/telemetry/cache_metrics.pyi +64 -0
  280. tnfr/telemetry/nu_f.py +422 -0
  281. tnfr/telemetry/nu_f.pyi +108 -0
  282. tnfr/telemetry/verbosity.py +36 -0
  283. tnfr/telemetry/verbosity.pyi +15 -0
  284. tnfr/tokens.py +2 -4
  285. tnfr/tokens.pyi +36 -0
  286. tnfr/tools/__init__.py +20 -0
  287. tnfr/tools/domain_templates.py +478 -0
  288. tnfr/tools/sequence_generator.py +846 -0
  289. tnfr/topology/__init__.py +13 -0
  290. tnfr/topology/asymmetry.py +151 -0
  291. tnfr/trace.py +300 -126
  292. tnfr/trace.pyi +42 -0
  293. tnfr/tutorials/__init__.py +38 -0
  294. tnfr/tutorials/autonomous_evolution.py +285 -0
  295. tnfr/tutorials/interactive.py +1576 -0
  296. tnfr/tutorials/structural_metabolism.py +238 -0
  297. tnfr/types.py +743 -12
  298. tnfr/types.pyi +357 -0
  299. tnfr/units.py +68 -0
  300. tnfr/units.pyi +13 -0
  301. tnfr/utils/__init__.py +282 -0
  302. tnfr/utils/__init__.pyi +215 -0
  303. tnfr/utils/cache.py +4223 -0
  304. tnfr/utils/cache.pyi +470 -0
  305. tnfr/{callback_utils.py → utils/callbacks.py} +26 -39
  306. tnfr/utils/callbacks.pyi +49 -0
  307. tnfr/utils/chunks.py +108 -0
  308. tnfr/utils/chunks.pyi +22 -0
  309. tnfr/utils/data.py +428 -0
  310. tnfr/utils/data.pyi +74 -0
  311. tnfr/utils/graph.py +85 -0
  312. tnfr/utils/graph.pyi +10 -0
  313. tnfr/utils/init.py +821 -0
  314. tnfr/utils/init.pyi +80 -0
  315. tnfr/utils/io.py +559 -0
  316. tnfr/utils/io.pyi +66 -0
  317. tnfr/{helpers → utils}/numeric.py +51 -24
  318. tnfr/utils/numeric.pyi +21 -0
  319. tnfr/validation/__init__.py +257 -0
  320. tnfr/validation/__init__.pyi +85 -0
  321. tnfr/validation/compatibility.py +460 -0
  322. tnfr/validation/compatibility.pyi +6 -0
  323. tnfr/validation/config.py +73 -0
  324. tnfr/validation/graph.py +139 -0
  325. tnfr/validation/graph.pyi +18 -0
  326. tnfr/validation/input_validation.py +755 -0
  327. tnfr/validation/invariants.py +712 -0
  328. tnfr/validation/rules.py +253 -0
  329. tnfr/validation/rules.pyi +44 -0
  330. tnfr/validation/runtime.py +279 -0
  331. tnfr/validation/runtime.pyi +28 -0
  332. tnfr/validation/sequence_validator.py +162 -0
  333. tnfr/validation/soft_filters.py +170 -0
  334. tnfr/validation/soft_filters.pyi +32 -0
  335. tnfr/validation/spectral.py +164 -0
  336. tnfr/validation/spectral.pyi +42 -0
  337. tnfr/validation/validator.py +1266 -0
  338. tnfr/validation/window.py +39 -0
  339. tnfr/validation/window.pyi +1 -0
  340. tnfr/visualization/__init__.py +98 -0
  341. tnfr/visualization/cascade_viz.py +256 -0
  342. tnfr/visualization/hierarchy.py +284 -0
  343. tnfr/visualization/sequence_plotter.py +784 -0
  344. tnfr/viz/__init__.py +60 -0
  345. tnfr/viz/matplotlib.py +278 -0
  346. tnfr/viz/matplotlib.pyi +35 -0
  347. tnfr-8.5.0.dist-info/METADATA +573 -0
  348. tnfr-8.5.0.dist-info/RECORD +353 -0
  349. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/entry_points.txt +1 -0
  350. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/licenses/LICENSE.md +1 -1
  351. tnfr/collections_utils.py +0 -300
  352. tnfr/config.py +0 -32
  353. tnfr/grammar.py +0 -344
  354. tnfr/graph_utils.py +0 -84
  355. tnfr/helpers/__init__.py +0 -71
  356. tnfr/import_utils.py +0 -228
  357. tnfr/json_utils.py +0 -162
  358. tnfr/logging_utils.py +0 -116
  359. tnfr/presets.py +0 -60
  360. tnfr/validators.py +0 -84
  361. tnfr/value_utils.py +0 -59
  362. tnfr-4.5.2.dist-info/METADATA +0 -379
  363. tnfr-4.5.2.dist-info/RECORD +0 -67
  364. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
  365. {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