physicskit 0.1.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.
Files changed (484) hide show
  1. physicskit/__init__.py +51 -0
  2. physicskit/astro/__init__.py +66 -0
  3. physicskit/astro/cosmic_web.py +395 -0
  4. physicskit/astro/galactic_dynamics.py +137 -0
  5. physicskit/astro/nbody.py +208 -0
  6. physicskit/astro/orbital_mechanics.py +220 -0
  7. physicskit/astro/stellar_dynamo.py +628 -0
  8. physicskit/astro/stellar_structure.py +198 -0
  9. physicskit/astro/tests/__init__.py +0 -0
  10. physicskit/astro/tests/conftest.py +7 -0
  11. physicskit/astro/tests/test_cosmic_web.py +159 -0
  12. physicskit/astro/tests/test_galactic_dynamics.py +46 -0
  13. physicskit/astro/tests/test_nbody.py +81 -0
  14. physicskit/astro/tests/test_orbital_mechanics.py +73 -0
  15. physicskit/astro/tests/test_stellar_dynamo.py +198 -0
  16. physicskit/astro/tests/test_stellar_structure.py +52 -0
  17. physicskit/astro/tests/test_visualizers.py +159 -0
  18. physicskit/astro/visualizers.py +628 -0
  19. physicskit/chaos/__init__.py +55 -0
  20. physicskit/chaos/core/__init__.py +3 -0
  21. physicskit/chaos/core/base_system.py +342 -0
  22. physicskit/chaos/core/integrators.py +40 -0
  23. physicskit/chaos/exceptions.py +24 -0
  24. physicskit/chaos/quantum/__init__.py +38 -0
  25. physicskit/chaos/quantum/billiards.py +274 -0
  26. physicskit/chaos/quantum/husimi.py +85 -0
  27. physicskit/chaos/quantum/maps.py +366 -0
  28. physicskit/chaos/systems/__init__.py +38 -0
  29. physicskit/chaos/systems/billiards.py +1131 -0
  30. physicskit/chaos/systems/continuous.py +1276 -0
  31. physicskit/chaos/systems/maps.py +557 -0
  32. physicskit/chaos/tests/__init__.py +0 -0
  33. physicskit/chaos/tests/test_basins.py +76 -0
  34. physicskit/chaos/tests/test_bifurcation.py +132 -0
  35. physicskit/chaos/tests/test_billiards.py +115 -0
  36. physicskit/chaos/tests/test_continuous.py +224 -0
  37. physicskit/chaos/tests/test_dimension.py +69 -0
  38. physicskit/chaos/tests/test_divergence.py +90 -0
  39. physicskit/chaos/tests/test_dynamic_plots.py +269 -0
  40. physicskit/chaos/tests/test_integrators.py +96 -0
  41. physicskit/chaos/tests/test_io.py +41 -0
  42. physicskit/chaos/tests/test_maps.py +110 -0
  43. physicskit/chaos/tests/test_metrics.py +115 -0
  44. physicskit/chaos/tests/test_phase_space.py +73 -0
  45. physicskit/chaos/tests/test_properties.py +116 -0
  46. physicskit/chaos/tests/test_quantum_billiards.py +94 -0
  47. physicskit/chaos/tests/test_quantum_maps.py +144 -0
  48. physicskit/chaos/tests/test_recurrence.py +51 -0
  49. physicskit/chaos/tests/test_repr_and_api_consistency.py +134 -0
  50. physicskit/chaos/tests/test_section.py +49 -0
  51. physicskit/chaos/tests/test_spectral.py +48 -0
  52. physicskit/chaos/tests/test_streamlit_app.py +45 -0
  53. physicskit/chaos/tests/test_timeseries.py +131 -0
  54. physicskit/chaos/tests/test_viewer3d.py +41 -0
  55. physicskit/chaos/utils/__init__.py +46 -0
  56. physicskit/chaos/utils/dimension.py +139 -0
  57. physicskit/chaos/utils/io.py +95 -0
  58. physicskit/chaos/utils/metrics.py +304 -0
  59. physicskit/chaos/utils/recurrence.py +122 -0
  60. physicskit/chaos/utils/spectral.py +69 -0
  61. physicskit/chaos/utils/timeseries.py +303 -0
  62. physicskit/chaos/visualizers/__init__.py +85 -0
  63. physicskit/chaos/visualizers/basins.py +185 -0
  64. physicskit/chaos/visualizers/bifurcation.py +273 -0
  65. physicskit/chaos/visualizers/divergence.py +374 -0
  66. physicskit/chaos/visualizers/dynamic_plots.py +1372 -0
  67. physicskit/chaos/visualizers/phase_space.py +133 -0
  68. physicskit/chaos/visualizers/quantum.py +299 -0
  69. physicskit/chaos/visualizers/recurrence.py +91 -0
  70. physicskit/chaos/visualizers/section.py +161 -0
  71. physicskit/chaos/visualizers/theme.py +31 -0
  72. physicskit/chaos/visualizers/viewer3d.py +82 -0
  73. physicskit/classical/__init__.py +55 -0
  74. physicskit/classical/core/__init__.py +17 -0
  75. physicskit/classical/core/base_system.py +510 -0
  76. physicskit/classical/core/integrators.py +397 -0
  77. physicskit/classical/systems/__init__.py +32 -0
  78. physicskit/classical/systems/chains.py +334 -0
  79. physicskit/classical/systems/hamiltonian.py +215 -0
  80. physicskit/classical/systems/lagrangian.py +308 -0
  81. physicskit/classical/systems/newtonian.py +536 -0
  82. physicskit/classical/systems/rotations.py +614 -0
  83. physicskit/classical/tests/__init__.py +0 -0
  84. physicskit/classical/tests/conftest.py +7 -0
  85. physicskit/classical/tests/test_base_system.py +113 -0
  86. physicskit/classical/tests/test_chains.py +84 -0
  87. physicskit/classical/tests/test_conservation.py +244 -0
  88. physicskit/classical/tests/test_conservation_utils.py +47 -0
  89. physicskit/classical/tests/test_hamiltonian.py +44 -0
  90. physicskit/classical/tests/test_integrators.py +123 -0
  91. physicskit/classical/tests/test_lagrangian.py +69 -0
  92. physicskit/classical/tests/test_newtonian.py +45 -0
  93. physicskit/classical/tests/test_rotations.py +160 -0
  94. physicskit/classical/tests/test_stepsize.py +149 -0
  95. physicskit/classical/tests/test_symbolic.py +125 -0
  96. physicskit/classical/tests/test_visualizers.py +172 -0
  97. physicskit/classical/utils/__init__.py +20 -0
  98. physicskit/classical/utils/conservation.py +126 -0
  99. physicskit/classical/utils/stepsize.py +187 -0
  100. physicskit/classical/utils/symbolic.py +235 -0
  101. physicskit/classical/visualizers/__init__.py +25 -0
  102. physicskit/classical/visualizers/animations.py +508 -0
  103. physicskit/classical/visualizers/interactive.py +113 -0
  104. physicskit/classical/visualizers/modal_analysis.py +97 -0
  105. physicskit/classical/visualizers/phase_space.py +214 -0
  106. physicskit/condensed/__init__.py +122 -0
  107. physicskit/condensed/anderson_localization.py +155 -0
  108. physicskit/condensed/correlated.py +284 -0
  109. physicskit/condensed/ginzburg_landau.py +220 -0
  110. physicskit/condensed/landau_levels.py +246 -0
  111. physicskit/condensed/laughlin.py +229 -0
  112. physicskit/condensed/models.py +627 -0
  113. physicskit/condensed/tests/__init__.py +0 -0
  114. physicskit/condensed/tests/test_condensed.py +518 -0
  115. physicskit/condensed/tests/test_visualizers.py +87 -0
  116. physicskit/condensed/tight_binding.py +445 -0
  117. physicskit/condensed/topological_insulator_3d.py +178 -0
  118. physicskit/condensed/topology.py +217 -0
  119. physicskit/condensed/visualizers.py +296 -0
  120. physicskit/condensed/weyl.py +180 -0
  121. physicskit/constants.py +184 -0
  122. physicskit/fields/__init__.py +111 -0
  123. physicskit/fields/electrodynamics.py +671 -0
  124. physicskit/fields/quantum_fields.py +510 -0
  125. physicskit/fields/solitons.py +527 -0
  126. physicskit/fields/tests/__init__.py +0 -0
  127. physicskit/fields/tests/conftest.py +7 -0
  128. physicskit/fields/tests/test_field_animations.py +154 -0
  129. physicskit/fields/tests/test_fields.py +439 -0
  130. physicskit/fields/tests/test_visualizers.py +101 -0
  131. physicskit/fields/visualizers.py +503 -0
  132. physicskit/fluids/__init__.py +134 -0
  133. physicskit/fluids/core/__init__.py +23 -0
  134. physicskit/fluids/core/grid.py +165 -0
  135. physicskit/fluids/core/timestepping.py +231 -0
  136. physicskit/fluids/exceptions.py +26 -0
  137. physicskit/fluids/systems/__init__.py +70 -0
  138. physicskit/fluids/systems/compressible_flow.py +281 -0
  139. physicskit/fluids/systems/instabilities.py +343 -0
  140. physicskit/fluids/systems/navier_stokes.py +221 -0
  141. physicskit/fluids/systems/potential_flow.py +440 -0
  142. physicskit/fluids/systems/viscous_flow.py +425 -0
  143. physicskit/fluids/systems/vortex_dynamics.py +304 -0
  144. physicskit/fluids/tests/__init__.py +0 -0
  145. physicskit/fluids/tests/conftest.py +7 -0
  146. physicskit/fluids/tests/test_compressible_flow.py +66 -0
  147. physicskit/fluids/tests/test_instabilities.py +115 -0
  148. physicskit/fluids/tests/test_navier_stokes.py +67 -0
  149. physicskit/fluids/tests/test_potential_flow.py +92 -0
  150. physicskit/fluids/tests/test_static_visualizers.py +91 -0
  151. physicskit/fluids/tests/test_utils.py +100 -0
  152. physicskit/fluids/tests/test_viscous_flow.py +110 -0
  153. physicskit/fluids/tests/test_visualizers.py +34 -0
  154. physicskit/fluids/tests/test_vortex_dynamics.py +88 -0
  155. physicskit/fluids/utils/__init__.py +18 -0
  156. physicskit/fluids/utils/dimensionless.py +239 -0
  157. physicskit/fluids/utils/spectral_analysis.py +142 -0
  158. physicskit/fluids/visualizers/__init__.py +14 -0
  159. physicskit/fluids/visualizers/compressible.py +39 -0
  160. physicskit/fluids/visualizers/flow_fields.py +252 -0
  161. physicskit/fluids/visualizers/potential_flow.py +51 -0
  162. physicskit/fluids/visualizers/spectra.py +62 -0
  163. physicskit/fluids/visualizers/theme.py +38 -0
  164. physicskit/integrators/__init__.py +52 -0
  165. physicskit/integrators/adaptive.py +199 -0
  166. physicskit/integrators/fixed_step.py +360 -0
  167. physicskit/integrators/tests/__init__.py +0 -0
  168. physicskit/integrators/tests/test_integrators.py +101 -0
  169. physicskit/optics/__init__.py +124 -0
  170. physicskit/optics/_compat.py +5 -0
  171. physicskit/optics/gaussian.py +382 -0
  172. physicskit/optics/quantum_optics.py +428 -0
  173. physicskit/optics/ray.py +469 -0
  174. physicskit/optics/tests/__init__.py +0 -0
  175. physicskit/optics/tests/test_diffraction_animation.py +38 -0
  176. physicskit/optics/tests/test_gaussian.py +170 -0
  177. physicskit/optics/tests/test_quantum_optics.py +147 -0
  178. physicskit/optics/tests/test_ray.py +177 -0
  179. physicskit/optics/tests/test_visualizers.py +58 -0
  180. physicskit/optics/tests/test_wave.py +146 -0
  181. physicskit/optics/visualizers.py +264 -0
  182. physicskit/optics/wave.py +365 -0
  183. physicskit/particle/__init__.py +152 -0
  184. physicskit/particle/collider.py +382 -0
  185. physicskit/particle/confinement.py +125 -0
  186. physicskit/particle/decays.py +357 -0
  187. physicskit/particle/electroweak.py +300 -0
  188. physicskit/particle/kinematics.py +271 -0
  189. physicskit/particle/neutrinos.py +84 -0
  190. physicskit/particle/nuclear.py +100 -0
  191. physicskit/particle/scattering.py +159 -0
  192. physicskit/particle/tests/__init__.py +0 -0
  193. physicskit/particle/tests/conftest.py +7 -0
  194. physicskit/particle/tests/test_collider.py +119 -0
  195. physicskit/particle/tests/test_confinement.py +42 -0
  196. physicskit/particle/tests/test_decays.py +112 -0
  197. physicskit/particle/tests/test_electroweak.py +97 -0
  198. physicskit/particle/tests/test_kinematics.py +138 -0
  199. physicskit/particle/tests/test_neutrinos.py +36 -0
  200. physicskit/particle/tests/test_nuclear.py +45 -0
  201. physicskit/particle/tests/test_scattering.py +64 -0
  202. physicskit/particle/tests/test_visualizers.py +214 -0
  203. physicskit/particle/visualizers/__init__.py +38 -0
  204. physicskit/particle/visualizers/animations.py +705 -0
  205. physicskit/particle/visualizers/static.py +101 -0
  206. physicskit/plasma/__init__.py +176 -0
  207. physicskit/plasma/acceleration.py +152 -0
  208. physicskit/plasma/instabilities.py +417 -0
  209. physicskit/plasma/kinetic.py +588 -0
  210. physicskit/plasma/mhd.py +463 -0
  211. physicskit/plasma/single_particle.py +562 -0
  212. physicskit/plasma/tests/__init__.py +0 -0
  213. physicskit/plasma/tests/conftest.py +7 -0
  214. physicskit/plasma/tests/test_new_time_domain_features.py +222 -0
  215. physicskit/plasma/tests/test_plasma.py +232 -0
  216. physicskit/plasma/tests/test_visualizers.py +79 -0
  217. physicskit/plasma/turbulence.py +219 -0
  218. physicskit/plasma/visualizers.py +868 -0
  219. physicskit/plasma/waves.py +549 -0
  220. physicskit/quantum/__init__.py +180 -0
  221. physicskit/quantum/_compat.py +20 -0
  222. physicskit/quantum/chapters/__init__.py +0 -0
  223. physicskit/quantum/chapters/entanglement.py +489 -0
  224. physicskit/quantum/chapters/harmonic_spin.py +517 -0
  225. physicskit/quantum/chapters/hydrogen_am.py +292 -0
  226. physicskit/quantum/chapters/perturbation.py +350 -0
  227. physicskit/quantum/chapters/potentials.py +997 -0
  228. physicskit/quantum/chapters/spin.py +306 -0
  229. physicskit/quantum/chapters/wave_packets.py +541 -0
  230. physicskit/quantum/core/__init__.py +0 -0
  231. physicskit/quantum/core/eigensolvers.py +311 -0
  232. physicskit/quantum/core/operators.py +296 -0
  233. physicskit/quantum/core/solvers.py +271 -0
  234. physicskit/quantum/tests/__init__.py +0 -0
  235. physicskit/quantum/tests/conftest.py +7 -0
  236. physicskit/quantum/tests/test_animations.py +225 -0
  237. physicskit/quantum/tests/test_compat.py +43 -0
  238. physicskit/quantum/tests/test_eigensolvers.py +24 -0
  239. physicskit/quantum/tests/test_entanglement.py +127 -0
  240. physicskit/quantum/tests/test_harmonic_spin.py +140 -0
  241. physicskit/quantum/tests/test_hydrogen_am.py +48 -0
  242. physicskit/quantum/tests/test_measure.py +110 -0
  243. physicskit/quantum/tests/test_operators.py +105 -0
  244. physicskit/quantum/tests/test_perturbation.py +112 -0
  245. physicskit/quantum/tests/test_physics_checks.py +131 -0
  246. physicskit/quantum/tests/test_potentials.py +191 -0
  247. physicskit/quantum/tests/test_solvers.py +70 -0
  248. physicskit/quantum/tests/test_spin.py +22 -0
  249. physicskit/quantum/tests/test_unitarity.py +84 -0
  250. physicskit/quantum/tests/test_wave_packets.py +53 -0
  251. physicskit/quantum/utils/__init__.py +0 -0
  252. physicskit/quantum/utils/measure.py +253 -0
  253. physicskit/quantum/visualizers/__init__.py +0 -0
  254. physicskit/quantum/visualizers/bloch_sphere.py +205 -0
  255. physicskit/quantum/visualizers/entanglement.py +58 -0
  256. physicskit/quantum/visualizers/orbitals.py +187 -0
  257. physicskit/quantum/visualizers/phase_space.py +179 -0
  258. physicskit/quantum/visualizers/wavefunctions.py +230 -0
  259. physicskit/relativity/__init__.py +61 -0
  260. physicskit/relativity/chapters/__init__.py +33 -0
  261. physicskit/relativity/chapters/cosmology.py +190 -0
  262. physicskit/relativity/chapters/gw_merger.py +399 -0
  263. physicskit/relativity/chapters/kerr.py +371 -0
  264. physicskit/relativity/chapters/lensing.py +196 -0
  265. physicskit/relativity/chapters/neutron_star.py +149 -0
  266. physicskit/relativity/chapters/schwarzschild.py +412 -0
  267. physicskit/relativity/chapters/timekeeping.py +133 -0
  268. physicskit/relativity/core/__init__.py +12 -0
  269. physicskit/relativity/core/geodesics.py +216 -0
  270. physicskit/relativity/core/kerr_raytracer.py +259 -0
  271. physicskit/relativity/core/raytracer.py +241 -0
  272. physicskit/relativity/core/tensors.py +468 -0
  273. physicskit/relativity/tests/__init__.py +0 -0
  274. physicskit/relativity/tests/test_conservations.py +54 -0
  275. physicskit/relativity/tests/test_constants.py +34 -0
  276. physicskit/relativity/tests/test_cosmology.py +61 -0
  277. physicskit/relativity/tests/test_gw_merger.py +117 -0
  278. physicskit/relativity/tests/test_kerr.py +116 -0
  279. physicskit/relativity/tests/test_kerr_raytracer.py +103 -0
  280. physicskit/relativity/tests/test_lensing.py +75 -0
  281. physicskit/relativity/tests/test_neutron_star.py +48 -0
  282. physicskit/relativity/tests/test_raytracer.py +67 -0
  283. physicskit/relativity/tests/test_schwarzschild.py +126 -0
  284. physicskit/relativity/tests/test_shadow_render.py +81 -0
  285. physicskit/relativity/tests/test_tensors.py +86 -0
  286. physicskit/relativity/tests/test_timekeeping.py +51 -0
  287. physicskit/relativity/tests/test_visualizers_misc.py +115 -0
  288. physicskit/relativity/utils/__init__.py +1 -0
  289. physicskit/relativity/utils/constants.py +232 -0
  290. physicskit/relativity/visualizers/__init__.py +46 -0
  291. physicskit/relativity/visualizers/interactive.py +127 -0
  292. physicskit/relativity/visualizers/shadow_render.py +367 -0
  293. physicskit/relativity/visualizers/spacetime_3d.py +92 -0
  294. physicskit/relativity/visualizers/spacetime_diagrams.py +198 -0
  295. physicskit/relativity/visualizers/wave_plots.py +185 -0
  296. physicskit/rmt/__init__.py +8 -0
  297. physicskit/rmt/ensembles/__init__.py +108 -0
  298. physicskit/rmt/ensembles/banded.py +114 -0
  299. physicskit/rmt/ensembles/base.py +135 -0
  300. physicskit/rmt/ensembles/bdg.py +218 -0
  301. physicskit/rmt/ensembles/chiral.py +308 -0
  302. physicskit/rmt/ensembles/circular.py +104 -0
  303. physicskit/rmt/ensembles/crossover.py +82 -0
  304. physicskit/rmt/ensembles/density_matrix.py +148 -0
  305. physicskit/rmt/ensembles/effective_hamiltonian.py +230 -0
  306. physicskit/rmt/ensembles/embedded.py +207 -0
  307. physicskit/rmt/ensembles/gaussian.py +153 -0
  308. physicskit/rmt/ensembles/ginibre.py +110 -0
  309. physicskit/rmt/ensembles/girko.py +190 -0
  310. physicskit/rmt/ensembles/graph_laplacian.py +92 -0
  311. physicskit/rmt/ensembles/haar.py +57 -0
  312. physicskit/rmt/ensembles/jacobi.py +182 -0
  313. physicskit/rmt/ensembles/poisson.py +77 -0
  314. physicskit/rmt/ensembles/polynomial.py +139 -0
  315. physicskit/rmt/ensembles/pt_symmetric.py +120 -0
  316. physicskit/rmt/ensembles/single_ring.py +170 -0
  317. physicskit/rmt/ensembles/sparse.py +95 -0
  318. physicskit/rmt/ensembles/syk.py +170 -0
  319. physicskit/rmt/ensembles/truncated_unitary.py +80 -0
  320. physicskit/rmt/ensembles/universality.py +92 -0
  321. physicskit/rmt/ensembles/wishart.py +88 -0
  322. physicskit/rmt/py.typed +0 -0
  323. physicskit/rmt/spectrum.py +82 -0
  324. physicskit/rmt/stats/__init__.py +160 -0
  325. physicskit/rmt/stats/characteristic_polynomial.py +77 -0
  326. physicskit/rmt/stats/circular_law.py +54 -0
  327. physicskit/rmt/stats/correlations.py +162 -0
  328. physicskit/rmt/stats/density.py +70 -0
  329. physicskit/rmt/stats/entanglement.py +36 -0
  330. physicskit/rmt/stats/free_probability.py +202 -0
  331. physicskit/rmt/stats/localization.py +338 -0
  332. physicskit/rmt/stats/marchenko_pastur.py +55 -0
  333. physicskit/rmt/stats/pt_symmetric.py +265 -0
  334. physicskit/rmt/stats/ratios.py +110 -0
  335. physicskit/rmt/stats/real_ginibre.py +134 -0
  336. physicskit/rmt/stats/rigidity.py +272 -0
  337. physicskit/rmt/stats/single_ring.py +95 -0
  338. physicskit/rmt/stats/spacing.py +150 -0
  339. physicskit/rmt/stats/tracy_widom.py +175 -0
  340. physicskit/rmt/stats/truncated_unitary.py +40 -0
  341. physicskit/rmt/stats/unfolding.py +37 -0
  342. physicskit/rmt/stats/wachter.py +67 -0
  343. physicskit/rmt/tests/__init__.py +0 -0
  344. physicskit/rmt/tests/cache_utils.py +107 -0
  345. physicskit/rmt/tests/conftest.py +8 -0
  346. physicskit/rmt/tests/test_banded.py +86 -0
  347. physicskit/rmt/tests/test_base.py +29 -0
  348. physicskit/rmt/tests/test_bdg.py +133 -0
  349. physicskit/rmt/tests/test_characteristic_polynomial.py +50 -0
  350. physicskit/rmt/tests/test_chiral.py +273 -0
  351. physicskit/rmt/tests/test_circular.py +225 -0
  352. physicskit/rmt/tests/test_crossover.py +72 -0
  353. physicskit/rmt/tests/test_density.py +23 -0
  354. physicskit/rmt/tests/test_density_matrix.py +181 -0
  355. physicskit/rmt/tests/test_effective_hamiltonian.py +140 -0
  356. physicskit/rmt/tests/test_embedded.py +169 -0
  357. physicskit/rmt/tests/test_free_probability.py +81 -0
  358. physicskit/rmt/tests/test_gaussian_ensembles.py +22 -0
  359. physicskit/rmt/tests/test_gaussian_semicircle.py +127 -0
  360. physicskit/rmt/tests/test_ginibre_circular_law.py +131 -0
  361. physicskit/rmt/tests/test_girko.py +178 -0
  362. physicskit/rmt/tests/test_graph_laplacian.py +102 -0
  363. physicskit/rmt/tests/test_haar.py +118 -0
  364. physicskit/rmt/tests/test_jacobi_wachter.py +136 -0
  365. physicskit/rmt/tests/test_localization.py +240 -0
  366. physicskit/rmt/tests/test_poisson.py +149 -0
  367. physicskit/rmt/tests/test_polynomial.py +107 -0
  368. physicskit/rmt/tests/test_pt_symmetric.py +178 -0
  369. physicskit/rmt/tests/test_random_state.py +19 -0
  370. physicskit/rmt/tests/test_real_ginibre.py +112 -0
  371. physicskit/rmt/tests/test_rigidity_universality.py +167 -0
  372. physicskit/rmt/tests/test_single_ring.py +132 -0
  373. physicskit/rmt/tests/test_spacing_and_ratios.py +122 -0
  374. physicskit/rmt/tests/test_sparse.py +71 -0
  375. physicskit/rmt/tests/test_spectrum.py +27 -0
  376. physicskit/rmt/tests/test_syk.py +136 -0
  377. physicskit/rmt/tests/test_tracy_widom.py +113 -0
  378. physicskit/rmt/tests/test_tridiagonal.py +41 -0
  379. physicskit/rmt/tests/test_truncated_unitary.py +80 -0
  380. physicskit/rmt/tests/test_validation_base.py +25 -0
  381. physicskit/rmt/tests/test_validation_misc.py +67 -0
  382. physicskit/rmt/tests/test_wishart_marchenko_pastur.py +120 -0
  383. physicskit/rmt/utils/__init__.py +0 -0
  384. physicskit/rmt/utils/haar.py +128 -0
  385. physicskit/rmt/utils/random_state.py +24 -0
  386. physicskit/rmt/utils/tridiagonal.py +167 -0
  387. physicskit/rmt/validation/__init__.py +36 -0
  388. physicskit/rmt/validation/base.py +126 -0
  389. physicskit/rmt/validation/circular_law.py +80 -0
  390. physicskit/rmt/validation/marchenko_pastur.py +53 -0
  391. physicskit/rmt/validation/ratio_distribution.py +53 -0
  392. physicskit/rmt/validation/real_ginibre.py +60 -0
  393. physicskit/rmt/validation/sine_kernel.py +57 -0
  394. physicskit/rmt/validation/single_ring.py +68 -0
  395. physicskit/rmt/validation/tracy_widom.py +89 -0
  396. physicskit/rmt/validation/universality.py +100 -0
  397. physicskit/rmt/validation/wachter.py +52 -0
  398. physicskit/rmt/validation/wigner_semicircle.py +40 -0
  399. physicskit/rmt/validation/wigner_surmise.py +58 -0
  400. physicskit/semiclassical/__init__.py +94 -0
  401. physicskit/semiclassical/_compat.py +5 -0
  402. physicskit/semiclassical/core/__init__.py +41 -0
  403. physicskit/semiclassical/core/gutzwiller.py +215 -0
  404. physicskit/semiclassical/core/path_integral.py +594 -0
  405. physicskit/semiclassical/core/propagators.py +579 -0
  406. physicskit/semiclassical/core/wkb.py +308 -0
  407. physicskit/semiclassical/systems/__init__.py +13 -0
  408. physicskit/semiclassical/systems/scarring.py +278 -0
  409. physicskit/semiclassical/tests/__init__.py +0 -0
  410. physicskit/semiclassical/tests/test_gutzwiller.py +35 -0
  411. physicskit/semiclassical/tests/test_path_integral.py +194 -0
  412. physicskit/semiclassical/tests/test_propagators.py +82 -0
  413. physicskit/semiclassical/tests/test_scarring.py +43 -0
  414. physicskit/semiclassical/tests/test_visualizers.py +89 -0
  415. physicskit/semiclassical/tests/test_wkb.py +30 -0
  416. physicskit/semiclassical/visualizers/__init__.py +17 -0
  417. physicskit/semiclassical/visualizers/gutzwiller.py +55 -0
  418. physicskit/semiclassical/visualizers/path_integral.py +195 -0
  419. physicskit/semiclassical/visualizers/propagators.py +89 -0
  420. physicskit/semiclassical/visualizers/scarring.py +146 -0
  421. physicskit/semiclassical/visualizers/wkb.py +55 -0
  422. physicskit/statphys/__init__.py +80 -0
  423. physicskit/statphys/chapters/__init__.py +43 -0
  424. physicskit/statphys/chapters/ehrenfest_urn.py +112 -0
  425. physicskit/statphys/chapters/ising_lattice.py +431 -0
  426. physicskit/statphys/chapters/kpz_growth.py +129 -0
  427. physicskit/statphys/chapters/molecular_dynamics.py +258 -0
  428. physicskit/statphys/chapters/nonequilibrium_work.py +146 -0
  429. physicskit/statphys/chapters/percolation.py +305 -0
  430. physicskit/statphys/chapters/random_walk.py +142 -0
  431. physicskit/statphys/chapters/renormalization.py +148 -0
  432. physicskit/statphys/chapters/sandpile.py +109 -0
  433. physicskit/statphys/chapters/spin_glass.py +275 -0
  434. physicskit/statphys/core/__init__.py +10 -0
  435. physicskit/statphys/core/kpz_engine.py +53 -0
  436. physicskit/statphys/core/md_engine.py +157 -0
  437. physicskit/statphys/core/monte_carlo.py +462 -0
  438. physicskit/statphys/core/random_walk_engine.py +68 -0
  439. physicskit/statphys/core/sandpile_engine.py +52 -0
  440. physicskit/statphys/tests/__init__.py +0 -0
  441. physicskit/statphys/tests/test_dynamics.py +66 -0
  442. physicskit/statphys/tests/test_ehrenfest_urn.py +44 -0
  443. physicskit/statphys/tests/test_finite_size_scaling.py +62 -0
  444. physicskit/statphys/tests/test_interactive.py +33 -0
  445. physicskit/statphys/tests/test_ising.py +68 -0
  446. physicskit/statphys/tests/test_kpz.py +55 -0
  447. physicskit/statphys/tests/test_landau_theory.py +49 -0
  448. physicskit/statphys/tests/test_md_engine.py +31 -0
  449. physicskit/statphys/tests/test_molecular_dynamics.py +72 -0
  450. physicskit/statphys/tests/test_nonequilibrium_work.py +33 -0
  451. physicskit/statphys/tests/test_partition_function.py +26 -0
  452. physicskit/statphys/tests/test_percolation.py +116 -0
  453. physicskit/statphys/tests/test_potts_model.py +46 -0
  454. physicskit/statphys/tests/test_random_walk.py +50 -0
  455. physicskit/statphys/tests/test_renormalization.py +44 -0
  456. physicskit/statphys/tests/test_sandpile.py +44 -0
  457. physicskit/statphys/tests/test_spin_glass.py +73 -0
  458. physicskit/statphys/tests/test_thermodynamics.py +121 -0
  459. physicskit/statphys/tests/test_xy_model.py +45 -0
  460. physicskit/statphys/utils/__init__.py +1 -0
  461. physicskit/statphys/utils/dynamics.py +100 -0
  462. physicskit/statphys/utils/finite_size_scaling.py +171 -0
  463. physicskit/statphys/utils/landau_theory.py +151 -0
  464. physicskit/statphys/utils/partition_function.py +109 -0
  465. physicskit/statphys/utils/thermodynamics.py +356 -0
  466. physicskit/statphys/visualizers/__init__.py +87 -0
  467. physicskit/statphys/visualizers/interactive.py +161 -0
  468. physicskit/statphys/visualizers/jarzynski_render.py +44 -0
  469. physicskit/statphys/visualizers/kpz_render.py +65 -0
  470. physicskit/statphys/visualizers/lattice_render.py +212 -0
  471. physicskit/statphys/visualizers/particle_render.py +121 -0
  472. physicskit/statphys/visualizers/random_walk_render.py +106 -0
  473. physicskit/statphys/visualizers/rg_render.py +45 -0
  474. physicskit/statphys/visualizers/sandpile_render.py +65 -0
  475. physicskit/statphys/visualizers/spin_glass_render.py +40 -0
  476. physicskit/statphys/visualizers/urn_render.py +38 -0
  477. physicskit/statphys/visualizers/vortex_render.py +84 -0
  478. physicskit/tests/__init__.py +0 -0
  479. physicskit/tests/test_constants.py +38 -0
  480. physicskit-0.1.0.dist-info/METADATA +196 -0
  481. physicskit-0.1.0.dist-info/RECORD +484 -0
  482. physicskit-0.1.0.dist-info/WHEEL +5 -0
  483. physicskit-0.1.0.dist-info/licenses/LICENSE +21 -0
  484. physicskit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1372 @@
