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
@@ -0,0 +1,783 @@
1
+ """N-body dynamics using pure TNFR physics (no external potentials).
2
+
3
+ This module implements N-body dynamics derived STRICTLY from TNFR structural
4
+ framework, without assuming any classical potentials (Newtonian, Coulomb, etc.).
5
+
6
+ Key Differences from Classical N-Body
7
+ --------------------------------------
8
+
9
+ **Classical Approach** (nbody.py):
10
+ - Assumes gravitational potential: U = -G*m*m/r
11
+ - Force computed as: F = -∇U
12
+ - ΔNFR = F/m (external assumption)
13
+
14
+ **TNFR Approach** (this module):
15
+ - NO assumed potential
16
+ - Coherence potential emerges from network structure
17
+ - ΔNFR computed from Hamiltonian commutator: ΔNFR = i[H_int, ·]/ℏ_str
18
+ - Attraction/repulsion emerges from phase synchronization and coupling
19
+
20
+ Theoretical Foundation
21
+ ----------------------
22
+
23
+ The nodal equation:
24
+ ∂EPI/∂t = νf · ΔNFR(t)
25
+
26
+ Where ΔNFR emerges from the internal Hamiltonian:
27
+ H_int = H_coh + H_freq + H_coupling
28
+
29
+ Components:
30
+ 1. **H_coh**: Coherence potential from structural similarity (phase, EPI, νf, Si)
31
+ 2. **H_freq**: Diagonal operator encoding each node's νf
32
+ 3. **H_coupling**: Network topology-induced interactions
33
+
34
+ The crucial insight: Attractive forces emerge naturally from maximizing
35
+ coherence between nodes, NOT from assuming gravity.
36
+
37
+ Phase-Dependent Interaction
38
+ ----------------------------
39
+
40
+ Unlike classical gravity (always attractive), TNFR coupling depends on phase:
41
+ - |φᵢ - φⱼ| small → strong coherence → attraction
42
+ - |φᵢ - φⱼ| ≈ π → destructive interference → repulsion
43
+
44
+ This captures wave-like behavior absent in classical mechanics.
45
+
46
+ Emergence of Classical Limit
47
+ -----------------------------
48
+
49
+ In the low-dissonance limit (ε → 0) with:
50
+ - Nearly synchronized phases: |φᵢ - φⱼ| → 0
51
+ - Strong coupling: all nodes connected
52
+ - High coherence: C(t) ≈ 1
53
+
54
+ The TNFR dynamics reproduces classical-like behavior, but from first principles.
55
+
56
+ References
57
+ ----------
58
+ - TNFR.pdf § 2.3: Nodal equation
59
+ - src/tnfr/operators/hamiltonian.py: H_int construction
60
+ - docs/source/theory/07_emergence_classical_mechanics.md: Classical limit
61
+ - AGENTS.md § Canonical Invariants: TNFR physics principles
62
+
63
+ Examples
64
+ --------
65
+ Two-body orbital resonance (no gravitational assumption):
66
+
67
+ >>> from tnfr.dynamics.nbody_tnfr import TNFRNBodySystem
68
+ >>> import numpy as np
69
+ >>>
70
+ >>> # Create 2-body system
71
+ >>> system = TNFRNBodySystem(
72
+ ... n_bodies=2,
73
+ ... masses=[1.0, 0.1], # Actually νf^-1
74
+ ... positions=np.array([[0, 0, 0], [1, 0, 0]]),
75
+ ... velocities=np.array([[0, 0, 0], [0, 1, 0]]),
76
+ ... phases=np.array([0.0, 0.0]) # Synchronized initially
77
+ ... )
78
+ >>>
79
+ >>> # Evolve via pure TNFR dynamics
80
+ >>> history = system.evolve(t_final=10.0, dt=0.01)
81
+ >>>
82
+ >>> # Check that orbital behavior emerges without assuming gravity
83
+ >>> print(f"Energy conservation: {history['energy_drift']:.2e}")
84
+ """
85
+
86
+ from __future__ import annotations
87
+
88
+ from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Optional
89
+
90
+ import numpy as np
91
+ from numpy.typing import NDArray
92
+
93
+ from ..structural import create_nfr
94
+ from ..types import TNFRGraph
95
+ from ..operators.hamiltonian import InternalHamiltonian
96
+ from ..alias import get_attr
97
+
98
+ if TYPE_CHECKING:
99
+ from matplotlib.figure import Figure
100
+
101
+ __all__ = (
102
+ "TNFRNBodySystem",
103
+ "compute_tnfr_coherence_potential",
104
+ "compute_tnfr_delta_nfr",
105
+ )
106
+
107
+
108
+ def compute_tnfr_coherence_potential(
109
+ G: TNFRGraph,
110
+ positions: NDArray[np.floating],
111
+ hbar_str: float = 1.0,
112
+ ) -> float:
113
+ """Compute coherence potential from TNFR network structure.
114
+
115
+ This is the pure TNFR potential - NO classical assumptions.
116
+
117
+ The potential emerges from:
118
+ - Structural similarity (coherence matrix)
119
+ - Network coupling topology
120
+ - Phase synchronization
121
+
122
+ NOT from Newtonian gravity or any other classical force law.
123
+
124
+ Parameters
125
+ ----------
126
+ G : TNFRGraph
127
+ Network graph with nodes containing TNFR attributes
128
+ positions : ndarray, shape (N, 3)
129
+ Current positions (affect phase evolution, not potential directly)
130
+ hbar_str : float, default=1.0
131
+ Structural Planck constant
132
+
133
+ Returns
134
+ -------
135
+ U : float
136
+ Coherence potential energy (lower = more stable)
137
+
138
+ Notes
139
+ -----
140
+ In TNFR, the potential U encodes structural stability landscape.
141
+ Nodes evolve toward configurations that maximize coherence (minimize U).
142
+ """
143
+ # Build internal Hamiltonian
144
+ ham = InternalHamiltonian(G, hbar_str=hbar_str)
145
+
146
+ # Potential is encoded in ground state energy
147
+ eigenvalues, _ = ham.get_spectrum()
148
+
149
+ # Total potential: sum of eigenvalues (trace of H_int)
150
+ # For energy conservation, we use ground state as reference
151
+ U = float(eigenvalues[0]) # Ground state energy
152
+
153
+ return U
154
+
155
+
156
+ def compute_tnfr_delta_nfr(
157
+ G: TNFRGraph,
158
+ node_ids: List[str],
159
+ hbar_str: float = 1.0,
160
+ ) -> NDArray[np.floating]:
161
+ """Compute ΔNFR from Hamiltonian commutator (pure TNFR).
162
+
163
+ This is the correct TNFR computation of ΔNFR:
164
+ ΔNFR = i[H_int, ·]/ℏ_str
165
+
166
+ NOT from classical forces: F = -∇U (external assumption).
167
+
168
+ Parameters
169
+ ----------
170
+ G : TNFRGraph
171
+ Network graph with TNFR attributes
172
+ node_ids : list of str
173
+ Node identifiers in order
174
+ hbar_str : float, default=1.0
175
+ Structural Planck constant
176
+
177
+ Returns
178
+ -------
179
+ dnfr : ndarray, shape (N,)
180
+ ΔNFR values for each node (structural reorganization pressure)
181
+
182
+ Notes
183
+ -----
184
+ The ΔNFR values represent the "reorganization pressure" driving
185
+ structural evolution via the nodal equation: ∂EPI/∂t = νf · ΔNFR
186
+ """
187
+ # Build Hamiltonian
188
+ ham = InternalHamiltonian(G, hbar_str=hbar_str)
189
+
190
+ # Compute ΔNFR for each node
191
+ dnfr = np.zeros(len(node_ids))
192
+ for i, node_id in enumerate(node_ids):
193
+ dnfr[i] = ham.compute_node_delta_nfr(node_id)
194
+
195
+ return dnfr
196
+
197
+
198
+ class TNFRNBodySystem:
199
+ """N-body system using pure TNFR physics (no classical assumptions).
200
+
201
+ This implementation computes dynamics from TNFR structural coherence,
202
+ without assuming any classical potentials (gravity, Coulomb, etc.).
203
+
204
+ Attributes
205
+ ----------
206
+ n_bodies : int
207
+ Number of bodies
208
+ masses : ndarray, shape (N,)
209
+ Masses (m = 1/νf, structural inertia)
210
+ positions : ndarray, shape (N, 3)
211
+ Current positions
212
+ velocities : ndarray, shape (N, 3)
213
+ Current velocities
214
+ phases : ndarray, shape (N,)
215
+ Current phases (θ ∈ [0, 2π])
216
+ time : float
217
+ Current structural time
218
+ graph : TNFRGraph
219
+ TNFR network representation
220
+ hbar_str : float
221
+ Structural Planck constant
222
+
223
+ Notes
224
+ -----
225
+ Dynamics follow from nodal equation: ∂EPI/∂t = νf · ΔNFR
226
+ where ΔNFR is computed from Hamiltonian commutator.
227
+
228
+ Attraction/repulsion emerges from phase synchronization,
229
+ NOT from assumed gravitational potential.
230
+ """
231
+
232
+ def __init__(
233
+ self,
234
+ n_bodies: int,
235
+ masses: List[float] | NDArray[np.floating],
236
+ positions: NDArray[np.floating],
237
+ velocities: NDArray[np.floating],
238
+ phases: NDArray[np.floating] | None = None,
239
+ hbar_str: float = 1.0,
240
+ coupling_strength: float = 0.1,
241
+ coherence_strength: float = -1.0,
242
+ ):
243
+ """Initialize TNFR N-body system.
244
+
245
+ Parameters
246
+ ----------
247
+ n_bodies : int
248
+ Number of bodies
249
+ masses : array_like, shape (N,)
250
+ Masses (m = 1/νf, must be positive)
251
+ positions : ndarray, shape (N, 3)
252
+ Initial positions
253
+ velocities : ndarray, shape (N, 3)
254
+ Initial velocities
255
+ phases : ndarray, shape (N,), optional
256
+ Initial phases. If None, initialized to zero (synchronized)
257
+ hbar_str : float, default=1.0
258
+ Structural Planck constant
259
+ coupling_strength : float, default=0.1
260
+ Network coupling strength (J_0 in H_coupling)
261
+ coherence_strength : float, default=-1.0
262
+ Coherence potential strength (C_0 in H_coh)
263
+ Negative = attractive potential well
264
+
265
+ Raises
266
+ ------
267
+ ValueError
268
+ If dimensions mismatch or masses non-positive
269
+ """
270
+ if n_bodies < 1:
271
+ raise ValueError(f"n_bodies must be >= 1, got {n_bodies}")
272
+
273
+ self.n_bodies = n_bodies
274
+ self.masses = np.array(masses, dtype=float)
275
+
276
+ if len(self.masses) != n_bodies:
277
+ raise ValueError(
278
+ f"masses length {len(self.masses)} != n_bodies {n_bodies}"
279
+ )
280
+
281
+ if np.any(self.masses <= 0):
282
+ raise ValueError("All masses must be positive")
283
+
284
+ # State vectors
285
+ self.positions = np.asarray(positions, dtype=float).copy()
286
+ self.velocities = np.asarray(velocities, dtype=float).copy()
287
+
288
+ if phases is None:
289
+ self.phases = np.zeros(n_bodies, dtype=float)
290
+ else:
291
+ self.phases = np.asarray(phases, dtype=float).copy()
292
+
293
+ # Validate shapes
294
+ expected_shape = (n_bodies, 3)
295
+ if self.positions.shape != expected_shape:
296
+ raise ValueError(
297
+ f"positions shape {self.positions.shape} != {expected_shape}"
298
+ )
299
+ if self.velocities.shape != expected_shape:
300
+ raise ValueError(
301
+ f"velocities shape {self.velocities.shape} != {expected_shape}"
302
+ )
303
+ if self.phases.shape != (n_bodies,):
304
+ raise ValueError(
305
+ f"phases shape {self.phases.shape} != ({n_bodies},)"
306
+ )
307
+
308
+ self.time = 0.0
309
+ self.hbar_str = float(hbar_str)
310
+
311
+ # TNFR parameters
312
+ self.coupling_strength = float(coupling_strength)
313
+ self.coherence_strength = float(coherence_strength)
314
+
315
+ # Build TNFR graph
316
+ self._build_graph()
317
+
318
+ def _build_graph(self) -> None:
319
+ """Build TNFR graph representation.
320
+
321
+ Each body becomes a resonant node with:
322
+ - νf = 1/m (structural frequency)
323
+ - EPI encoding (position, velocity)
324
+ - Phase θ
325
+ - All-to-all coupling (full network)
326
+ """
327
+ import networkx as nx
328
+
329
+ self.graph: TNFRGraph = nx.Graph()
330
+ self.graph.graph["name"] = "tnfr_nbody_system"
331
+ self.graph.graph["H_COUPLING_STRENGTH"] = self.coupling_strength
332
+ self.graph.graph["H_COH_STRENGTH"] = self.coherence_strength
333
+
334
+ # Add nodes with TNFR attributes
335
+ for i in range(self.n_bodies):
336
+ node_id = f"body_{i}"
337
+
338
+ # Structural frequency: νf = 1/m
339
+ nu_f = 1.0 / self.masses[i]
340
+
341
+ # Create NFR node with structured EPI
342
+ epi_state = {
343
+ "position": self.positions[i].copy(),
344
+ "velocity": self.velocities[i].copy(),
345
+ }
346
+
347
+ # Note: create_nfr expects scalar epi for initialization
348
+ # We'll override it immediately
349
+ _, _ = create_nfr(
350
+ node_id,
351
+ epi=1.0, # Temporary, will be overwritten
352
+ vf=nu_f,
353
+ theta=float(self.phases[i]),
354
+ graph=self.graph,
355
+ )
356
+
357
+ # Override with structured EPI
358
+ self.graph.nodes[node_id]["epi"] = epi_state
359
+
360
+ # Add edges (all-to-all coupling)
361
+ # In TNFR, coupling strength depends on structural similarity
362
+ # Here we use uniform coupling for simplicity
363
+ for i in range(self.n_bodies):
364
+ for j in range(i + 1, self.n_bodies):
365
+ node_i = f"body_{i}"
366
+ node_j = f"body_{j}"
367
+
368
+ # Edge weight: coupling strength
369
+ # (In more sophisticated version, could depend on distance)
370
+ weight = self.coupling_strength
371
+ self.graph.add_edge(node_i, node_j, weight=weight)
372
+
373
+ def compute_energy(self) -> Tuple[float, float, float]:
374
+ """Compute system energy (kinetic + coherence potential).
375
+
376
+ Returns
377
+ -------
378
+ kinetic : float
379
+ Kinetic energy K = Σ (1/2) m v²
380
+ potential : float
381
+ Coherence potential U from TNFR Hamiltonian
382
+ total : float
383
+ Total energy H = K + U
384
+
385
+ Notes
386
+ -----
387
+ Unlike classical n-body (assumes U = -Gm₁m₂/r), the potential
388
+ here emerges from TNFR coherence matrix and coupling topology.
389
+ """
390
+ # Kinetic energy (same as classical)
391
+ v_squared = np.sum(self.velocities**2, axis=1)
392
+ kinetic = 0.5 * np.sum(self.masses * v_squared)
393
+
394
+ # Coherence potential from TNFR Hamiltonian
395
+ # This is the key difference: NO assumption about gravity
396
+ potential = compute_tnfr_coherence_potential(
397
+ self.graph, self.positions, self.hbar_str
398
+ )
399
+
400
+ total = kinetic + potential
401
+
402
+ return kinetic, potential, total
403
+
404
+ def compute_momentum(self) -> NDArray[np.floating]:
405
+ """Compute total linear momentum.
406
+
407
+ Returns
408
+ -------
409
+ momentum : ndarray, shape (3,)
410
+ Total momentum P = Σ m v
411
+ """
412
+ momentum = np.sum(self.masses[:, np.newaxis] * self.velocities, axis=0)
413
+ return momentum
414
+
415
+ def compute_angular_momentum(self) -> NDArray[np.floating]:
416
+ """Compute total angular momentum.
417
+
418
+ Returns
419
+ -------
420
+ angular_momentum : ndarray, shape (3,)
421
+ Total L = Σ r × (m v)
422
+ """
423
+ L = np.zeros(3)
424
+ for i in range(self.n_bodies):
425
+ L += self.masses[i] * np.cross(self.positions[i], self.velocities[i])
426
+ return L
427
+
428
+ def step(self, dt: float) -> None:
429
+ """Advance system by one time step using TNFR dynamics.
430
+
431
+ This implements the nodal equation: ∂EPI/∂t = νf · ΔNFR
432
+ where ΔNFR is computed from Hamiltonian commutator.
433
+
434
+ Parameters
435
+ ----------
436
+ dt : float
437
+ Time step (structural time units)
438
+
439
+ Notes
440
+ -----
441
+ Uses velocity Verlet-like integration for position/velocity,
442
+ but accelerations come from TNFR ΔNFR via coherence gradients.
443
+ """
444
+ # Update graph with current state
445
+ self._update_graph()
446
+
447
+ # Compute accelerations from TNFR coherence-based forces
448
+ # This is the key: forces emerge from coherence gradient, not gravity
449
+ accel = self._compute_tnfr_accelerations()
450
+
451
+ # Velocity Verlet integration
452
+ # v(t+dt/2) = v(t) + a(t) * dt/2
453
+ v_half = self.velocities + 0.5 * accel * dt
454
+
455
+ # r(t+dt) = r(t) + v(t+dt/2) * dt
456
+ self.positions += v_half * dt
457
+
458
+ # Update graph with new positions
459
+ self._update_graph()
460
+
461
+ # Recompute accelerations at new positions
462
+ accel_new = self._compute_tnfr_accelerations()
463
+
464
+ # v(t+dt) = v(t+dt/2) + a(t+dt) * dt/2
465
+ self.velocities = v_half + 0.5 * accel_new * dt
466
+
467
+ # Update phases based on ΔNFR
468
+ # Compute scalar ΔNFR for phase evolution
469
+ node_ids = [f"body_{i}" for i in range(self.n_bodies)]
470
+ dnfr_values = compute_tnfr_delta_nfr(
471
+ self.graph, node_ids, self.hbar_str
472
+ )
473
+
474
+ # Phase evolution: dθ/dt ~ ΔNFR
475
+ self.phases += dnfr_values * dt
476
+ self.phases = np.mod(self.phases, 2 * np.pi) # Keep in [0, 2π]
477
+
478
+ # Update time
479
+ self.time += dt
480
+
481
+ def _compute_tnfr_accelerations(self) -> NDArray[np.floating]:
482
+ """Compute accelerations from TNFR coherence-based forces.
483
+
484
+ This is where TNFR physics determines motion:
485
+ - Forces emerge from coherence gradient (NOT gravity!)
486
+ - Phase differences create attraction/repulsion
487
+ - Coupling strength determines force magnitude
488
+
489
+ Returns
490
+ -------
491
+ accelerations : ndarray, shape (N, 3)
492
+ Acceleration vectors for each body
493
+
494
+ Notes
495
+ -----
496
+ The key TNFR insight: Forces emerge from maximizing coherence.
497
+
498
+ Coherence between nodes i and j depends on:
499
+ 1. Phase difference: cos(θᵢ - θⱼ) (in-phase → attractive)
500
+ 2. Coupling strength: J₀ (from network edges)
501
+ 3. Distance dependence: Coherence decreases with separation
502
+
503
+ This gives rise to attraction/repulsion WITHOUT assuming gravity!
504
+ """
505
+ accelerations = np.zeros((self.n_bodies, 3))
506
+
507
+ # For each pair of bodies
508
+ for i in range(self.n_bodies):
509
+ for j in range(self.n_bodies):
510
+ if i == j:
511
+ continue
512
+
513
+ # Position difference
514
+ r_ij = self.positions[j] - self.positions[i]
515
+ dist = np.linalg.norm(r_ij)
516
+
517
+ if dist < 1e-10:
518
+ continue # Avoid singularity
519
+
520
+ # Unit vector from i to j
521
+ r_hat = r_ij / dist
522
+
523
+ # Phase difference (key TNFR element!)
524
+ phase_diff = self.phases[j] - self.phases[i]
525
+
526
+ # Coherence factor: positive when in-phase, negative when anti-phase
527
+ # This creates attraction for synchronized nodes
528
+ coherence_factor = np.cos(phase_diff)
529
+
530
+ # Distance-dependent coupling (coherence decays with distance)
531
+ # This emerges from spatial structure of coherence matrix
532
+ # Use exponential decay or power law
533
+ distance_factor = 1.0 / (dist**2 + 0.1) # Softened power law
534
+
535
+ # Frequency coupling: Both nodes contribute
536
+ nu_i = 1.0 / self.masses[i]
537
+ nu_j = 1.0 / self.masses[j]
538
+ freq_factor = np.sqrt(nu_i * nu_j)
539
+
540
+ # Total TNFR force magnitude
541
+ force_mag = (
542
+ self.coupling_strength *
543
+ self.coherence_strength * # Negative = attractive well
544
+ coherence_factor *
545
+ distance_factor *
546
+ freq_factor
547
+ )
548
+
549
+ # Force direction
550
+ force_vec = force_mag * r_hat
551
+
552
+ # Acceleration: a = F/m = F * νf
553
+ accelerations[i] += force_vec * nu_i
554
+
555
+ return accelerations
556
+
557
+ def _update_graph(self) -> None:
558
+ """Update graph representation with current state."""
559
+ for i in range(self.n_bodies):
560
+ node_id = f"body_{i}"
561
+
562
+ # Update EPI
563
+ epi_state = {
564
+ "position": self.positions[i].copy(),
565
+ "velocity": self.velocities[i].copy(),
566
+ }
567
+ self.graph.nodes[node_id]["epi"] = epi_state
568
+
569
+ # Update phase
570
+ self.graph.nodes[node_id]["theta"] = float(self.phases[i])
571
+
572
+ def evolve(
573
+ self,
574
+ t_final: float,
575
+ dt: float,
576
+ store_interval: int = 1,
577
+ ) -> Dict[str, Any]:
578
+ """Evolve system using pure TNFR dynamics.
579
+
580
+ Parameters
581
+ ----------
582
+ t_final : float
583
+ Final time
584
+ dt : float
585
+ Time step
586
+ store_interval : int, default=1
587
+ Store state every N steps
588
+
589
+ Returns
590
+ -------
591
+ history : dict
592
+ Contains: time, positions, velocities, phases,
593
+ energy, kinetic, potential, momentum, angular_momentum
594
+
595
+ Notes
596
+ -----
597
+ Evolution follows nodal equation with ΔNFR from Hamiltonian.
598
+ NO classical gravitational assumptions.
599
+ """
600
+ n_steps = int((t_final - self.time) / dt)
601
+
602
+ if n_steps < 1:
603
+ raise ValueError(f"t_final {t_final} <= current time {self.time}")
604
+
605
+ # Pre-allocate storage
606
+ n_stored = (n_steps // store_interval) + 1
607
+ times = np.zeros(n_stored)
608
+ positions_hist = np.zeros((n_stored, self.n_bodies, 3))
609
+ velocities_hist = np.zeros((n_stored, self.n_bodies, 3))
610
+ phases_hist = np.zeros((n_stored, self.n_bodies))
611
+ energies = np.zeros(n_stored)
612
+ kinetic_energies = np.zeros(n_stored)
613
+ potential_energies = np.zeros(n_stored)
614
+ momenta = np.zeros((n_stored, 3))
615
+ angular_momenta = np.zeros((n_stored, 3))
616
+
617
+ # Store initial state
618
+ store_idx = 0
619
+ times[store_idx] = self.time
620
+ positions_hist[store_idx] = self.positions.copy()
621
+ velocities_hist[store_idx] = self.velocities.copy()
622
+ phases_hist[store_idx] = self.phases.copy()
623
+ K, U, E = self.compute_energy()
624
+ kinetic_energies[store_idx] = K
625
+ potential_energies[store_idx] = U
626
+ energies[store_idx] = E
627
+ momenta[store_idx] = self.compute_momentum()
628
+ angular_momenta[store_idx] = self.compute_angular_momentum()
629
+ store_idx += 1
630
+
631
+ # Evolution loop
632
+ for step in range(n_steps):
633
+ self.step(dt)
634
+
635
+ # Store state if needed
636
+ if (step + 1) % store_interval == 0 and store_idx < n_stored:
637
+ times[store_idx] = self.time
638
+ positions_hist[store_idx] = self.positions.copy()
639
+ velocities_hist[store_idx] = self.velocities.copy()
640
+ phases_hist[store_idx] = self.phases.copy()
641
+ K, U, E = self.compute_energy()
642
+ kinetic_energies[store_idx] = K
643
+ potential_energies[store_idx] = U
644
+ energies[store_idx] = E
645
+ momenta[store_idx] = self.compute_momentum()
646
+ angular_momenta[store_idx] = self.compute_angular_momentum()
647
+ store_idx += 1
648
+
649
+ # Compute energy drift
650
+ energy_drift = abs(energies[-1] - energies[0]) / abs(energies[0])
651
+
652
+ return {
653
+ "time": times[:store_idx],
654
+ "positions": positions_hist[:store_idx],
655
+ "velocities": velocities_hist[:store_idx],
656
+ "phases": phases_hist[:store_idx],
657
+ "energy": energies[:store_idx],
658
+ "kinetic": kinetic_energies[:store_idx],
659
+ "potential": potential_energies[:store_idx],
660
+ "momentum": momenta[:store_idx],
661
+ "angular_momentum": angular_momenta[:store_idx],
662
+ "energy_drift": energy_drift,
663
+ }
664
+
665
+ def plot_trajectories(
666
+ self,
667
+ history: Dict[str, Any],
668
+ show_energy: bool = True,
669
+ show_phases: bool = True,
670
+ ) -> Figure:
671
+ """Plot trajectories, energy, and phase evolution.
672
+
673
+ Parameters
674
+ ----------
675
+ history : dict
676
+ Result from evolve()
677
+ show_energy : bool, default=True
678
+ Show energy conservation plot
679
+ show_phases : bool, default=True
680
+ Show phase evolution plot
681
+
682
+ Returns
683
+ -------
684
+ fig : matplotlib Figure
685
+
686
+ Raises
687
+ ------
688
+ ImportError
689
+ If matplotlib not available
690
+ """
691
+ try:
692
+ import matplotlib.pyplot as plt
693
+ from mpl_toolkits.mplot3d import Axes3D
694
+ except ImportError as exc:
695
+ raise ImportError(
696
+ "matplotlib required for plotting. "
697
+ "Install with: pip install 'tnfr[viz-basic]'"
698
+ ) from exc
699
+
700
+ n_plots = 1 + int(show_energy) + int(show_phases)
701
+ fig = plt.figure(figsize=(6 * n_plots, 5))
702
+
703
+ plot_idx = 1
704
+
705
+ # 3D trajectories
706
+ ax_3d = fig.add_subplot(1, n_plots, plot_idx, projection="3d")
707
+ plot_idx += 1
708
+
709
+ positions = history["positions"]
710
+ colors = plt.cm.rainbow(np.linspace(0, 1, self.n_bodies))
711
+
712
+ for i in range(self.n_bodies):
713
+ traj = positions[:, i, :]
714
+ ax_3d.plot(
715
+ traj[:, 0],
716
+ traj[:, 1],
717
+ traj[:, 2],
718
+ color=colors[i],
719
+ label=f"Body {i+1} (m={self.masses[i]:.2f})",
720
+ alpha=0.7,
721
+ )
722
+ ax_3d.scatter(
723
+ traj[0, 0], traj[0, 1], traj[0, 2],
724
+ color=colors[i], s=100, marker="o"
725
+ )
726
+ ax_3d.scatter(
727
+ traj[-1, 0], traj[-1, 1], traj[-1, 2],
728
+ color=colors[i], s=50, marker="x"
729
+ )
730
+
731
+ ax_3d.set_xlabel("X")
732
+ ax_3d.set_ylabel("Y")
733
+ ax_3d.set_zlabel("Z")
734
+ ax_3d.set_title("TNFR N-Body Trajectories (No Gravitational Assumption)")
735
+ ax_3d.legend()
736
+
737
+ # Energy conservation
738
+ if show_energy:
739
+ ax_energy = fig.add_subplot(1, n_plots, plot_idx)
740
+ plot_idx += 1
741
+
742
+ time = history["time"]
743
+ E = history["energy"]
744
+ E0 = E[0]
745
+
746
+ ax_energy.plot(
747
+ time,
748
+ (E - E0) / abs(E0) * 100,
749
+ label="Energy drift (%)",
750
+ color="red",
751
+ linewidth=2,
752
+ )
753
+ ax_energy.axhline(0, color="black", linestyle="--", alpha=0.3)
754
+ ax_energy.set_xlabel("Structural Time")
755
+ ax_energy.set_ylabel("ΔE/E₀ (%)")
756
+ ax_energy.set_title("Energy Conservation (TNFR Hamiltonian)")
757
+ ax_energy.legend()
758
+ ax_energy.grid(True, alpha=0.3)
759
+
760
+ # Phase evolution
761
+ if show_phases:
762
+ ax_phases = fig.add_subplot(1, n_plots, plot_idx)
763
+
764
+ time = history["time"]
765
+ phases = history["phases"]
766
+
767
+ for i in range(self.n_bodies):
768
+ ax_phases.plot(
769
+ time,
770
+ phases[:, i],
771
+ color=colors[i],
772
+ label=f"Body {i+1}",
773
+ linewidth=2,
774
+ )
775
+
776
+ ax_phases.set_xlabel("Structural Time")
777
+ ax_phases.set_ylabel("Phase θ (rad)")
778
+ ax_phases.set_title("Phase Evolution (TNFR Synchronization)")
779
+ ax_phases.legend()
780
+ ax_phases.grid(True, alpha=0.3)
781
+
782
+ plt.tight_layout()
783
+ return fig