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,1276 @@
1
+ """Continuous-time chaotic dynamical systems: Lorenz, Rossler, Double Pendulum, Duffing, Chua.
2
+
3
+ Each system exposes a plain-Python :meth:`rhs` (for interactive use / plotting)
4
+ plus a module-level Numba ``@njit`` right-hand-side function with the signature
5
+ ``rhs(state, t, params) -> ndarray`` compatible with
6
+ :func:`physicskit.chaos.core.integrators.rk4_integrate`, and a convenience
7
+ :meth:`trajectory` method that drives that integrator.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import numpy as np
13
+ from numba import njit
14
+ from numpy.typing import NDArray
15
+ from scipy.optimize import brentq
16
+
17
+ from physicskit.chaos.core.base_system import DynamicalSystem
18
+ from physicskit.chaos.core.integrators import rk4_integrate
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Lorenz system
22
+ # ---------------------------------------------------------------------------
23
+
24
+
25
+ @njit(cache=True)
26
+ def _lorenz_rhs(state: NDArray[np.float64], t: float, params: NDArray[np.float64]) -> NDArray[np.float64]:
27
+ """Lorenz vector field ``dx/dt = f(x, t; sigma, rho, beta)``.
28
+
29
+ Parameters
30
+ ----------
31
+ state : ndarray of float, shape (3,)
32
+ State vector ``(x, y, z)``.
33
+ t : float
34
+ Current time (unused; the system is autonomous).
35
+ params : ndarray of float, shape (3,)
36
+ Parameters ``(sigma, rho, beta)``.
37
+
38
+ Returns
39
+ -------
40
+ ndarray of float, shape (3,)
41
+ Time derivative ``(dx/dt, dy/dt, dz/dt)``.
42
+ """
43
+ sigma, rho, beta = params[0], params[1], params[2]
44
+ x, y, z = state[0], state[1], state[2]
45
+ out = np.empty(3)
46
+ out[0] = sigma * (y - x)
47
+ out[1] = x * (rho - z) - y
48
+ out[2] = x * y - beta * z
49
+ return out
50
+
51
+
52
+ class Lorenz(DynamicalSystem):
53
+ """The Lorenz attractor.
54
+
55
+ Parameters
56
+ ----------
57
+ sigma : float, default 10.0
58
+ Prandtl-number-like parameter.
59
+ rho : float, default 28.0
60
+ Rayleigh-number-like parameter.
61
+ beta : float, default 8.0 / 3.0
62
+ Geometric parameter.
63
+
64
+ Attributes
65
+ ----------
66
+ sigma, rho, beta : float
67
+ System parameters.
68
+ """
69
+
70
+ #: State dimension, always 3.
71
+ dim = 3
72
+
73
+ def __init__(self, sigma: float = 10.0, rho: float = 28.0, beta: float = 8.0 / 3.0):
74
+ self.sigma = float(sigma)
75
+ self.rho = float(rho)
76
+ self.beta = float(beta)
77
+
78
+ @property
79
+ def params(self) -> NDArray[np.float64]:
80
+ """Parameter vector ``(sigma, rho, beta)``.
81
+
82
+ Returns
83
+ -------
84
+ ndarray of float, shape (3,)
85
+ """
86
+ return np.array([self.sigma, self.rho, self.beta])
87
+
88
+ def rhs(self, state: NDArray[np.float64], t: float) -> NDArray[np.float64]:
89
+ """Evaluate the Lorenz vector field.
90
+
91
+ Parameters
92
+ ----------
93
+ state : ndarray of float, shape (3,)
94
+ State vector ``(x, y, z)``.
95
+ t : float
96
+ Current time (unused; the system is autonomous).
97
+
98
+ Returns
99
+ -------
100
+ ndarray of float, shape (3,)
101
+ Time derivative ``(dx/dt, dy/dt, dz/dt)``.
102
+ """
103
+ return np.asarray(_lorenz_rhs(np.asarray(state, dtype=np.float64), t, self.params))
104
+
105
+ def initial_state(self) -> NDArray[np.float64]:
106
+ """Default initial condition ``(1, 1, 1)``.
107
+
108
+ Returns
109
+ -------
110
+ ndarray of float, shape (3,)
111
+ """
112
+ return np.array([1.0, 1.0, 1.0])
113
+
114
+ def trajectory(
115
+ self,
116
+ state0: NDArray[np.float64] | None = None,
117
+ t0: float = 0.0,
118
+ dt: float = 0.01,
119
+ n_steps: int = 10000,
120
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
121
+ """Integrate a trajectory with the Numba-accelerated RK4 integrator.
122
+
123
+ Parameters
124
+ ----------
125
+ state0 : array_like of float, shape (3,), optional
126
+ Initial state; defaults to :meth:`initial_state`.
127
+ t0 : float, default 0.0
128
+ Initial time.
129
+ dt : float, default 0.01
130
+ Integration step size.
131
+ n_steps : int, default 10000
132
+ Number of integration steps.
133
+
134
+ Returns
135
+ -------
136
+ times : ndarray of float, shape (n_steps + 1,)
137
+ states : ndarray of float, shape (n_steps + 1, 3)
138
+ """
139
+ state0 = self.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
140
+ return rk4_integrate(_lorenz_rhs, state0, t0, dt, n_steps, self.params)
141
+
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # Rossler system
145
+ # ---------------------------------------------------------------------------
146
+
147
+
148
+ @njit(cache=True)
149
+ def _rossler_rhs(state: NDArray[np.float64], t: float, params: NDArray[np.float64]) -> NDArray[np.float64]:
150
+ """Rossler vector field ``dx/dt = f(x, t; a, b, c)``.
151
+
152
+ Parameters
153
+ ----------
154
+ state : ndarray of float, shape (3,)
155
+ State vector ``(x, y, z)``.
156
+ t : float
157
+ Current time (unused; the system is autonomous).
158
+ params : ndarray of float, shape (3,)
159
+ Parameters ``(a, b, c)``.
160
+
161
+ Returns
162
+ -------
163
+ ndarray of float, shape (3,)
164
+ Time derivative ``(dx/dt, dy/dt, dz/dt)``.
165
+ """
166
+ a, b, c = params[0], params[1], params[2]
167
+ x, y, z = state[0], state[1], state[2]
168
+ out = np.empty(3)
169
+ out[0] = -y - z
170
+ out[1] = x + a * y
171
+ out[2] = b + z * (x - c)
172
+ return out
173
+
174
+
175
+ class Rossler(DynamicalSystem):
176
+ """The Rossler attractor.
177
+
178
+ Parameters
179
+ ----------
180
+ a : float, default 0.2
181
+ System parameter.
182
+ b : float, default 0.2
183
+ System parameter.
184
+ c : float, default 5.7
185
+ System parameter.
186
+
187
+ Attributes
188
+ ----------
189
+ a, b, c : float
190
+ System parameters.
191
+ """
192
+
193
+ #: State dimension, always 3.
194
+ dim = 3
195
+
196
+ def __init__(self, a: float = 0.2, b: float = 0.2, c: float = 5.7):
197
+ self.a = float(a)
198
+ self.b = float(b)
199
+ self.c = float(c)
200
+
201
+ @property
202
+ def params(self) -> NDArray[np.float64]:
203
+ """Parameter vector ``(a, b, c)``.
204
+
205
+ Returns
206
+ -------
207
+ ndarray of float, shape (3,)
208
+ """
209
+ return np.array([self.a, self.b, self.c])
210
+
211
+ def rhs(self, state: NDArray[np.float64], t: float) -> NDArray[np.float64]:
212
+ """Evaluate the Rossler vector field.
213
+
214
+ Parameters
215
+ ----------
216
+ state : ndarray of float, shape (3,)
217
+ State vector ``(x, y, z)``.
218
+ t : float
219
+ Current time (unused; the system is autonomous).
220
+
221
+ Returns
222
+ -------
223
+ ndarray of float, shape (3,)
224
+ Time derivative ``(dx/dt, dy/dt, dz/dt)``.
225
+ """
226
+ return np.asarray(_rossler_rhs(np.asarray(state, dtype=np.float64), t, self.params))
227
+
228
+ def initial_state(self) -> NDArray[np.float64]:
229
+ """Default initial condition ``(1, 1, 1)``.
230
+
231
+ Returns
232
+ -------
233
+ ndarray of float, shape (3,)
234
+ """
235
+ return np.array([1.0, 1.0, 1.0])
236
+
237
+ def trajectory(
238
+ self,
239
+ state0: NDArray[np.float64] | None = None,
240
+ t0: float = 0.0,
241
+ dt: float = 0.01,
242
+ n_steps: int = 10000,
243
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
244
+ """Integrate a trajectory with the Numba-accelerated RK4 integrator.
245
+
246
+ Parameters
247
+ ----------
248
+ state0 : array_like of float, shape (3,), optional
249
+ Initial state; defaults to :meth:`initial_state`.
250
+ t0 : float, default 0.0
251
+ Initial time.
252
+ dt : float, default 0.01
253
+ Integration step size.
254
+ n_steps : int, default 10000
255
+ Number of integration steps.
256
+
257
+ Returns
258
+ -------
259
+ times : ndarray of float, shape (n_steps + 1,)
260
+ states : ndarray of float, shape (n_steps + 1, 3)
261
+ """
262
+ state0 = self.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
263
+ return rk4_integrate(_rossler_rhs, state0, t0, dt, n_steps, self.params)
264
+
265
+
266
+ # ---------------------------------------------------------------------------
267
+ # Double pendulum
268
+ # ---------------------------------------------------------------------------
269
+
270
+
271
+ @njit(cache=True)
272
+ def _double_pendulum_rhs(state: NDArray[np.float64], t: float, params: NDArray[np.float64]) -> NDArray[np.float64]:
273
+ """Double-pendulum vector field ``dx/dt = f(x, t; m1, m2, l1, l2, g)``.
274
+
275
+ Parameters
276
+ ----------
277
+ state : ndarray of float, shape (4,)
278
+ State vector ``(theta1, theta2, omega1, omega2)``.
279
+ t : float
280
+ Current time (unused; the system is autonomous).
281
+ params : ndarray of float, shape (5,)
282
+ Parameters ``(m1, m2, l1, l2, g)``.
283
+
284
+ Returns
285
+ -------
286
+ ndarray of float, shape (4,)
287
+ Time derivative ``(dtheta1/dt, dtheta2/dt, domega1/dt, domega2/dt)``.
288
+ """
289
+ m1, m2, l1, l2, g = params[0], params[1], params[2], params[3], params[4]
290
+ th1, th2, w1, w2 = state[0], state[1], state[2], state[3]
291
+ delta = th1 - th2
292
+ den1 = l1 * (2.0 * m1 + m2 - m2 * np.cos(2.0 * delta))
293
+ den2 = l2 * (2.0 * m1 + m2 - m2 * np.cos(2.0 * delta))
294
+
295
+ dw1 = (
296
+ -g * (2.0 * m1 + m2) * np.sin(th1) - m2 * g * np.sin(th1 - 2.0 * th2) - 2.0 * np.sin(delta) * m2 * (w2 * w2 * l2 + w1 * w1 * l1 * np.cos(delta))
297
+ ) / den1
298
+ dw2 = (2.0 * np.sin(delta) * (w1 * w1 * l1 * (m1 + m2) + g * (m1 + m2) * np.cos(th1) + w2 * w2 * l2 * m2 * np.cos(delta))) / den2
299
+
300
+ out = np.empty(4)
301
+ out[0] = w1
302
+ out[1] = w2
303
+ out[2] = dw1
304
+ out[3] = dw2
305
+ return out
306
+
307
+
308
+ class DoublePendulum(DynamicalSystem):
309
+ """Planar double pendulum: point masses on massless rods.
310
+
311
+ Parameters
312
+ ----------
313
+ m1 : float, default 1.0
314
+ Mass of the first (inner) bob.
315
+ m2 : float, default 1.0
316
+ Mass of the second (outer) bob.
317
+ l1 : float, default 1.0
318
+ Length of the first (inner) rod.
319
+ l2 : float, default 1.0
320
+ Length of the second (outer) rod.
321
+ g : float, default 9.81
322
+ Gravitational acceleration.
323
+
324
+ Attributes
325
+ ----------
326
+ m1, m2, l1, l2, g : float
327
+ System parameters.
328
+ """
329
+
330
+ #: State dimension, always 4. State is ``(theta1, theta2, omega1, omega2)``.
331
+ dim = 4
332
+
333
+ def __init__(self, m1: float = 1.0, m2: float = 1.0, l1: float = 1.0, l2: float = 1.0, g: float = 9.81):
334
+ self.m1, self.m2, self.l1, self.l2, self.g = (
335
+ float(m1),
336
+ float(m2),
337
+ float(l1),
338
+ float(l2),
339
+ float(g),
340
+ )
341
+
342
+ @property
343
+ def params(self) -> NDArray[np.float64]:
344
+ """Parameter vector ``(m1, m2, l1, l2, g)``.
345
+
346
+ Returns
347
+ -------
348
+ ndarray of float, shape (5,)
349
+ """
350
+ return np.array([self.m1, self.m2, self.l1, self.l2, self.g])
351
+
352
+ def rhs(self, state: NDArray[np.float64], t: float) -> NDArray[np.float64]:
353
+ """Evaluate the double-pendulum vector field.
354
+
355
+ Parameters
356
+ ----------
357
+ state : ndarray of float, shape (4,)
358
+ State vector ``(theta1, theta2, omega1, omega2)``.
359
+ t : float
360
+ Current time (unused; the system is autonomous).
361
+
362
+ Returns
363
+ -------
364
+ ndarray of float, shape (4,)
365
+ Time derivative ``(dtheta1/dt, dtheta2/dt, domega1/dt, domega2/dt)``.
366
+ """
367
+ return np.asarray(_double_pendulum_rhs(np.asarray(state, dtype=np.float64), t, self.params))
368
+
369
+ def initial_state(self) -> NDArray[np.float64]:
370
+ """Default initial condition: both rods horizontal, at rest.
371
+
372
+ Returns
373
+ -------
374
+ ndarray of float, shape (4,)
375
+ """
376
+ return np.array([np.pi / 2.0, np.pi / 2.0, 0.0, 0.0])
377
+
378
+ def trajectory(
379
+ self,
380
+ state0: NDArray[np.float64] | None = None,
381
+ t0: float = 0.0,
382
+ dt: float = 0.005,
383
+ n_steps: int = 10000,
384
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
385
+ """Integrate a trajectory with the Numba-accelerated RK4 integrator.
386
+
387
+ Parameters
388
+ ----------
389
+ state0 : array_like of float, shape (4,), optional
390
+ Initial state; defaults to :meth:`initial_state`.
391
+ t0 : float, default 0.0
392
+ Initial time.
393
+ dt : float, default 0.005
394
+ Integration step size.
395
+ n_steps : int, default 10000
396
+ Number of integration steps.
397
+
398
+ Returns
399
+ -------
400
+ times : ndarray of float, shape (n_steps + 1,)
401
+ states : ndarray of float, shape (n_steps + 1, 4)
402
+ """
403
+ state0 = self.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
404
+ return rk4_integrate(_double_pendulum_rhs, state0, t0, dt, n_steps, self.params)
405
+
406
+ def energy(self, state: NDArray[np.float64]) -> float:
407
+ """Total mechanical energy, useful for checking integrator drift.
408
+
409
+ Parameters
410
+ ----------
411
+ state : array_like of float, shape (4,)
412
+ State vector ``(theta1, theta2, omega1, omega2)``.
413
+
414
+ Returns
415
+ -------
416
+ float
417
+ Total (kinetic + potential) mechanical energy.
418
+ """
419
+ th1, th2, w1, w2 = state
420
+ m1, m2, l1, l2, g = self.m1, self.m2, self.l1, self.l2, self.g
421
+ kinetic = 0.5 * m1 * (l1 * w1) ** 2 + 0.5 * m2 * ((l1 * w1) ** 2 + (l2 * w2) ** 2 + 2 * l1 * l2 * w1 * w2 * np.cos(th1 - th2))
422
+ potential = -(m1 + m2) * g * l1 * np.cos(th1) - m2 * g * l2 * np.cos(th2)
423
+ return float(kinetic + potential)
424
+
425
+
426
+ # ---------------------------------------------------------------------------
427
+ # Duffing oscillator
428
+ # ---------------------------------------------------------------------------
429
+
430
+
431
+ @njit(cache=True)
432
+ def _duffing_rhs(state: NDArray[np.float64], t: float, params: NDArray[np.float64]) -> NDArray[np.float64]:
433
+ """Duffing vector field ``dx/dt = f(x, t; delta, alpha, beta, gamma, omega)``.
434
+
435
+ Parameters
436
+ ----------
437
+ state : ndarray of float, shape (2,)
438
+ State vector ``(x, v)``.
439
+ t : float
440
+ Current time (the system is non-autonomous: forcing depends on `t`).
441
+ params : ndarray of float, shape (5,)
442
+ Parameters ``(delta, alpha, beta, gamma, omega)``.
443
+
444
+ Returns
445
+ -------
446
+ ndarray of float, shape (2,)
447
+ Time derivative ``(dx/dt, dv/dt)``.
448
+ """
449
+ delta, alpha, beta, gamma, omega = params[0], params[1], params[2], params[3], params[4]
450
+ x, v = state[0], state[1]
451
+ out = np.empty(2)
452
+ out[0] = v
453
+ out[1] = -delta * v - alpha * x - beta * x**3 + gamma * np.cos(omega * t)
454
+ return out
455
+
456
+
457
+ class Duffing(DynamicalSystem):
458
+ """The forced, damped Duffing oscillator.
459
+
460
+ Governed by ``x'' + delta*x' + alpha*x + beta*x^3 = gamma*cos(omega*t)``.
461
+
462
+ Parameters
463
+ ----------
464
+ delta : float, default 0.3
465
+ Damping coefficient.
466
+ alpha : float, default -1.0
467
+ Linear stiffness.
468
+ beta : float, default 1.0
469
+ Cubic (nonlinear) stiffness.
470
+ gamma : float, default 0.37
471
+ Forcing amplitude.
472
+ omega : float, default 1.2
473
+ Forcing angular frequency.
474
+
475
+ Attributes
476
+ ----------
477
+ delta, alpha, beta, gamma, omega : float
478
+ System parameters.
479
+ """
480
+
481
+ #: State dimension, always 2. State is ``(x, v)``.
482
+ dim = 2
483
+
484
+ def __init__(
485
+ self,
486
+ delta: float = 0.3,
487
+ alpha: float = -1.0,
488
+ beta: float = 1.0,
489
+ gamma: float = 0.37,
490
+ omega: float = 1.2,
491
+ ):
492
+ self.delta, self.alpha, self.beta, self.gamma, self.omega = (
493
+ float(delta),
494
+ float(alpha),
495
+ float(beta),
496
+ float(gamma),
497
+ float(omega),
498
+ )
499
+
500
+ @property
501
+ def params(self) -> NDArray[np.float64]:
502
+ """Parameter vector ``(delta, alpha, beta, gamma, omega)``.
503
+
504
+ Returns
505
+ -------
506
+ ndarray of float, shape (5,)
507
+ """
508
+ return np.array([self.delta, self.alpha, self.beta, self.gamma, self.omega])
509
+
510
+ def rhs(self, state: NDArray[np.float64], t: float) -> NDArray[np.float64]:
511
+ """Evaluate the Duffing vector field.
512
+
513
+ Parameters
514
+ ----------
515
+ state : ndarray of float, shape (2,)
516
+ State vector ``(x, v)``.
517
+ t : float
518
+ Current time (the forcing term depends on `t`).
519
+
520
+ Returns
521
+ -------
522
+ ndarray of float, shape (2,)
523
+ Time derivative ``(dx/dt, dv/dt)``.
524
+ """
525
+ return np.asarray(_duffing_rhs(np.asarray(state, dtype=np.float64), t, self.params))
526
+
527
+ def initial_state(self) -> NDArray[np.float64]:
528
+ """Default initial condition ``(1, 0)``.
529
+
530
+ Returns
531
+ -------
532
+ ndarray of float, shape (2,)
533
+ """
534
+ return np.array([1.0, 0.0])
535
+
536
+ def trajectory(
537
+ self,
538
+ state0: NDArray[np.float64] | None = None,
539
+ t0: float = 0.0,
540
+ dt: float = 0.01,
541
+ n_steps: int = 10000,
542
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
543
+ """Integrate a trajectory with the Numba-accelerated RK4 integrator.
544
+
545
+ Parameters
546
+ ----------
547
+ state0 : array_like of float, shape (2,), optional
548
+ Initial state; defaults to :meth:`initial_state`.
549
+ t0 : float, default 0.0
550
+ Initial time.
551
+ dt : float, default 0.01
552
+ Integration step size.
553
+ n_steps : int, default 10000
554
+ Number of integration steps.
555
+
556
+ Returns
557
+ -------
558
+ times : ndarray of float, shape (n_steps + 1,)
559
+ states : ndarray of float, shape (n_steps + 1, 2)
560
+ """
561
+ state0 = self.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
562
+ return rk4_integrate(_duffing_rhs, state0, t0, dt, n_steps, self.params)
563
+
564
+
565
+ # ---------------------------------------------------------------------------
566
+ # Chua's circuit
567
+ # ---------------------------------------------------------------------------
568
+
569
+
570
+ @njit(cache=True)
571
+ def _chua_rhs(state: NDArray[np.float64], t: float, params: NDArray[np.float64]) -> NDArray[np.float64]:
572
+ """Chua's circuit vector field ``dx/dt = f(x, t; alpha, beta, m0, m1)``.
573
+
574
+ Parameters
575
+ ----------
576
+ state : ndarray of float, shape (3,)
577
+ State vector ``(x, y, z)``.
578
+ t : float
579
+ Current time (unused; the system is autonomous).
580
+ params : ndarray of float, shape (4,)
581
+ Parameters ``(alpha, beta, m0, m1)``.
582
+
583
+ Returns
584
+ -------
585
+ ndarray of float, shape (3,)
586
+ Time derivative ``(dx/dt, dy/dt, dz/dt)``.
587
+ """
588
+ alpha, beta, m0, m1 = params[0], params[1], params[2], params[3]
589
+ x, y, z = state[0], state[1], state[2]
590
+ h = m1 * x + 0.5 * (m0 - m1) * (abs(x + 1.0) - abs(x - 1.0))
591
+ out = np.empty(3)
592
+ out[0] = alpha * (y - x - h)
593
+ out[1] = x - y + z
594
+ out[2] = -beta * y
595
+ return out
596
+
597
+
598
+ class Chua(DynamicalSystem):
599
+ """Chua's circuit: a simple chaotic electronic oscillator.
600
+
601
+ Built from just a resistor, two capacitors, an inductor, and one
602
+ piecewise-linear nonlinear resistor (the "Chua diode", ``h`` below), this
603
+ is one of the simplest physical systems known to be chaotic, and the
604
+ first to have its chaos confirmed experimentally in real hardware. For
605
+ the classic parameters below it produces the famous *double-scroll*
606
+ attractor: two spiral lobes, with the trajectory unpredictably switching
607
+ between them.
608
+
609
+ .. math::
610
+
611
+ \\dot{x} &= \\alpha (y - x - h(x)) \\\\
612
+ \\dot{y} &= x - y + z \\\\
613
+ \\dot{z} &= -\\beta y \\\\
614
+ h(x) &= m_1 x + \\tfrac{1}{2}(m_0 - m_1)(|x + 1| - |x - 1|)
615
+
616
+ Parameters
617
+ ----------
618
+ alpha : float, default 15.6
619
+ Ratio of the two capacitances.
620
+ beta : float, default 28.0
621
+ Ratio involving the inductance and second capacitance.
622
+ m0 : float, default -8/7
623
+ Inner (small-``|x|``) slope of the Chua diode's piecewise-linear
624
+ characteristic.
625
+ m1 : float, default -5/7
626
+ Outer (large-``|x|``) slope of the Chua diode's piecewise-linear
627
+ characteristic.
628
+
629
+ Attributes
630
+ ----------
631
+ alpha, beta, m0, m1 : float
632
+ System parameters.
633
+ """
634
+
635
+ #: State dimension, always 3.
636
+ dim = 3
637
+
638
+ def __init__(
639
+ self,
640
+ alpha: float = 15.6,
641
+ beta: float = 28.0,
642
+ m0: float = -8.0 / 7.0,
643
+ m1: float = -5.0 / 7.0,
644
+ ):
645
+ self.alpha = float(alpha)
646
+ self.beta = float(beta)
647
+ self.m0 = float(m0)
648
+ self.m1 = float(m1)
649
+
650
+ @property
651
+ def params(self) -> NDArray[np.float64]:
652
+ """Parameter vector ``(alpha, beta, m0, m1)``.
653
+
654
+ Returns
655
+ -------
656
+ ndarray of float, shape (4,)
657
+ """
658
+ return np.array([self.alpha, self.beta, self.m0, self.m1])
659
+
660
+ def rhs(self, state: NDArray[np.float64], t: float) -> NDArray[np.float64]:
661
+ """Evaluate Chua's circuit vector field.
662
+
663
+ Parameters
664
+ ----------
665
+ state : ndarray of float, shape (3,)
666
+ State vector ``(x, y, z)``.
667
+ t : float
668
+ Current time (unused; the system is autonomous).
669
+
670
+ Returns
671
+ -------
672
+ ndarray of float, shape (3,)
673
+ Time derivative ``(dx/dt, dy/dt, dz/dt)``.
674
+ """
675
+ return np.asarray(_chua_rhs(np.asarray(state, dtype=np.float64), t, self.params))
676
+
677
+ def initial_state(self) -> NDArray[np.float64]:
678
+ """Default initial condition, slightly off the unstable origin.
679
+
680
+ Returns
681
+ -------
682
+ ndarray of float, shape (3,)
683
+ """
684
+ return np.array([0.7, 0.0, 0.0])
685
+
686
+ def trajectory(
687
+ self,
688
+ state0: NDArray[np.float64] | None = None,
689
+ t0: float = 0.0,
690
+ dt: float = 0.01,
691
+ n_steps: int = 20000,
692
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
693
+ """Integrate a trajectory with the Numba-accelerated RK4 integrator.
694
+
695
+ Parameters
696
+ ----------
697
+ state0 : array_like of float, shape (3,), optional
698
+ Initial state; defaults to :meth:`initial_state`.
699
+ t0 : float, default 0.0
700
+ Initial time.
701
+ dt : float, default 0.01
702
+ Integration step size.
703
+ n_steps : int, default 20000
704
+ Number of integration steps.
705
+
706
+ Returns
707
+ -------
708
+ times : ndarray of float, shape (n_steps + 1,)
709
+ states : ndarray of float, shape (n_steps + 1, 3)
710
+ """
711
+ state0 = self.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
712
+ return rk4_integrate(_chua_rhs, state0, t0, dt, n_steps, self.params)
713
+
714
+
715
+ # ---------------------------------------------------------------------------
716
+ # Restricted three-body problem
717
+ # ---------------------------------------------------------------------------
718
+
719
+
720
+ @njit(cache=True)
721
+ def _restricted_three_body_rhs(state: NDArray[np.float64], t: float, params: NDArray[np.float64]) -> NDArray[np.float64]:
722
+ """CR3BP vector field (planar, rotating frame) ``dx/dt = f(x, t; mu)``.
723
+
724
+ Parameters
725
+ ----------
726
+ state : ndarray of float, shape (4,)
727
+ State vector ``(x, y, vx, vy)`` in the rotating frame.
728
+ t : float
729
+ Current time (unused; the system is autonomous in the rotating frame).
730
+ params : ndarray of float, shape (1,)
731
+ Parameters ``(mu,)``.
732
+
733
+ Returns
734
+ -------
735
+ ndarray of float, shape (4,)
736
+ Time derivative ``(dx/dt, dy/dt, dvx/dt, dvy/dt)``.
737
+ """
738
+ mu = params[0]
739
+ x, y, vx, vy = state[0], state[1], state[2], state[3]
740
+ r1 = np.sqrt((x + mu) ** 2 + y * y)
741
+ r2 = np.sqrt((x - 1.0 + mu) ** 2 + y * y)
742
+ out = np.empty(4)
743
+ out[0] = vx
744
+ out[1] = vy
745
+ out[2] = 2.0 * vy + x - (1.0 - mu) * (x + mu) / r1**3 - mu * (x - 1.0 + mu) / r2**3
746
+ out[3] = -2.0 * vx + y - (1.0 - mu) * y / r1**3 - mu * y / r2**3
747
+ return out
748
+
749
+
750
+ class RestrictedThreeBody(DynamicalSystem):
751
+ """The planar circular restricted three-body problem (CR3BP).
752
+
753
+ A massless third body moves under the gravity of two massive primaries
754
+ (masses ``1 - mu`` and ``mu``, in normalized units) that are themselves in
755
+ a fixed circular orbit about their common center of mass. In the
756
+ rotating (co-precessing) reference frame, the primaries sit fixed at
757
+ ``(-mu, 0)`` and ``(1 - mu, 0)``, and the third body's motion picks up
758
+ centrifugal and Coriolis terms alongside the two gravitational pulls.
759
+
760
+ This system is what led Poincare to the first discovery of deterministic
761
+ chaos: he found that, unlike the exactly solvable two-body problem, CR3BP
762
+ trajectories can depend on initial conditions in an essentially
763
+ unpredictable way. It also genuinely coexists with regular
764
+ (quasi-periodic, KAM-stable) motion -- the default initial condition
765
+ below is the classic *Arenstorf orbit*, a stable *periodic* orbit famous
766
+ in the numerical-methods literature as an ODE-solver stress test (it
767
+ passes very close to the smaller primary); see the example gallery for a
768
+ nearby, only slightly perturbed initial condition that is chaotic instead.
769
+
770
+ Because of the velocity-dependent Coriolis terms, this system is *not* a
771
+ separable Hamiltonian of the form ``pos'' = force(pos, t)``, so it must
772
+ be integrated with :func:`physicskit.chaos.core.integrators.rk4_integrate`
773
+ (the symplectic :func:`~physicskit.chaos.core.integrators.leapfrog_integrate` /
774
+ :func:`~physicskit.chaos.core.integrators.yoshida4_integrate` do not apply).
775
+
776
+ Parameters
777
+ ----------
778
+ mu : float, default 0.012277471
779
+ Mass parameter (mass of the smaller primary, in units where the
780
+ total mass is 1); the default is the Earth-Moon-like value used in
781
+ the classic Arenstorf orbit.
782
+
783
+ Attributes
784
+ ----------
785
+ mu : float
786
+ Mass parameter.
787
+ """
788
+
789
+ #: State dimension, always 4. State is ``(x, y, vx, vy)``.
790
+ dim = 4
791
+
792
+ def __init__(self, mu: float = 0.012277471):
793
+ self.mu = float(mu)
794
+
795
+ @property
796
+ def params(self) -> NDArray[np.float64]:
797
+ """Parameter vector ``(mu,)``.
798
+
799
+ Returns
800
+ -------
801
+ ndarray of float, shape (1,)
802
+ """
803
+ return np.array([self.mu])
804
+
805
+ def rhs(self, state: NDArray[np.float64], t: float) -> NDArray[np.float64]:
806
+ """Evaluate the CR3BP vector field.
807
+
808
+ Parameters
809
+ ----------
810
+ state : ndarray of float, shape (4,)
811
+ State vector ``(x, y, vx, vy)``.
812
+ t : float
813
+ Current time (unused; the system is autonomous in the rotating
814
+ frame).
815
+
816
+ Returns
817
+ -------
818
+ ndarray of float, shape (4,)
819
+ Time derivative ``(dx/dt, dy/dt, dvx/dt, dvy/dt)``.
820
+ """
821
+ return np.asarray(_restricted_three_body_rhs(np.asarray(state, dtype=np.float64), t, self.params))
822
+
823
+ def initial_state(self) -> NDArray[np.float64]:
824
+ """The classic Arenstorf periodic-orbit initial condition.
825
+
826
+ Returns
827
+ -------
828
+ ndarray of float, shape (4,)
829
+ """
830
+ return np.array([0.994, 0.0, 0.0, -2.00158510637908252240537862224])
831
+
832
+ def trajectory(
833
+ self,
834
+ state0: NDArray[np.float64] | None = None,
835
+ t0: float = 0.0,
836
+ dt: float = 0.0005,
837
+ n_steps: int = 40000,
838
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
839
+ """Integrate a trajectory with the Numba-accelerated RK4 integrator.
840
+
841
+ Parameters
842
+ ----------
843
+ state0 : array_like of float, shape (4,), optional
844
+ Initial state; defaults to :meth:`initial_state`.
845
+ t0 : float, default 0.0
846
+ Initial time.
847
+ dt : float, default 0.0005
848
+ Integration step size (small, since the Arenstorf orbit's default
849
+ initial condition passes very close to the smaller primary).
850
+ n_steps : int, default 40000
851
+ Number of integration steps.
852
+
853
+ Returns
854
+ -------
855
+ times : ndarray of float, shape (n_steps + 1,)
856
+ states : ndarray of float, shape (n_steps + 1, 4)
857
+ """
858
+ state0 = self.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
859
+ return rk4_integrate(_restricted_three_body_rhs, state0, t0, dt, n_steps, self.params)
860
+
861
+ def jacobi_constant(self, state: NDArray[np.float64]) -> float:
862
+ """The Jacobi constant: the CR3BP's conserved rotating-frame energy analog.
863
+
864
+ Parameters
865
+ ----------
866
+ state : array_like of float, shape (4,)
867
+ State vector ``(x, y, vx, vy)``.
868
+
869
+ Returns
870
+ -------
871
+ float
872
+ The Jacobi constant ``C = 2*Omega(x, y) - (vx^2 + vy^2)``, where
873
+ ``Omega`` is the effective (gravitational + centrifugal)
874
+ potential. Conserved along any trajectory; useful for checking
875
+ integrator fidelity (e.g. with
876
+ :func:`physicskit.chaos.utils.metrics.energy_drift`).
877
+ """
878
+ mu = self.mu
879
+ x, y, vx, vy = state
880
+ omega = effective_potential(x, y, mu)
881
+ return float(2.0 * omega - (vx * vx + vy * vy))
882
+
883
+
884
+ def effective_potential(x: NDArray[np.float64] | float, y: NDArray[np.float64] | float, mu: float) -> NDArray[np.float64]:
885
+ """The CR3BP's effective (gravitational + centrifugal) potential ``Omega(x, y)``.
886
+
887
+ Vectorized over `x`/`y` (works directly on ``np.meshgrid`` output), so it
888
+ is the building block for both
889
+ :meth:`RestrictedThreeBody.jacobi_constant` and that orbit's *zero-velocity
890
+ curve*: since ``C = 2*Omega(x, y) - speed**2``, a trajectory with Jacobi
891
+ constant `C` can only reach points where ``Omega(x, y) >= C / 2`` (speed
892
+ would otherwise be imaginary) -- the boundary ``Omega(x, y) == C / 2`` is
893
+ a curve the body can never cross, carving the plane into allowed and
894
+ forbidden ("Hill") regions.
895
+
896
+ Parameters
897
+ ----------
898
+ x, y : float or ndarray of float
899
+ Rotating-frame position(s).
900
+ mu : float
901
+ CR3BP mass parameter.
902
+
903
+ Returns
904
+ -------
905
+ ndarray of float
906
+ ``Omega(x, y) = 0.5*(x^2 + y^2) + (1 - mu)/r1 + mu/r2``.
907
+ """
908
+ x = np.asarray(x, dtype=np.float64)
909
+ y = np.asarray(y, dtype=np.float64)
910
+ r1 = np.sqrt((x + mu) ** 2 + y * y)
911
+ r2 = np.sqrt((x - 1.0 + mu) ** 2 + y * y)
912
+ return 0.5 * (x * x + y * y) + (1.0 - mu) / r1 + mu / r2
913
+
914
+
915
+ def lagrange_points(mu: float) -> NDArray[np.float64]:
916
+ """The five Lagrange (libration) equilibrium points of the CR3BP, in
917
+ the rotating frame, for mass parameter `mu`.
918
+
919
+ L1, L2, L3 are the collinear equilibria on the ``y = 0`` axis: points
920
+ where a body held at rest (``vx = vy = 0``) in the rotating frame feels
921
+ zero net acceleration, found here by root-finding
922
+ :func:`_restricted_three_body_rhs`'s ``ax`` output directly (rather
923
+ than re-deriving the quintic equations by hand) in the three intervals
924
+ it is known to have exactly one root each: between the primaries
925
+ (L1), beyond the smaller primary (L2), and beyond the larger primary,
926
+ on the opposite side (L3). L4 and L5 form equilateral triangles with
927
+ the two primaries and have the closed-form rotating-frame position
928
+ ``(0.5 - mu, +-sqrt(3)/2)``.
929
+
930
+ Parameters
931
+ ----------
932
+ mu : float
933
+ CR3BP mass parameter.
934
+
935
+ Returns
936
+ -------
937
+ ndarray of float, shape (5, 2)
938
+ ``(x, y)`` of L1 through L5, in that order.
939
+ """
940
+
941
+ def ax_residual(x: float) -> float:
942
+ state = np.array([x, 0.0, 0.0, 0.0])
943
+ return float(_restricted_three_body_rhs(state, 0.0, np.array([mu]))[2])
944
+
945
+ eps = 1e-8
946
+ x_l1 = brentq(ax_residual, -mu + eps, 1.0 - mu - eps)
947
+ x_l2 = brentq(ax_residual, 1.0 - mu + eps, 1.0 - mu + 2.0)
948
+ x_l3 = brentq(ax_residual, -mu - 2.0, -mu - eps)
949
+ return np.array(
950
+ [
951
+ [x_l1, 0.0],
952
+ [x_l2, 0.0],
953
+ [x_l3, 0.0],
954
+ [0.5 - mu, np.sqrt(3.0) / 2.0],
955
+ [0.5 - mu, -np.sqrt(3.0) / 2.0],
956
+ ]
957
+ )
958
+
959
+
960
+ # ---------------------------------------------------------------------------
961
+ # Magnetic pendulum
962
+ # ---------------------------------------------------------------------------
963
+
964
+
965
+ @njit(cache=True)
966
+ def magnetic_pendulum_rhs(state: NDArray[np.float64], t: float, params: NDArray[np.float64]) -> NDArray[np.float64]:
967
+ """Magnetic pendulum vector field ``dx/dt = f(x, t; params)``.
968
+
969
+ Public (not underscore-prefixed) so it can be passed directly to the
970
+ low-level, Numba-accelerated integrators in
971
+ :mod:`physicskit.chaos.core.integrators` for high-throughput grid computations
972
+ such as :func:`physicskit.chaos.visualizers.basins.basin_of_attraction`, which
973
+ needs to integrate one trajectory per pixel of a basin map.
974
+
975
+ Parameters
976
+ ----------
977
+ state : ndarray of float, shape (4,)
978
+ State vector ``(x, y, vx, vy)``.
979
+ t : float
980
+ Current time (unused; the system is autonomous).
981
+ params : ndarray of float, shape (4 + 2*n_magnets,)
982
+ ``(friction, spring, height, strength, mx_0, my_0, mx_1, my_1, ...)``:
983
+ see :class:`MagneticPendulum` for the meaning of each.
984
+
985
+ Returns
986
+ -------
987
+ ndarray of float, shape (4,)
988
+ Time derivative ``(dx/dt, dy/dt, dvx/dt, dvy/dt)``.
989
+ """
990
+ friction, spring, height, strength = params[0], params[1], params[2], params[3]
991
+ x, y, vx, vy = state[0], state[1], state[2], state[3]
992
+ n_magnets = (params.shape[0] - 4) // 2
993
+
994
+ ax = -friction * vx - spring * x
995
+ ay = -friction * vy - spring * y
996
+ for k in range(n_magnets):
997
+ mx = params[4 + 2 * k]
998
+ my = params[4 + 2 * k + 1]
999
+ dx = x - mx
1000
+ dy = y - my
1001
+ r3 = (dx * dx + dy * dy + height * height) ** 1.5
1002
+ ax -= strength * dx / r3
1003
+ ay -= strength * dy / r3
1004
+
1005
+ out = np.empty(4)
1006
+ out[0] = vx
1007
+ out[1] = vy
1008
+ out[2] = ax
1009
+ out[3] = ay
1010
+ return out
1011
+
1012
+
1013
+ class MagneticPendulum(DynamicalSystem):
1014
+ """A pendulum bob swinging over several fixed magnets: a multistable, chaotic system.
1015
+
1016
+ A damped pendulum bob, modeled in the small-swing (flat, 2D) limit, is
1017
+ pulled down toward the origin by a linear restoring force and attracted
1018
+ toward each of several fixed magnets by an inverse-square-like force
1019
+ (softened by a "height" offset `height`, the bob's height above the
1020
+ magnet plane, which avoids a force singularity directly above a magnet).
1021
+ Friction eventually settles the bob at rest near whichever magnet "won" --
1022
+ but *which* magnet wins depends on the starting position with famously
1023
+ fractal sensitivity, making this the classic system for visualizing
1024
+ fractal basin boundaries; see
1025
+ :func:`physicskit.chaos.visualizers.basins.plot_basin_of_attraction`.
1026
+
1027
+ Parameters
1028
+ ----------
1029
+ magnet_positions : array_like of float, shape (n_magnets, 2), optional
1030
+ Magnet ``(x, y)`` positions; defaults to 3 magnets at the vertices of
1031
+ an equilateral triangle inscribed in the unit circle.
1032
+ friction : float, default 0.2
1033
+ Damping coefficient.
1034
+ spring : float, default 0.2
1035
+ Linear restoring-force coefficient (pulling the bob back toward the
1036
+ origin, as in the small-swing limit of gravity).
1037
+ height : float, default 0.2
1038
+ The bob's height above the magnet plane; softens the force near a
1039
+ magnet (larger values give a gentler, less singular pull).
1040
+ strength : float, default 1.0
1041
+ Magnet attraction strength.
1042
+
1043
+ Attributes
1044
+ ----------
1045
+ magnet_positions : ndarray of float, shape (n_magnets, 2)
1046
+ Magnet positions.
1047
+ friction, spring, height, strength : float
1048
+ System parameters.
1049
+ """
1050
+
1051
+ #: State dimension, always 4. State is ``(x, y, vx, vy)``.
1052
+ dim = 4
1053
+
1054
+ def __init__(
1055
+ self,
1056
+ magnet_positions: NDArray[np.float64] | None = None,
1057
+ friction: float = 0.2,
1058
+ spring: float = 0.2,
1059
+ height: float = 0.2,
1060
+ strength: float = 1.0,
1061
+ ):
1062
+ if magnet_positions is None:
1063
+ angles = np.array([90.0, 210.0, 330.0]) * np.pi / 180.0
1064
+ magnet_positions = np.column_stack([np.cos(angles), np.sin(angles)])
1065
+ self.magnet_positions = np.asarray(magnet_positions, dtype=np.float64)
1066
+ self.friction = float(friction)
1067
+ self.spring = float(spring)
1068
+ self.height = float(height)
1069
+ self.strength = float(strength)
1070
+
1071
+ @property
1072
+ def params(self) -> NDArray[np.float64]:
1073
+ """Parameter vector ``(friction, spring, height, strength, mx_0, my_0, ...)``.
1074
+
1075
+ Returns
1076
+ -------
1077
+ ndarray of float, shape (4 + 2*n_magnets,)
1078
+ """
1079
+ return np.concatenate(
1080
+ [
1081
+ [self.friction, self.spring, self.height, self.strength],
1082
+ self.magnet_positions.ravel(),
1083
+ ]
1084
+ )
1085
+
1086
+ def rhs(self, state: NDArray[np.float64], t: float) -> NDArray[np.float64]:
1087
+ """Evaluate the magnetic pendulum vector field.
1088
+
1089
+ Parameters
1090
+ ----------
1091
+ state : ndarray of float, shape (4,)
1092
+ State vector ``(x, y, vx, vy)``.
1093
+ t : float
1094
+ Current time (unused; the system is autonomous).
1095
+
1096
+ Returns
1097
+ -------
1098
+ ndarray of float, shape (4,)
1099
+ Time derivative ``(dx/dt, dy/dt, dvx/dt, dvy/dt)``.
1100
+ """
1101
+ return np.asarray(magnetic_pendulum_rhs(np.asarray(state, dtype=np.float64), t, self.params))
1102
+
1103
+ def initial_state(self) -> NDArray[np.float64]:
1104
+ """Default initial condition: released from rest, off-center.
1105
+
1106
+ Returns
1107
+ -------
1108
+ ndarray of float, shape (4,)
1109
+ """
1110
+ return np.array([0.5, 0.5, 0.0, 0.0])
1111
+
1112
+ def trajectory(
1113
+ self,
1114
+ state0: NDArray[np.float64] | None = None,
1115
+ t0: float = 0.0,
1116
+ dt: float = 0.02,
1117
+ n_steps: int = 5000,
1118
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
1119
+ """Integrate a trajectory with the Numba-accelerated RK4 integrator.
1120
+
1121
+ Parameters
1122
+ ----------
1123
+ state0 : array_like of float, shape (4,), optional
1124
+ Initial state; defaults to :meth:`initial_state`.
1125
+ t0 : float, default 0.0
1126
+ Initial time.
1127
+ dt : float, default 0.02
1128
+ Integration step size.
1129
+ n_steps : int, default 5000
1130
+ Number of integration steps.
1131
+
1132
+ Returns
1133
+ -------
1134
+ times : ndarray of float, shape (n_steps + 1,)
1135
+ states : ndarray of float, shape (n_steps + 1, 4)
1136
+ """
1137
+ state0 = self.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
1138
+ return rk4_integrate(magnetic_pendulum_rhs, state0, t0, dt, n_steps, self.params)
1139
+
1140
+
1141
+ # ---------------------------------------------------------------------------
1142
+ # Driven, damped pendulum
1143
+ # ---------------------------------------------------------------------------
1144
+
1145
+
1146
+ @njit(cache=True)
1147
+ def _driven_pendulum_rhs(state: NDArray[np.float64], t: float, params: NDArray[np.float64]) -> NDArray[np.float64]:
1148
+ """Driven-pendulum vector field ``dx/dt = f(x, t; damping, g_over_l, A, omega_d)``.
1149
+
1150
+ Parameters
1151
+ ----------
1152
+ state : ndarray of float, shape (2,)
1153
+ State vector ``(theta, omega)``.
1154
+ t : float
1155
+ Current time (the forcing term depends on `t`).
1156
+ params : ndarray of float, shape (4,)
1157
+ Parameters ``(damping, g_over_l, A, omega_d)``.
1158
+
1159
+ Returns
1160
+ -------
1161
+ ndarray of float, shape (2,)
1162
+ Time derivative ``(dtheta/dt, domega/dt)``.
1163
+ """
1164
+ damping, g_over_l, A, omega_d = params[0], params[1], params[2], params[3]
1165
+ theta, omega = state[0], state[1]
1166
+ out = np.empty(2)
1167
+ out[0] = omega
1168
+ out[1] = -damping * omega - g_over_l * np.sin(theta) + A * np.cos(omega_d * t)
1169
+ return out
1170
+
1171
+
1172
+ class DrivenPendulum(DynamicalSystem):
1173
+ """The damped, sinusoidally forced pendulum.
1174
+
1175
+ Governed by ``theta'' + damping*theta' + (g/l)*sin(theta) = A*cos(omega_d*t)``
1176
+ -- the classic mechanical system for the period-doubling route to chaos
1177
+ (Baker & Gollub, *Chaotic Dynamics: An Introduction*): for fixed
1178
+ damping and forcing frequency, sweeping the forcing amplitude `A`
1179
+ produces the same period-doubling cascade as the logistic map. Feed
1180
+ :meth:`rhs` to
1181
+ :func:`physicskit.chaos.visualizers.bifurcation.stroboscopic_bifurcation_sampler`
1182
+ (sampling `theta` once per forcing period ``2*pi/omega_d``) to build
1183
+ that bifurcation diagram directly with
1184
+ :func:`physicskit.chaos.visualizers.bifurcation.bifurcation_diagram`.
1185
+
1186
+ Parameters
1187
+ ----------
1188
+ damping : float, default 0.5
1189
+ Viscous damping coefficient.
1190
+ g_over_l : float, default 1.0
1191
+ ``g/l``, the undamped/undriven pendulum's small-angle frequency
1192
+ squared.
1193
+ A : float, default 1.5
1194
+ Forcing torque amplitude; the default, with the other defaults
1195
+ below, is deep in the chaotic regime.
1196
+ omega_d : float, default 2.0/3.0
1197
+ Forcing angular frequency.
1198
+
1199
+ Attributes
1200
+ ----------
1201
+ damping, g_over_l, A, omega_d : float
1202
+ System parameters.
1203
+ """
1204
+
1205
+ #: State dimension, always 2. State is ``(theta, omega)``.
1206
+ dim = 2
1207
+
1208
+ def __init__(self, damping: float = 0.5, g_over_l: float = 1.0, A: float = 1.5, omega_d: float = 2.0 / 3.0):
1209
+ self.damping = float(damping)
1210
+ self.g_over_l = float(g_over_l)
1211
+ self.A = float(A)
1212
+ self.omega_d = float(omega_d)
1213
+
1214
+ @property
1215
+ def params(self) -> NDArray[np.float64]:
1216
+ """Parameter vector ``(damping, g_over_l, A, omega_d)``.
1217
+
1218
+ Returns
1219
+ -------
1220
+ ndarray of float, shape (4,)
1221
+ """
1222
+ return np.array([self.damping, self.g_over_l, self.A, self.omega_d])
1223
+
1224
+ def rhs(self, state: NDArray[np.float64], t: float) -> NDArray[np.float64]:
1225
+ """Evaluate the driven-pendulum vector field.
1226
+
1227
+ Parameters
1228
+ ----------
1229
+ state : ndarray of float, shape (2,)
1230
+ State vector ``(theta, omega)``.
1231
+ t : float
1232
+ Current time (the forcing term depends on `t`).
1233
+
1234
+ Returns
1235
+ -------
1236
+ ndarray of float, shape (2,)
1237
+ Time derivative ``(dtheta/dt, domega/dt)``.
1238
+ """
1239
+ return np.asarray(_driven_pendulum_rhs(np.asarray(state, dtype=np.float64), t, self.params))
1240
+
1241
+ def initial_state(self) -> NDArray[np.float64]:
1242
+ """Default initial condition: small angle, at rest.
1243
+
1244
+ Returns
1245
+ -------
1246
+ ndarray of float, shape (2,)
1247
+ """
1248
+ return np.array([0.2, 0.0])
1249
+
1250
+ def trajectory(
1251
+ self,
1252
+ state0: NDArray[np.float64] | None = None,
1253
+ t0: float = 0.0,
1254
+ dt: float = 0.02,
1255
+ n_steps: int = 5000,
1256
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
1257
+ """Integrate a trajectory with the Numba-accelerated RK4 integrator.
1258
+
1259
+ Parameters
1260
+ ----------
1261
+ state0 : array_like of float, shape (2,), optional
1262
+ Initial state; defaults to :meth:`initial_state`.
1263
+ t0 : float, default 0.0
1264
+ Initial time.
1265
+ dt : float, default 0.02
1266
+ Integration step size.
1267
+ n_steps : int, default 5000
1268
+ Number of integration steps.
1269
+
1270
+ Returns
1271
+ -------
1272
+ times : ndarray of float, shape (n_steps + 1,)
1273
+ states : ndarray of float, shape (n_steps + 1, 2)
1274
+ """
1275
+ state0 = self.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
1276
+ return rk4_integrate(_driven_pendulum_rhs, state0, t0, dt, n_steps, self.params)