1
+ """Matplotlib animation helpers, trajectory color-coding, and interactive Plotly figures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Protocol, cast
6
+
7
+ import matplotlib.pyplot as plt
8
+ import numpy as np
9
+ import plotly.graph_objects as go
10
+ from matplotlib.animation import FuncAnimation
11
+ from matplotlib.axes import Axes
12
+ from matplotlib.collections import LineCollection
13
+ from matplotlib.figure import Figure
14
+ from numpy.typing import ArrayLike, NDArray
15
+
16
+ from physicskit.chaos.core.base_system import BilliardSystem, DiscreteMap
17
+ from physicskit.chaos.systems.continuous import DoublePendulum, DrivenPendulum, MagneticPendulum, RestrictedThreeBody, lagrange_points
18
+ from physicskit.chaos.systems.maps import BakersMap
19
+ from physicskit.chaos.visualizers import theme
20
+
21
+
22
+ class _TrajectorySystem(Protocol):
23
+ """Structural type for a :class:`~physicskit.chaos.core.base_system.DynamicalSystem`
24
+ that also implements its own ``trajectory`` method (every concrete
25
+ continuous system in physicskit.chaos does, but the base class itself does not
26
+ declare it, so it isn't part of ``DynamicalSystem``'s static type)."""
27
+
28
+ dim: int
29
+
30
+ def trajectory(
31
+ self,
32
+ state0: NDArray[np.float64] | None = ...,
33
+ t0: float = ...,
34
+ dt: float = ...,
35
+ n_steps: int = ...,
36
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: ...
37
+
38
+
39
+ def animate_billiard_trajectory(
40
+ billiard: BilliardSystem,
41
+ pos: ArrayLike,
42
+ vel: ArrayLike,
43
+ n_bounces: int = 100,
44
+ interval: int = 50,
45
+ trail: int = 200,
46
+ ) -> FuncAnimation:
47
+ """Animate a trajectory bouncing inside a billiard's boundary.
48
+
49
+ Draws the real-space trajectory (left panel) alongside its
50
+ live-updating Poincare section ``(s, sin phi)`` (right panel).
51
+
52
+ Parameters
53
+ ----------
54
+ billiard : BilliardSystem
55
+ Billiard to animate.
56
+ pos : array_like of float, shape (2,)
57
+ Initial position ``(x, y)``; must lie in the billiard's interior.
58
+ vel : array_like of float, shape (2,)
59
+ Initial velocity direction ``(vx, vy)``; normalized internally.
60
+ n_bounces : int, default 100
61
+ Number of reflections to trace and animate.
62
+ interval : int, default 50
63
+ Delay between animation frames, in milliseconds.
64
+ trail : int, default 200
65
+ Number of most-recent trajectory points to keep visible as a trail.
66
+
67
+ Returns
68
+ -------
69
+ matplotlib.animation.FuncAnimation
70
+ The animation object; assign it to a variable to keep it alive, and
71
+ display it with ``plt.show()`` or save it with ``anim.save(...)``.
72
+ """
73
+ boundary = billiard.boundary_polyline()
74
+ path, result = billiard.trajectory_segments(pos, vel, n_bounces)
75
+ s, sin_phi = result["s"], result["sin_phi"]
76
+
77
+ fig, (ax_space, ax_poincare) = plt.subplots(1, 2, figsize=(12, 5.5))
78
+
79
+ ax_space.plot(boundary[:, 0], boundary[:, 1], color=theme.STRUCTURE, lw=1.5)
80
+ ax_space.set_aspect("equal")
81
+ ax_space.set_title(f"{billiard.__class__.__name__} trajectory")
82
+ (trail_line,) = ax_space.plot([], [], color=theme.ACCENT, lw=0.8)
83
+ (ball,) = ax_space.plot([], [], "o", color=theme.ACCENT, markersize=5)
84
+
85
+ ax_poincare.set_xlim(0.0, billiard.perimeter())
86
+ ax_poincare.set_ylim(-1.0, 1.0)
87
+ ax_poincare.set_xlabel("s")
88
+ ax_poincare.set_ylabel(r"$\sin\varphi$")
89
+ ax_poincare.set_title("Poincare section (live)")
90
+ poincare_scatter = ax_poincare.scatter([], [], s=6, color=theme.PRIMARY)
91
+
92
+ def init() -> tuple:
93
+ trail_line.set_data([], [])
94
+ ball.set_data([], [])
95
+ poincare_scatter.set_offsets(np.empty((0, 2)))
96
+ return trail_line, ball, poincare_scatter
97
+
98
+ def update(frame: int) -> tuple:
99
+ lo = max(0, frame - trail)
100
+ trail_line.set_data(path[lo : frame + 1, 0], path[lo : frame + 1, 1])
101
+ ball.set_data([path[frame, 0]], [path[frame, 1]])
102
+ n_hits = max(0, frame)
103
+ poincare_scatter.set_offsets(np.column_stack([s[:n_hits], sin_phi[:n_hits]]))
104
+ return trail_line, ball, poincare_scatter
105
+
106
+ anim = FuncAnimation(fig, update, frames=path.shape[0], init_func=init, interval=interval, blit=False)
107
+ return anim
108
+
109
+
110
+ def animate_billiard_divergence(
111
+ billiard: BilliardSystem,
112
+ pos: ArrayLike,
113
+ vel: ArrayLike,
114
+ delta_0: float = 1e-8,
115
+ n_bounces: int = 100,
116
+ interval: int = 50,
117
+ trail: int = 200,
118
+ ) -> FuncAnimation:
119
+ """Animate two initially-nearby billiard rays bouncing side by side, diverging.
120
+
121
+ Both rays start at the same position and are launched at angles
122
+ `delta_0` radians apart; watch them bounce together, indistinguishably,
123
+ until the perturbation is amplified enough (a single bounce off a
124
+ defocusing or truncated wall can be all it takes) to send them down
125
+ completely different paths -- sensitive dependence on initial
126
+ conditions, seen directly rather than only in a divergence plot.
127
+
128
+ Parameters
129
+ ----------
130
+ billiard : BilliardSystem
131
+ Billiard to animate both rays within.
132
+ pos : array_like of float, shape (2,)
133
+ Shared initial position ``(x, y)``; must lie in the billiard's
134
+ interior.
135
+ vel : array_like of float, shape (2,)
136
+ Initial velocity direction ``(vx, vy)`` of the reference ray;
137
+ normalized internally.
138
+ delta_0 : float, default 1e-8
139
+ Initial angular perturbation (radians) applied to `vel` to launch
140
+ the second ray.
141
+ n_bounces : int, default 100
142
+ Number of reflections to trace and animate for each ray.
143
+ interval : int, default 50
144
+ Delay between animation frames, in milliseconds.
145
+ trail : int, default 200
146
+ Number of most-recent trajectory points to keep visible as a trail,
147
+ for each ray.
148
+
149
+ Returns
150
+ -------
151
+ matplotlib.animation.FuncAnimation
152
+ The animation object; assign it to a variable to keep it alive, and
153
+ display it with ``plt.show()`` or save it with ``anim.save(...)``.
154
+ """
155
+ vel_ref = np.asarray(vel, dtype=np.float64)
156
+ vel_ref = vel_ref / np.linalg.norm(vel_ref)
157
+ angle = float(np.arctan2(vel_ref[1], vel_ref[0])) + delta_0
158
+ vel_perturbed = np.array([np.cos(angle), np.sin(angle)])
159
+
160
+ boundary = billiard.boundary_polyline()
161
+ path_ref, _ = billiard.trajectory_segments(pos, vel_ref, n_bounces)
162
+ path_pert, _ = billiard.trajectory_segments(pos, vel_perturbed, n_bounces)
163
+
164
+ fig, ax = plt.subplots(figsize=(6.5, 6.5))
165
+ ax.plot(boundary[:, 0], boundary[:, 1], color=theme.STRUCTURE, lw=1.5)
166
+ ax.set_aspect("equal")
167
+ ax.set_title(f"{billiard.__class__.__name__}: two rays {delta_0:.0e} rad apart")
168
+
169
+ (trail_ref,) = ax.plot([], [], color=theme.PRIMARY, lw=0.8)
170
+ (ball_ref,) = ax.plot([], [], "o", color=theme.PRIMARY, markersize=5, label="reference")
171
+ (trail_pert,) = ax.plot([], [], color=theme.ACCENT, lw=0.8)
172
+ (ball_pert,) = ax.plot([], [], "o", color=theme.ACCENT, markersize=5, label="perturbed")
173
+ ax.legend(loc="upper right")
174
+
175
+ def init() -> tuple:
176
+ for artist in (trail_ref, ball_ref, trail_pert, ball_pert):
177
+ artist.set_data([], [])
178
+ return trail_ref, ball_ref, trail_pert, ball_pert
179
+
180
+ def update(frame: int) -> tuple:
181
+ lo = max(0, frame - trail)
182
+ trail_ref.set_data(path_ref[lo : frame + 1, 0], path_ref[lo : frame + 1, 1])
183
+ ball_ref.set_data([path_ref[frame, 0]], [path_ref[frame, 1]])
184
+ trail_pert.set_data(path_pert[lo : frame + 1, 0], path_pert[lo : frame + 1, 1])
185
+ ball_pert.set_data([path_pert[frame, 0]], [path_pert[frame, 1]])
186
+ return trail_ref, ball_ref, trail_pert, ball_pert
187
+
188
+ anim = FuncAnimation(fig, update, frames=path_ref.shape[0], init_func=init, interval=interval, blit=False)
189
+ return anim
190
+
191
+
192
+ def plotly_billiard_trajectory(billiard: BilliardSystem, pos: ArrayLike, vel: ArrayLike, n_bounces: int = 100) -> go.Figure:
193
+ """Build an interactive Plotly figure of a trajectory inside a billiard.
194
+
195
+ Parameters
196
+ ----------
197
+ billiard : BilliardSystem
198
+ Billiard to trace the trajectory within.
199
+ pos : array_like of float, shape (2,)
200
+ Initial position ``(x, y)``; must lie in the billiard's interior.
201
+ vel : array_like of float, shape (2,)
202
+ Initial velocity direction ``(vx, vy)``; normalized internally.
203
+ n_bounces : int, default 100
204
+ Number of reflections to trace.
205
+
206
+ Returns
207
+ -------
208
+ plotly.graph_objects.Figure
209
+ Interactive figure with the boundary and trajectory as line traces.
210
+ """
211
+ boundary = billiard.boundary_polyline()
212
+ path, _ = billiard.trajectory_segments(pos, vel, n_bounces)
213
+
214
+ fig = go.Figure()
215
+ fig.add_trace(
216
+ go.Scatter(
217
+ x=boundary[:, 0],
218
+ y=boundary[:, 1],
219
+ mode="lines",
220
+ line={"color": theme.STRUCTURE, "width": 2},
221
+ name="boundary",
222
+ )
223
+ )
224
+ fig.add_trace(
225
+ go.Scatter(
226
+ x=path[:, 0],
227
+ y=path[:, 1],
228
+ mode="lines",
229
+ line={"color": theme.ACCENT, "width": 1},
230
+ name="trajectory",
231
+ )
232
+ )
233
+ fig.update_yaxes(scaleanchor="x", scaleratio=1)
234
+ fig.update_layout(
235
+ title=f"{billiard.__class__.__name__} trajectory",
236
+ xaxis_title="x",
237
+ yaxis_title="y",
238
+ )
239
+ return fig
240
+
241
+
242
+ def plotly_poincare_section(billiard: BilliardSystem, n_rays: int = 50, n_bounces: int = 200, seed: int | None = None) -> go.Figure:
243
+ """Build an interactive Plotly scatter of a billiard's boundary phase space.
244
+
245
+ Parameters
246
+ ----------
247
+ billiard : BilliardSystem
248
+ Billiard to sample.
249
+ n_rays : int, default 50
250
+ Number of independent trajectories to launch.
251
+ n_bounces : int, default 200
252
+ Number of reflections to trace per trajectory.
253
+ seed : int, optional
254
+ Seed for the random angle generator, for reproducibility.
255
+
256
+ Returns
257
+ -------
258
+ plotly.graph_objects.Figure
259
+ Interactive scatter figure of ``(s, sin phi)`` boundary hits.
260
+ """
261
+ rng = np.random.default_rng(seed)
262
+ interior = billiard.sample_interior_point()
263
+ angles = rng.uniform(0.0, 2.0 * np.pi, size=n_rays)
264
+ result = billiard.simulate_many_rays(interior, angles, n_bounces)
265
+ s, sin_phi = result["s"], result["sin_phi"]
266
+
267
+ fig = go.Figure(
268
+ go.Scattergl(
269
+ x=s,
270
+ y=sin_phi,
271
+ mode="markers",
272
+ marker={"size": 3, "color": theme.PRIMARY, "opacity": 0.6},
273
+ )
274
+ )
275
+ fig.update_layout(
276
+ title=f"{billiard.__class__.__name__} Poincare section",
277
+ xaxis_title="s (boundary arclength)",
278
+ yaxis_title="sin(phi)",
279
+ yaxis_range=[-1.0, 1.0],
280
+ xaxis_range=[0.0, billiard.perimeter()],
281
+ )
282
+ return fig
283
+
284
+
285
+ def plot_colored_trajectory(
286
+ x: ArrayLike,
287
+ y: ArrayLike,
288
+ values: ArrayLike,
289
+ ax: Axes | None = None,
290
+ cmap: str = theme.SEQUENTIAL_CMAP,
291
+ colorbar: bool = True,
292
+ colorbar_label: str = "",
293
+ **line_kwargs: Any,
294
+ ) -> tuple[Figure, Axes]:
295
+ """Plot a 2D path colored segment-by-segment by an arbitrary scalar.
296
+
297
+ Useful for highlighting where a trajectory is "interesting" -- e.g. a
298
+ billiard trajectory colored by bounce index (to see how quickly it
299
+ explores the table), or a continuous trajectory colored by local speed or
300
+ an instantaneous divergence-rate estimate.
301
+
302
+ Parameters
303
+ ----------
304
+ x, y : array_like of float, shape (n,)
305
+ Path coordinates.
306
+ values : array_like of float, shape (n,)
307
+ Scalar value at each point, used to color the path; each line
308
+ segment between consecutive points is colored by the value at its
309
+ starting point.
310
+ ax : matplotlib.axes.Axes, optional
311
+ Axes to draw into; a new figure and axes are created if omitted.
312
+ cmap : str, default "viridis"
313
+ Colormap name.
314
+ colorbar : bool, default True
315
+ Whether to add a colorbar.
316
+ colorbar_label : str, default ""
317
+ Label for the colorbar.
318
+ **line_kwargs
319
+ Additional keyword arguments forwarded to
320
+ ``matplotlib.collections.LineCollection`` (e.g. ``linewidth``).
321
+
322
+ Returns
323
+ -------
324
+ fig : matplotlib.figure.Figure
325
+ ax : matplotlib.axes.Axes
326
+ """
327
+ x = np.asarray(x, dtype=np.float64)
328
+ y = np.asarray(y, dtype=np.float64)
329
+ values = np.asarray(values, dtype=np.float64)
330
+
331
+ points = np.column_stack([x, y]).reshape(-1, 1, 2)
332
+ segments = np.concatenate([points[:-1], points[1:]], axis=1)
333
+
334
+ if ax is None:
335
+ fig, ax = plt.subplots(figsize=(7, 6))
336
+ else:
337
+ fig = cast(Figure, ax.figure)
338
+
339
+ kwargs: dict[str, Any] = {"linewidth": 1.5}
340
+ kwargs.update(line_kwargs)
341
+ line_collection = LineCollection(cast(Any, segments), cmap=cmap, **kwargs)
342
+ line_collection.set_array(values[:-1])
343
+ ax.add_collection(line_collection)
344
+ ax.set_xlim(x.min(), x.max())
345
+ ax.set_ylim(y.min(), y.max())
346
+
347
+ if colorbar:
348
+ fig.colorbar(line_collection, ax=ax, label=colorbar_label)
349
+
350
+ return fig, ax
351
+
352
+
353
+ def plotly_3d_trajectory(
354
+ states: ArrayLike,
355
+ labels: tuple[str, str, str] = ("x", "y", "z"),
356
+ color_by: ArrayLike | None = None,
357
+ colorbar_label: str = "",
358
+ title: str | None = None,
359
+ ) -> go.Figure:
360
+ """Build an interactive, orbit/pan/zoom-able 3D Plotly trajectory figure.
361
+
362
+ Parameters
363
+ ----------
364
+ states : array_like of float, shape (n, 3)
365
+ 3D trajectory points (e.g. Lorenz/Rossler/Chua state history).
366
+ labels : tuple of str, default ("x", "y", "z")
367
+ Axis labels.
368
+ color_by : array_like of float, shape (n,), optional
369
+ Scalar value at each point used to color the trajectory (e.g. local
370
+ speed, or time); a single solid color is used if omitted.
371
+ colorbar_label : str, default ""
372
+ Label for the colorbar, if `color_by` is given.
373
+ title : str, optional
374
+ Figure title.
375
+
376
+ Returns
377
+ -------
378
+ plotly.graph_objects.Figure
379
+ """
380
+ states = np.asarray(states, dtype=np.float64)
381
+
382
+ if color_by is None:
383
+ line = {"color": theme.PRIMARY, "width": 2}
384
+ else:
385
+ line = {
386
+ "color": np.asarray(color_by, dtype=np.float64),
387
+ "width": 3,
388
+ "colorscale": "Viridis",
389
+ "colorbar": {"title": colorbar_label},
390
+ }
391
+
392
+ fig = go.Figure(
393
+ go.Scatter3d(
394
+ x=states[:, 0],
395
+ y=states[:, 1],
396
+ z=states[:, 2],
397
+ mode="lines",
398
+ line=line,
399
+ )
400
+ )
401
+ fig.update_layout(
402
+ title=title,
403
+ scene={
404
+ "xaxis_title": labels[0],
405
+ "yaxis_title": labels[1],
406
+ "zaxis_title": labels[2],
407
+ },
408
+ )
409
+ return fig
410
+
411
+
412
+ def animate_bakers_map(
413
+ system: BakersMap,
414
+ n_points: int = 10000,
415
+ n_iterations: int = 20,
416
+ frames_per_iteration: int = 15,
417
+ interval: int = 60,
418
+ stripe_colors: tuple[str, str] = (theme.PRIMARY, theme.ACCENT),
419
+ seed: int | None = 0,
420
+ ) -> FuncAnimation:
421
+ """Animate the baker's map's "stretch, cut, and stack" action, in three
422
+ explicit phases per iteration.
423
+
424
+ Starts from a regular grid of square markers, colored in exactly two
425
+ vertical bands (left half / right half) -- which, for the default
426
+ ``alpha = 0.5``, lines up with the map's own cut point, so the first
427
+ cut slices exactly along the original color boundary. Each iteration is
428
+ then animated as three distinct, visually separated phases rather than
429
+ one smooth blend, so the mechanism itself is legible:
430
+
431
+ 1. **Stretch** -- the square is squashed vertically by a factor
432
+ ``alpha`` and stretched horizontally onto ``[0, 2)``: the left branch
433
+ (``x < alpha``) stretches onto ``[0, 1)`` in place, and the right
434
+ branch (``x >= alpha``) stretches onto its own ``[0, 1)`` slot, shown
435
+ offset to ``[1, 2)`` so the two pieces don't overlap mid-flight.
436
+ 2. **Cut** -- a brief pause with a dashed guide line at ``x = 1``
437
+ marking where the stretched strip is about to be sliced in two.
438
+ 3. **Stack** -- the right-hand piece slides left by 1 and up onto
439
+ ``y in [alpha, 1)``, landing exactly on top of the left piece, which
440
+ has not moved since the stretch phase. The square is whole again, now
441
+ twice as finely interleaved.
442
+
443
+ Because every marker keeps its original color, the two bands are seen
444
+ getting sliced and restacked into progressively thinner, more numerous
445
+ stripes -- and because the whole grid is present from frame 0, the
446
+ increasing fineness of that interleaving (the mixing) is directly
447
+ visible, iteration after iteration.
448
+
449
+ Parameters
450
+ ----------
451
+ system : BakersMap
452
+ Map to animate.
453
+ n_points : int, default 10000
454
+ Approximate number of square markers: laid out on a regular
455
+ ``round(sqrt(n_points))``-by-``round(sqrt(n_points))`` grid so they
456
+ tile the unit square with no gaps at the start.
457
+ n_iterations : int, default 20
458
+ Number of map iterations to animate through.
459
+ frames_per_iteration : int, default 15
460
+ Number of frames used to animate each iteration's stretch, cut, and
461
+ stack phases combined.
462
+ interval : int, default 60
463
+ Delay between animation frames, in milliseconds.
464
+ stripe_colors : tuple of str, default (theme.PRIMARY, theme.ACCENT)
465
+ Colors for the left half and right half of the initial grid,
466
+ respectively.
467
+ seed : int, optional
468
+ Seed for a small random jitter applied to the grid positions. A
469
+ perfectly regular grid sits at exact dyadic rationals, which this
470
+ map's doubling action on the ``x`` coordinate degenerates in very
471
+ few iterations (the same finite-precision collapse documented on
472
+ :class:`~physicskit.chaos.systems.maps.BakersMap`); a tiny jitter, much
473
+ smaller than the grid spacing, avoids that without visibly
474
+ disturbing the tiling.
475
+
476
+ Returns
477
+ -------
478
+ matplotlib.animation.FuncAnimation
479
+ Assign it to a variable to keep it alive, and display it with
480
+ ``plt.show()`` or save it with ``anim.save(...)``.
481
+ """
482
+ alpha = system.alpha
483
+ n_side = max(2, round(np.sqrt(n_points)))
484
+ centers = (np.arange(n_side) + 0.5) / n_side
485
+ grid_x, grid_y = np.meshgrid(centers, centers)
486
+ rng = np.random.default_rng(seed)
487
+ jitter = rng.uniform(-0.15, 0.15, size=(2,) + grid_x.shape) / n_side
488
+ x0 = np.clip(grid_x + jitter[0], 0.0, 1.0 - 1e-12).ravel()
489
+ y0 = np.clip(grid_y + jitter[1], 0.0, 1.0 - 1e-12).ravel()
490
+
491
+ colors = np.where(x0 < 0.5, stripe_colors[0], stripe_colors[1])
492
+
493
+ fig, ax = plt.subplots(figsize=(8.0, 5.0))
494
+ ax.set_xlim(-0.05, 2.05)
495
+ ax.set_ylim(-0.05, 1.05)
496
+ ax.set_aspect("equal")
497
+ ax.set_xlabel("x")
498
+ ax.set_ylabel("y")
499
+ ax.add_patch(plt.Rectangle((0.0, 0.0), 1.0, 1.0, fill=False, edgecolor=theme.STRUCTURE, lw=1.2, zorder=3))
500
+ cut_line = ax.axvline(1.0, color=theme.STRUCTURE, ls="--", lw=1.2, zorder=3, visible=False)
501
+ title = ax.set_title("Baker's map: stretch (iteration 0 -> 1)")
502
+
503
+ # Marker size is chosen once, from the (fixed) data-to-pixel scale, so
504
+ # that markers exactly tile the resting unit square without gaps or
505
+ # overlaps -- `scatter`'s `s` is in fixed points^2, not data units, so
506
+ # this only works because the axis limits never change during the
507
+ # animation (a zooming camera would desync marker size from grid spacing).
508
+ fig.canvas.draw()
509
+ p0 = ax.transData.transform((0.0, 0.0))
510
+ p1 = ax.transData.transform((1.0 / n_side, 0.0))
511
+ marker_points = (p1[0] - p0[0]) * 72.0 / fig.dpi
512
+ scatter = ax.scatter(x0, y0, s=marker_points**2, c=colors, marker="s", linewidths=0, zorder=2)
513
+
514
+ # Frame budget within each iteration: a stretch phase, a single-frame
515
+ # pause on the cut, and a stack phase.
516
+ n_stretch = max(1, round(0.45 * frames_per_iteration))
517
+ n_pause = 1 if frames_per_iteration >= 3 else 0
518
+ n_stack = max(1, frames_per_iteration - n_stretch - n_pause)
519
+ frames_per_iteration = n_stretch + n_pause + n_stack
520
+ total_frames = n_iterations * frames_per_iteration
521
+
522
+ # `state` holds the grid's *resting* (post-stack) positions, carried over
523
+ # between iterations; the phase-1 and final targets for the iteration in
524
+ # progress are derived from it once, at that iteration's first sub-frame,
525
+ # and held fixed while sub-frames interpolate between them.
526
+ state: dict[str, Any] = {"x": x0.copy(), "y": y0.copy()}
527
+
528
+ def update(frame: int) -> tuple:
529
+ it, sub = divmod(frame, frames_per_iteration)
530
+ if sub == 0:
531
+ rx, ry = state["x"], state["y"]
532
+ left = rx < alpha
533
+ # Stretch target: left branch lands on its true final position
534
+ # directly; right branch stretches onto its own [0, 1) slot,
535
+ # offset to [1, 2) so it doesn't overlap the left branch.
536
+ x_stretch = np.where(left, rx / alpha, (rx - alpha) / (1.0 - alpha) + 1.0)
537
+ y_stretch = alpha * ry
538
+ x_final = np.where(left, rx / alpha, (rx - alpha) / (1.0 - alpha))
539
+ y_final = np.where(left, alpha * ry, alpha + (1.0 - alpha) * ry)
540
+ state["left"] = left
541
+ state["x_stretch"], state["y_stretch"] = x_stretch, y_stretch
542
+ state["x_final"], state["y_final"] = x_final, y_final
543
+ state["x_rest"], state["y_rest"] = rx, ry
544
+
545
+ left = state["left"]
546
+ rx, ry = state["x_rest"], state["y_rest"]
547
+ x_stretch, y_stretch = state["x_stretch"], state["y_stretch"]
548
+ x_final, y_final = state["x_final"], state["y_final"]
549
+
550
+ if sub < n_stretch:
551
+ t = (sub + 1) / n_stretch
552
+ x = rx + t * (x_stretch - rx)
553
+ y = ry + t * (y_stretch - ry)
554
+ cut_line.set_visible(True)
555
+ phase = "stretch"
556
+ elif sub < n_stretch + n_pause:
557
+ x, y = x_stretch, y_stretch
558
+ cut_line.set_visible(True)
559
+ phase = "cut"
560
+ else:
561
+ s = sub - n_stretch - n_pause
562
+ t = (s + 1) / n_stack
563
+ x = np.where(left, x_final, x_stretch + t * (x_final - x_stretch))
564
+ y = np.where(left, y_final, y_stretch + t * (y_final - y_stretch))
565
+ cut_line.set_visible(False)
566
+ phase = "stack"
567
+
568
+ scatter.set_offsets(np.column_stack([x, y]))
569
+ title.set_text(f"Baker's map: {phase} (iteration {it} -> {it + 1})")
570
+ if sub == frames_per_iteration - 1:
571
+ state["x"], state["y"] = x_final, y_final
572
+ return scatter, title, cut_line
573
+
574
+ anim = FuncAnimation(fig, update, frames=total_frames, interval=interval, blit=False)
575
+ return anim
576
+
577
+
578
+ def animate_map_orbit(
579
+ system: DiscreteMap,
580
+ state0: ArrayLike | None = None,
581
+ n_iter: int = 4000,
582
+ points_per_frame: int = 8,
583
+ interval: int = 40,
584
+ color: str = theme.PRIMARY,
585
+ marker_size: float = 3.0,
586
+ labels: tuple[str, str] = ("x", "y"),
587
+ title: str | None = None,
588
+ ) -> FuncAnimation:
589
+ """Animate a 2D map's orbit being built up, iteration by iteration.
590
+
591
+ Useful for watching a strange attractor (e.g. the Henon map's) emerge
592
+ from a single seed point, rather than seeing only the finished scatter.
593
+
594
+ Parameters
595
+ ----------
596
+ system : DiscreteMap
597
+ Map to iterate; must have ``dim == 2``.
598
+ state0 : array_like of float, shape (2,), optional
599
+ Initial state; defaults to ``system.initial_state()``.
600
+ n_iter : int, default 4000
601
+ Total number of iterations to animate.
602
+ points_per_frame : int, default 8
603
+ Number of new orbit points revealed per animation frame.
604
+ interval : int, default 40
605
+ Delay between animation frames, in milliseconds.
606
+ color : str, default theme.PRIMARY
607
+ Marker color.
608
+ marker_size : float, default 3.0
609
+ Scatter marker size.
610
+ labels : tuple of str, default ("x", "y")
611
+ Axis labels.
612
+ title : str, optional
613
+ Axes title; defaults to ``"<ClassName> orbit"``.
614
+
615
+ Returns
616
+ -------
617
+ matplotlib.animation.FuncAnimation
618
+ """
619
+ state0 = system.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
620
+ traj = system.trajectory(state0, n_iter=n_iter)
621
+
622
+ fig, ax = plt.subplots(figsize=(7, 6))
623
+ pad_x = 0.05 * (traj[:, 0].max() - traj[:, 0].min() + 1e-12)
624
+ pad_y = 0.05 * (traj[:, 1].max() - traj[:, 1].min() + 1e-12)
625
+ ax.set_xlim(traj[:, 0].min() - pad_x, traj[:, 0].max() + pad_x)
626
+ ax.set_ylim(traj[:, 1].min() - pad_y, traj[:, 1].max() + pad_y)
627
+ ax.set_xlabel(labels[0])
628
+ ax.set_ylabel(labels[1])
629
+ ax.set_title(title or f"{system.__class__.__name__} orbit")
630
+
631
+ scatter = ax.scatter([], [], s=marker_size, color=color, alpha=0.6)
632
+
633
+ n_frames = int(np.ceil(traj.shape[0] / points_per_frame))
634
+
635
+ def update(frame: int) -> tuple:
636
+ end = min(traj.shape[0], (frame + 1) * points_per_frame)
637
+ scatter.set_offsets(traj[:end])
638
+ return (scatter,)
639
+
640
+ anim = FuncAnimation(fig, update, frames=n_frames, interval=interval, blit=False)
641
+ return anim
642
+
643
+
644
+ def animate_multi_orbit_map(
645
+ trajectories: list[ArrayLike],
646
+ interval: int = 60,
647
+ colors: list[Any] | None = None,
648
+ xlim: tuple[float, float] | None = None,
649
+ ylim: tuple[float, float] | None = None,
650
+ labels: tuple[str, str] = ("x", "y"),
651
+ title: str | None = None,
652
+ marker_size: float = 2.0,
653
+ n_frames: int = 100,
654
+ ) -> FuncAnimation:
655
+ """Animate a family of 2D orbits growing together, iteration by iteration.
656
+
657
+ Useful for a phase-space portrait built from many initial conditions
658
+ (e.g. the standard map's islands-in-a-chaotic-sea picture): every orbit
659
+ grows one new point per frame (all together, not one whole orbit at a
660
+ time), so the picture keeps visibly filling in throughout the animation
661
+ rather than mostly settling within the first few frames.
662
+
663
+ Parameters
664
+ ----------
665
+ trajectories : list of array_like of float, each shape (n_i, 2)
666
+ Orbits to grow together; need not all have the same length (each
667
+ stops advancing once it runs out of points).
668
+ interval : int, default 60
669
+ Delay between animation frames, in milliseconds.
670
+ colors : list, optional
671
+ Per-orbit colors; a `theme.SEQUENTIAL_CMAP` sweep is used if omitted.
672
+ xlim, ylim : tuple of float, optional
673
+ Axis limits; inferred from the data if omitted.
674
+ labels : tuple of str, default ("x", "y")
675
+ Axis labels.
676
+ title : str, optional
677
+ Axes title, updated each frame with the running iteration count.
678
+ marker_size : float, default 2.0
679
+ Marker size for each orbit's points (uses a real, visibly-sized
680
+ marker, unlike Matplotlib's 1-device-pixel ``","`` marker which
681
+ ignores this parameter entirely).
682
+ n_frames : int, default 100
683
+ Number of animation frames spanning every orbit's full length; each
684
+ frame reveals ``1 / n_frames`` of the points, evenly paced so growth
685
+ stays visible for the whole animation instead of being front-loaded.
686
+
687
+ Returns
688
+ -------
689
+ matplotlib.animation.FuncAnimation
690
+ """
691
+ traj_arrays: list[NDArray[np.float64]] = [np.asarray(t, dtype=np.float64) for t in trajectories]
692
+ n_orbits = len(traj_arrays)
693
+ max_len = max(t.shape[0] for t in traj_arrays)
694
+
695
+ fig, ax = plt.subplots(figsize=(7, 6.5))
696
+ if xlim is None or ylim is None:
697
+ all_pts = np.concatenate(traj_arrays, axis=0)
698
+ xlim = xlim or (float(all_pts[:, 0].min()), float(all_pts[:, 0].max()))
699
+ ylim = ylim or (float(all_pts[:, 1].min()), float(all_pts[:, 1].max()))
700
+ ax.set_xlim(*xlim)
701
+ ax.set_ylim(*ylim)
702
+ ax.set_xlabel(labels[0])
703
+ ax.set_ylabel(labels[1])
704
+
705
+ cmap = plt.get_cmap(theme.SEQUENTIAL_CMAP)
706
+ orbit_colors = [cmap(i / max(1, n_orbits - 1)) for i in range(n_orbits)] if colors is None else list(colors)
707
+
708
+ lines = [ax.plot([], [], ".", color=orbit_colors[i], alpha=0.7, markersize=marker_size, lw=0)[0] for i in range(n_orbits)]
709
+
710
+ frame_ends = np.linspace(1, max_len, n_frames).astype(int)
711
+
712
+ def update(frame: int) -> tuple:
713
+ end = int(frame_ends[frame])
714
+ for line, traj in zip(lines, traj_arrays):
715
+ stop = min(end, traj.shape[0])
716
+ line.set_data(traj[:stop, 0], traj[:stop, 1])
717
+ if title:
718
+ ax.set_title(f"{title} (iteration {end}/{max_len})")
719
+ return tuple(lines)
720
+
721
+ anim = FuncAnimation(fig, update, frames=n_frames, interval=interval, blit=False)
722
+ return anim
723
+
724
+
725
+ def animate_map_cobweb(
726
+ system: DiscreteMap,
727
+ x0: float | None = None,
728
+ n_iter: int = 30,
729
+ interval: int = 400,
730
+ x_range: tuple[float, float] = (0.0, 1.0),
731
+ n_curve: int = 400,
732
+ title: str | None = None,
733
+ ) -> FuncAnimation:
734
+ """Animate a cobweb (staircase) diagram for a 1D map ``x' = f(x)``.
735
+
736
+ Alternates vertical jumps (up to the curve, i.e. computing ``f(x)``) with
737
+ horizontal jumps (back down to the diagonal ``y = x``, i.e. feeding that
738
+ value back in as the next ``x``) -- the classic way to see fixed points,
739
+ cycles, and chaos geometrically for a 1D map.
740
+
741
+ Parameters
742
+ ----------
743
+ system : DiscreteMap
744
+ Map to iterate; must have ``dim == 1``.
745
+ x0 : float, optional
746
+ Starting point; defaults to ``system.initial_state()[0]``.
747
+ n_iter : int, default 30
748
+ Number of map iterations (staircase steps) to animate.
749
+ interval : int, default 400
750
+ Delay between animation frames, in milliseconds.
751
+ x_range : tuple of float, default (0.0, 1.0)
752
+ Range over which to plot the map curve ``f(x)`` and diagonal.
753
+ n_curve : int, default 400
754
+ Number of points used to draw the smooth ``f(x)`` curve.
755
+ title : str, optional
756
+ Axes title.
757
+
758
+ Returns
759
+ -------
760
+ matplotlib.animation.FuncAnimation
761
+ """
762
+ x0 = float(system.initial_state()[0]) if x0 is None else float(x0)
763
+
764
+ xs_curve = np.linspace(x_range[0], x_range[1], n_curve)
765
+ ys_curve = np.array([system.step(np.array([xv]))[0] for xv in xs_curve])
766
+
767
+ fig, ax = plt.subplots(figsize=(6.5, 6.5))
768
+ ax.plot(xs_curve, ys_curve, color=theme.PRIMARY, lw=1.5, label="f(x)")
769
+ ax.plot(x_range, x_range, color=theme.MUTED, lw=1.0, ls="--", label="y = x")
770
+ ax.set_xlim(*x_range)
771
+ ax.set_ylim(*x_range)
772
+ ax.set_aspect("equal")
773
+ ax.set_xlabel("x")
774
+ ax.set_ylabel("f(x)")
775
+ ax.set_title(title or f"{system.__class__.__name__} cobweb diagram")
776
+ ax.legend(loc="upper left")
777
+
778
+ (web,) = ax.plot([], [], color=theme.ACCENT, lw=1.0)
779
+ (point,) = ax.plot([], [], "o", color=theme.ACCENT, markersize=5)
780
+
781
+ web_x = [x0]
782
+ web_y = [x0]
783
+ x = x0
784
+ for _ in range(n_iter):
785
+ fx = float(system.step(np.array([x]))[0])
786
+ web_x += [x, fx]
787
+ web_y += [fx, fx]
788
+ x = fx
789
+ web_x_arr = np.array(web_x)
790
+ web_y_arr = np.array(web_y)
791
+
792
+ def update(frame: int) -> tuple:
793
+ end = min(len(web_x_arr), 2 * frame + 1)
794
+ web.set_data(web_x_arr[:end], web_y_arr[:end])
795
+ point.set_data([web_x_arr[end - 1]], [web_y_arr[end - 1]])
796
+ return web, point
797
+
798
+ anim = FuncAnimation(fig, update, frames=n_iter + 1, interval=interval, blit=False)
799
+ return anim
800
+
801
+
802
+ def animate_trajectory_3d(
803
+ states: ArrayLike,
804
+ labels: tuple[str, str, str] = ("x", "y", "z"),
805
+ interval: int = 50,
806
+ skip: int = 1,
807
+ trail: int | None = None,
808
+ color: str = theme.PRIMARY,
809
+ title: str | None = None,
810
+ ) -> FuncAnimation:
811
+ """Animate a 3D trajectory being traced out over time.
812
+
813
+ Draws the path as a growing line with a bright marker at its leading
814
+ edge -- e.g. watching the Lorenz butterfly or a Rossler/Chua scroll being
815
+ drawn stroke by stroke, rather than seeing only the finished attractor.
816
+
817
+ Parameters
818
+ ----------
819
+ states : array_like of float, shape (n, 3)
820
+ Trajectory to animate (e.g. from
821
+ :meth:`~physicskit.chaos.core.base_system.DynamicalSystem.trajectory`).
822
+ labels : tuple of str, default ("x", "y", "z")
823
+ Axis labels.
824
+ interval : int, default 50
825
+ Delay between animation frames, in milliseconds.
826
+ skip : int, default 1
827
+ Number of trajectory points advanced per animation frame; increase
828
+ for long trajectories so the animation covers them in a reasonable
829
+ number of frames.
830
+ trail : int, optional
831
+ Number of most-recent points to keep visible; the whole path-so-far
832
+ is kept if omitted.
833
+ color : str, default theme.PRIMARY
834
+ Line and marker color.
835
+ title : str, optional
836
+ Axes title.
837
+
838
+ Returns
839
+ -------
840
+ matplotlib.animation.FuncAnimation
841
+ """
842
+ states = np.asarray(states, dtype=np.float64)
843
+ n = states.shape[0]
844
+
845
+ fig = plt.figure(figsize=(7, 6))
846
+ ax = fig.add_subplot(projection="3d")
847
+ ax.set_xlim(states[:, 0].min(), states[:, 0].max())
848
+ ax.set_ylim(states[:, 1].min(), states[:, 1].max())
849
+ ax.set_zlim(states[:, 2].min(), states[:, 2].max())
850
+ ax.set_xlabel(labels[0])
851
+ ax.set_ylabel(labels[1])
852
+ ax.set_zlabel(labels[2])
853
+ if title:
854
+ ax.set_title(title)
855
+
856
+ (line,) = ax.plot([], [], [], color=color, lw=0.8)
857
+ (head,) = ax.plot([], [], [], "o", color=theme.ACCENT, markersize=5)
858
+
859
+ frame_ends = np.arange(skip, n + skip, skip)
860
+ frame_ends[-1] = n
861
+
862
+ def update(frame: int) -> tuple:
863
+ end = int(frame_ends[frame])
864
+ lo = 0 if trail is None else max(0, end - trail)
865
+ line.set_data_3d(states[lo:end, 0], states[lo:end, 1], states[lo:end, 2])
866
+ head.set_data_3d([states[end - 1, 0]], [states[end - 1, 1]], [states[end - 1, 2]])
867
+ return line, head
868
+
869
+ anim = FuncAnimation(fig, update, frames=len(frame_ends), interval=interval, blit=False)
870
+ return anim
871
+
872
+
873
+ def animate_phase_volume_contraction(
874
+ system: _TrajectorySystem,
875
+ center_state: ArrayLike | None = None,
876
+ ball_radius: float = 3.0,
877
+ n_points: int = 800,
878
+ t_max: float = 8.0,
879
+ dt: float = 0.01,
880
+ n_frames: int = 150,
881
+ interval: int = 50,
882
+ warmup_steps: int = 500,
883
+ seed: int | None = 0,
884
+ ) -> FuncAnimation:
885
+ """Animate a small ball of nearby states collapsing under a dissipative 3D flow.
886
+
887
+ Liouville's theorem (``d(log V)/dt = trace(Jacobian)``) is usually
888
+ illustrated by a single number -- the cumulative log-volume of an
889
+ infinitesimal ball, computed along one trajectory (see
890
+ :func:`~physicskit.chaos.utils.metrics.phase_volume_expansion`). This animates
891
+ what that number actually *means*: a literal ball of `n_points` nearby
892
+ initial conditions, each integrated forward independently, visibly
893
+ flattening from a sphere into a thin sheet draped over the attractor --
894
+ the geometric mechanism, for a dissipative system, behind both volume
895
+ contraction and the emergence of a lower-dimensional (often fractal)
896
+ attractor.
897
+
898
+ Parameters
899
+ ----------
900
+ system : DynamicalSystem
901
+ System to integrate; must have ``dim == 3``.
902
+ center_state : array_like of float, shape (3,), optional
903
+ Center of the initial ball. If omitted, a short `warmup_steps`
904
+ integration from ``system.initial_state()`` is used to land near the
905
+ attractor first, so the ball starts already collapsing onto it
906
+ rather than spending time drifting there.
907
+ ball_radius : float, default 3.0
908
+ Radius of the initial ball of points.
909
+ n_points : int, default 800
910
+ Number of points sampled uniformly inside the ball.
911
+ t_max : float, default 8.0
912
+ Total integration time for every point.
913
+ dt : float, default 0.01
914
+ Integration step size.
915
+ n_frames : int, default 150
916
+ Number of animation frames, evenly spaced in time over ``[0, t_max]``.
917
+ interval : int, default 50
918
+ Delay between animation frames, in milliseconds.
919
+ warmup_steps : int, default 500
920
+ Number of integration steps used to find a default `center_state`
921
+ near the attractor; ignored if `center_state` is given.
922
+ seed : int, optional
923
+ Seed for the random point sampler, for reproducibility.
924
+
925
+ Returns
926
+ -------
927
+ matplotlib.animation.FuncAnimation
928
+ Assign it to a variable to keep it alive, and display it with
929
+ ``plt.show()`` or save it with ``anim.save(...)``.
930
+
931
+ Raises
932
+ ------
933
+ ValueError
934
+ If ``system.dim != 3``.
935
+ """
936
+ if system.dim != 3:
937
+ raise ValueError("animate_phase_volume_contraction only supports 3D systems")
938
+
939
+ if center_state is None:
940
+ _, warmup = system.trajectory(n_steps=warmup_steps, dt=dt)
941
+ center = warmup[-1]
942
+ else:
943
+ center = np.asarray(center_state, dtype=np.float64)
944
+
945
+ rng = np.random.default_rng(seed)
946
+ directions = rng.normal(size=(n_points, 3))
947
+ directions /= np.linalg.norm(directions, axis=1, keepdims=True)
948
+ radii = ball_radius * rng.uniform(size=n_points) ** (1.0 / 3.0)
949
+ points0 = center + directions * radii[:, None]
950
+
951
+ n_steps = round(t_max / dt)
952
+ all_states = np.empty((n_points, n_steps + 1, 3))
953
+ for i in range(n_points):
954
+ _, states = system.trajectory(state0=points0[i], dt=dt, n_steps=n_steps)
955
+ all_states[i] = states
956
+
957
+ frame_indices = np.linspace(0, n_steps, n_frames).astype(int)
958
+
959
+ fig = plt.figure(figsize=(7, 6))
960
+ ax = fig.add_subplot(projection="3d")
961
+ flat = all_states.reshape(-1, 3)
962
+ ax.set_xlim(flat[:, 0].min(), flat[:, 0].max())
963
+ ax.set_ylim(flat[:, 1].min(), flat[:, 1].max())
964
+ ax.set_zlim(flat[:, 2].min(), flat[:, 2].max())
965
+ ax.set_xlabel("x")
966
+ ax.set_ylabel("y")
967
+ ax.set_zlabel("z")
968
+
969
+ scatter = ax.scatter(points0[:, 0], points0[:, 1], points0[:, 2], s=4, color=theme.ACCENT, alpha=0.6)
970
+ title = ax.set_title(f"{system.__class__.__name__}: phase-space volume contracting (t=0.00)")
971
+
972
+ def update(frame: int) -> tuple:
973
+ idx = int(frame_indices[frame])
974
+ pts = all_states[:, idx, :]
975
+ scatter._offsets3d = (pts[:, 0], pts[:, 1], pts[:, 2])
976
+ title.set_text(f"{system.__class__.__name__}: phase-space volume contracting (t={idx * dt:.2f})")
977
+ return scatter, title
978
+
979
+ anim = FuncAnimation(fig, update, frames=n_frames, interval=interval, blit=False)
980
+ return anim
981
+
982
+
983
+ def animate_trajectory_2d(
984
+ states: ArrayLike,
985
+ labels: tuple[str, str] = ("x", "y"),
986
+ interval: int = 50,
987
+ skip: int = 1,
988
+ trail: int | None = None,
989
+ color: str = theme.PRIMARY,
990
+ markers: list[dict[str, Any]] | None = None,
991
+ equal_aspect: bool = False,
992
+ title: str | None = None,
993
+ ) -> FuncAnimation:
994
+ """Animate a 2D trajectory being traced out over time.
995
+
996
+ Draws the path as a growing line with a bright marker at its leading
997
+ edge, e.g. a Duffing oscillator's phase portrait ``(x, v)`` unspooling or
998
+ a restricted-three-body orbit looping around its primaries.
999
+
1000
+ Parameters
1001
+ ----------
1002
+ states : array_like of float, shape (n, 2)
1003
+ Trajectory to animate; only the first two columns are used, so a
1004
+ higher-dimensional state (e.g. ``(x, y, vx, vy)``) can be passed
1005
+ directly to plot its first two components.
1006
+ labels : tuple of str, default ("x", "y")
1007
+ Axis labels.
1008
+ interval : int, default 50
1009
+ Delay between animation frames, in milliseconds.
1010
+ skip : int, default 1
1011
+ Number of trajectory points advanced per animation frame; increase
1012
+ for long trajectories so the animation covers them in a reasonable
1013
+ number of frames.
1014
+ trail : int, optional
1015
+ Number of most-recent points to keep visible; the whole path-so-far
1016
+ is kept if omitted.
1017
+ color : str, default theme.PRIMARY
1018
+ Line and marker color.
1019
+ markers : list of dict, optional
1020
+ Extra fixed reference points to scatter once (e.g. the two primaries
1021
+ of a restricted three-body orbit); each dict needs ``"x"`` and
1022
+ ``"y"`` keys plus any ``ax.scatter`` keyword arguments.
1023
+ equal_aspect : bool, default False
1024
+ Whether to force an equal aspect ratio.
1025
+ title : str, optional
1026
+ Axes title.
1027
+
1028
+ Returns
1029
+ -------
1030
+ matplotlib.animation.FuncAnimation
1031
+ """
1032
+ states = np.asarray(states, dtype=np.float64)
1033
+ n = states.shape[0]
1034
+
1035
+ fig, ax = plt.subplots(figsize=(6.5, 6))
1036
+ pad_x = 0.05 * (states[:, 0].max() - states[:, 0].min() + 1e-12)
1037
+ pad_y = 0.05 * (states[:, 1].max() - states[:, 1].min() + 1e-12)
1038
+ ax.set_xlim(states[:, 0].min() - pad_x, states[:, 0].max() + pad_x)
1039
+ ax.set_ylim(states[:, 1].min() - pad_y, states[:, 1].max() + pad_y)
1040
+ ax.set_xlabel(labels[0])
1041
+ ax.set_ylabel(labels[1])
1042
+ if equal_aspect:
1043
+ ax.set_aspect("equal")
1044
+ if title:
1045
+ ax.set_title(title)
1046
+
1047
+ for m in markers or []:
1048
+ ax.scatter([m["x"]], [m["y"]], **{k: v for k, v in m.items() if k not in ("x", "y")})
1049
+ if markers and any("label" in m for m in markers):
1050
+ ax.legend(loc="upper right", fontsize=8)
1051
+
1052
+ (line,) = ax.plot([], [], color=color, lw=0.8)
1053
+ (head,) = ax.plot([], [], "o", color=theme.ACCENT, markersize=6)
1054
+
1055
+ frame_ends = np.arange(skip, n + skip, skip)
1056
+ frame_ends[-1] = n
1057
+
1058
+ def update(frame: int) -> tuple:
1059
+ end = int(frame_ends[frame])
1060
+ lo = 0 if trail is None else max(0, end - trail)
1061
+ line.set_data(states[lo:end, 0], states[lo:end, 1])
1062
+ head.set_data([states[end - 1, 0]], [states[end - 1, 1]])
1063
+ return line, head
1064
+
1065
+ anim = FuncAnimation(fig, update, frames=len(frame_ends), interval=interval, blit=False)
1066
+ return anim
1067
+
1068
+
1069
+ def animate_double_pendulum(
1070
+ system: DoublePendulum,
1071
+ state0: ArrayLike | None = None,
1072
+ dt: float = 0.01,
1073
+ n_steps: int = 1000,
1074
+ interval: int = 40,
1075
+ trail: int = 300,
1076
+ skip: int = 1,
1077
+ ) -> FuncAnimation:
1078
+ """Animate the double pendulum swinging, arms and all.
1079
+
1080
+ Integrates the pendulum and renders it kinematically -- the two rigid
1081
+ rods and bobs swinging in real ``(x, y)`` space, with a fading trail
1082
+ behind the outer bob -- rather than only the abstract
1083
+ ``(theta, omega)`` trajectory.
1084
+
1085
+ Parameters
1086
+ ----------
1087
+ system : DoublePendulum
1088
+ Pendulum to animate.
1089
+ state0 : array_like of float, shape (4,), optional
1090
+ Initial state ``(theta1, theta2, omega1, omega2)``; defaults to
1091
+ :meth:`~physicskit.chaos.systems.continuous.DoublePendulum.initial_state`.
1092
+ dt : float, default 0.01
1093
+ Integration step size.
1094
+ n_steps : int, default 1000
1095
+ Number of integration steps to integrate.
1096
+ interval : int, default 40
1097
+ Delay between animation frames, in milliseconds.
1098
+ trail : int, default 300
1099
+ Number of most-recent integration steps to keep visible as a trail
1100
+ behind the outer bob.
1101
+ skip : int, default 1
1102
+ Number of integration steps advanced per animation frame; increase
1103
+ for long integrations so the animation covers them in a reasonable
1104
+ number of frames.
1105
+
1106
+ Returns
1107
+ -------
1108
+ matplotlib.animation.FuncAnimation
1109
+ """
1110
+ state0 = system.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
1111
+ _, states = system.trajectory(state0, dt=dt, n_steps=n_steps)
1112
+ th1, th2 = states[:, 0], states[:, 1]
1113
+ l1, l2 = system.l1, system.l2
1114
+
1115
+ x1 = l1 * np.sin(th1)
1116
+ y1 = -l1 * np.cos(th1)
1117
+ x2 = x1 + l2 * np.sin(th2)
1118
+ y2 = y1 - l2 * np.cos(th2)
1119
+
1120
+ reach = l1 + l2
1121
+ fig, ax = plt.subplots(figsize=(6, 6))
1122
+ ax.set_xlim(-1.05 * reach, 1.05 * reach)
1123
+ ax.set_ylim(-1.05 * reach, 1.05 * reach)
1124
+ ax.set_aspect("equal")
1125
+ ax.set_title("Double pendulum")
1126
+
1127
+ (trail_line,) = ax.plot([], [], color=theme.ACCENT, lw=0.8, alpha=0.7)
1128
+ (rods,) = ax.plot([], [], "-", color=theme.STRUCTURE, lw=1.5)
1129
+ (bobs,) = ax.plot([], [], "o", color=theme.PRIMARY, markersize=9)
1130
+
1131
+ n = states.shape[0]
1132
+ frame_ends = np.arange(skip, n + skip, skip)
1133
+ frame_ends[-1] = n
1134
+
1135
+ def update(frame: int) -> tuple:
1136
+ end = int(frame_ends[frame]) - 1
1137
+ lo = max(0, end - trail)
1138
+ trail_line.set_data(x2[lo : end + 1], y2[lo : end + 1])
1139
+ rods.set_data([0.0, x1[end], x2[end]], [0.0, y1[end], y2[end]])
1140
+ bobs.set_data([x1[end], x2[end]], [y1[end], y2[end]])
1141
+ return trail_line, rods, bobs
1142
+
1143
+ anim = FuncAnimation(fig, update, frames=len(frame_ends), interval=interval, blit=False)
1144
+ return anim
1145
+
1146
+
1147
+ def animate_magnetic_pendulum(
1148
+ system: MagneticPendulum,
1149
+ state0: ArrayLike | None = None,
1150
+ dt: float = 0.05,
1151
+ n_steps: int = 600,
1152
+ interval: int = 40,
1153
+ trail: int = 150,
1154
+ skip: int = 1,
1155
+ ) -> FuncAnimation:
1156
+ """Animate a magnetic pendulum bob swinging in toward whichever magnet wins.
1157
+
1158
+ Parameters
1159
+ ----------
1160
+ system : MagneticPendulum
1161
+ Pendulum to animate.
1162
+ state0 : array_like of float, shape (4,), optional
1163
+ Initial state ``(x, y, vx, vy)``; defaults to
1164
+ :meth:`~physicskit.chaos.systems.continuous.MagneticPendulum.initial_state`.
1165
+ dt : float, default 0.05
1166
+ Integration step size.
1167
+ n_steps : int, default 600
1168
+ Number of integration steps to integrate.
1169
+ interval : int, default 40
1170
+ Delay between animation frames, in milliseconds.
1171
+ trail : int, default 150
1172
+ Number of most-recent integration steps to keep visible as a trail.
1173
+ skip : int, default 1
1174
+ Number of integration steps advanced per animation frame; increase
1175
+ for long integrations so the animation covers them in a reasonable
1176
+ number of frames.
1177
+
1178
+ Returns
1179
+ -------
1180
+ matplotlib.animation.FuncAnimation
1181
+ """
1182
+ state0 = system.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
1183
+ _, states = system.trajectory(state0, dt=dt, n_steps=n_steps)
1184
+ x, y = states[:, 0], states[:, 1]
1185
+ magnets = system.magnet_positions
1186
+
1187
+ extent = max(1.5, float(np.abs(magnets).max()) * 1.4, float(np.abs(states[:, :2]).max()) * 1.1)
1188
+ fig, ax = plt.subplots(figsize=(6, 6))
1189
+ ax.set_xlim(-extent, extent)
1190
+ ax.set_ylim(-extent, extent)
1191
+ ax.set_aspect("equal")
1192
+ ax.scatter(
1193
+ magnets[:, 0],
1194
+ magnets[:, 1],
1195
+ marker="X",
1196
+ s=120,
1197
+ color=theme.STRUCTURE,
1198
+ zorder=3,
1199
+ label="magnets",
1200
+ )
1201
+ ax.set_title("Magnetic pendulum")
1202
+ ax.legend(loc="upper right")
1203
+
1204
+ (trail_line,) = ax.plot([], [], color=theme.PRIMARY, lw=1.0, alpha=0.8)
1205
+ (bob,) = ax.plot([], [], "o", color=theme.ACCENT, markersize=8, zorder=4)
1206
+
1207
+ n = states.shape[0]
1208
+ frame_ends = np.arange(skip, n + skip, skip)
1209
+ frame_ends[-1] = n
1210
+
1211
+ def update(frame: int) -> tuple:
1212
+ end = int(frame_ends[frame]) - 1
1213
+ lo = max(0, end - trail)
1214
+ trail_line.set_data(x[lo : end + 1], y[lo : end + 1])
1215
+ bob.set_data([x[end]], [y[end]])
1216
+ return trail_line, bob
1217
+
1218
+ anim = FuncAnimation(fig, update, frames=len(frame_ends), interval=interval, blit=False)
1219
+ return anim
1220
+
1221
+
1222
+ def animate_restricted_three_body(
1223
+ system: RestrictedThreeBody,
1224
+ state0: ArrayLike | None = None,
1225
+ dt: float = 0.0005,
1226
+ n_steps: int = 20000,
1227
+ interval: int = 30,
1228
+ trail: int = 1000,
1229
+ skip: int = 10,
1230
+ ) -> FuncAnimation:
1231
+ """Animate a CR3BP trajectory in the rotating frame, alongside both
1232
+ primaries and all five Lagrange points.
1233
+
1234
+ The two primaries sit fixed (by construction of the rotating frame)
1235
+ at ``(-mu, 0)`` and ``(1 - mu, 0)``; the five Lagrange points (see
1236
+ :func:`~physicskit.chaos.systems.continuous.lagrange_points`) are marked and
1237
+ labeled once, since they too are fixed equilibria of the rotating-frame
1238
+ dynamics -- watching the test particle's trajectory loop past them
1239
+ directly shows how close (or not) a given orbit passes to each one.
1240
+
1241
+ Parameters
1242
+ ----------
1243
+ system : RestrictedThreeBody
1244
+ CR3BP system to animate.
1245
+ state0 : array_like of float, shape (4,), optional
1246
+ Initial state ``(x, y, vx, vy)``; defaults to
1247
+ :meth:`~physicskit.chaos.systems.continuous.RestrictedThreeBody.initial_state`.
1248
+ dt : float, default 0.0005
1249
+ Integration step size.
1250
+ n_steps : int, default 20000
1251
+ Number of integration steps to integrate.
1252
+ interval : int, default 30
1253
+ Delay between animation frames, in milliseconds.
1254
+ trail : int, default 1000
1255
+ Number of most-recent integration steps to keep visible as a trail.
1256
+ skip : int, default 10
1257
+ Number of integration steps advanced per animation frame; increase
1258
+ for long integrations so the animation covers them in a reasonable
1259
+ number of frames.
1260
+
1261
+ Returns
1262
+ -------
1263
+ matplotlib.animation.FuncAnimation
1264
+ """
1265
+ state0 = system.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
1266
+ _, states = system.trajectory(state0, dt=dt, n_steps=n_steps)
1267
+ x, y = states[:, 0], states[:, 1]
1268
+ mu = system.mu
1269
+ lpoints = lagrange_points(mu)
1270
+
1271
+ fig, ax = plt.subplots(figsize=(7, 7))
1272
+ pad = 0.15 * max(1.0, float(np.abs(np.concatenate([x, y, lpoints.ravel()])).max()))
1273
+ ax.set_xlim(min(x.min(), lpoints[:, 0].min()) - pad, max(x.max(), lpoints[:, 0].max()) + pad)
1274
+ ax.set_ylim(min(y.min(), lpoints[:, 1].min()) - pad, max(y.max(), lpoints[:, 1].max()) + pad)
1275
+ ax.set_aspect("equal")
1276
+ ax.set_title(f"{system.__class__.__name__} (rotating frame)")
1277
+
1278
+ ax.scatter([-mu], [0.0], marker="o", s=100, color=theme.STRUCTURE, zorder=3, label="primary")
1279
+ ax.scatter([1.0 - mu], [0.0], marker="o", s=35, color=theme.MUTED, zorder=3, label="secondary")
1280
+ ax.scatter(lpoints[:, 0], lpoints[:, 1], marker="^", s=40, color=theme.ACCENT, zorder=3, label="Lagrange points")
1281
+ for i, (lx, ly) in enumerate(lpoints, start=1):
1282
+ ax.annotate(f"L{i}", (lx, ly), textcoords="offset points", xytext=(4, 4), fontsize=8)
1283
+ ax.legend(loc="upper right", fontsize=8)
1284
+
1285
+ (trail_line,) = ax.plot([], [], color=theme.PRIMARY, lw=0.8)
1286
+ (head,) = ax.plot([], [], "o", color=theme.PRIMARY, markersize=6, zorder=4)
1287
+
1288
+ n = states.shape[0]
1289
+ frame_ends = np.arange(skip, n + skip, skip)
1290
+ frame_ends[-1] = n
1291
+
1292
+ def update(frame: int) -> tuple:
1293
+ end = int(frame_ends[frame]) - 1
1294
+ lo = max(0, end - trail)
1295
+ trail_line.set_data(x[lo : end + 1], y[lo : end + 1])
1296
+ head.set_data([x[end]], [y[end]])
1297
+ return trail_line, head
1298
+
1299
+ anim = FuncAnimation(fig, update, frames=len(frame_ends), interval=interval, blit=False)
1300
+ return anim
1301
+
1302
+
1303
+ def animate_driven_pendulum(
1304
+ system: DrivenPendulum,
1305
+ state0: ArrayLike | None = None,
1306
+ dt: float = 0.02,
1307
+ n_steps: int = 1500,
1308
+ interval: int = 40,
1309
+ trail: int = 200,
1310
+ skip: int = 1,
1311
+ length: float = 1.0,
1312
+ ) -> FuncAnimation:
1313
+ """Animate the driven, damped pendulum swinging in real space.
1314
+
1315
+ Parameters
1316
+ ----------
1317
+ system : DrivenPendulum
1318
+ Pendulum to animate.
1319
+ state0 : array_like of float, shape (2,), optional
1320
+ Initial state ``(theta, omega)``; defaults to
1321
+ :meth:`~physicskit.chaos.systems.continuous.DrivenPendulum.initial_state`.
1322
+ dt : float, default 0.02
1323
+ Integration step size.
1324
+ n_steps : int, default 1500
1325
+ Number of integration steps to integrate.
1326
+ interval : int, default 40
1327
+ Delay between animation frames, in milliseconds.
1328
+ trail : int, default 200
1329
+ Number of most-recent integration steps to keep visible as a trail.
1330
+ skip : int, default 1
1331
+ Number of integration steps advanced per animation frame; increase
1332
+ for long integrations so the animation covers them in a reasonable
1333
+ number of frames.
1334
+ length : float, default 1.0
1335
+ Visual rod length; only ``g/l`` enters the dynamics, so the actual
1336
+ drawn length is an arbitrary visualization choice.
1337
+
1338
+ Returns
1339
+ -------
1340
+ matplotlib.animation.FuncAnimation
1341
+ """
1342
+ state0 = system.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
1343
+ _, states = system.trajectory(state0, dt=dt, n_steps=n_steps)
1344
+ theta = states[:, 0]
1345
+ x = length * np.sin(theta)
1346
+ y = -length * np.cos(theta)
1347
+
1348
+ reach = length * 1.2
1349
+ fig, ax = plt.subplots(figsize=(6, 6))
1350
+ ax.set_xlim(-reach, reach)
1351
+ ax.set_ylim(-reach, reach)
1352
+ ax.set_aspect("equal")
1353
+ ax.set_title("Driven, damped pendulum")
1354
+
1355
+ (trail_line,) = ax.plot([], [], color=theme.ACCENT, lw=0.8, alpha=0.7)
1356
+ (rod,) = ax.plot([], [], "-", color=theme.STRUCTURE, lw=1.5)
1357
+ (bob,) = ax.plot([], [], "o", color=theme.PRIMARY, markersize=10)
1358
+
1359
+ n = states.shape[0]
1360
+ frame_ends = np.arange(skip, n + skip, skip)
1361
+ frame_ends[-1] = n
1362
+
1363
+ def update(frame: int) -> tuple:
1364
+ end = int(frame_ends[frame]) - 1
1365
+ lo = max(0, end - trail)
1366
+ trail_line.set_data(x[lo : end + 1], y[lo : end + 1])
1367
+ rod.set_data([0.0, x[end]], [0.0, y[end]])
1368
+ bob.set_data([x[end]], [y[end]])
1369
+ return trail_line, rod, bob
1370
+
1371
+ anim = FuncAnimation(fig, update, frames=len(frame_ends), interval=interval, blit=False)
1372
+ return anim