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,273 @@
1
+ """Generic bifurcation diagrams: sweep a parameter, plot the attractor cross-section.
2
+
3
+ A bifurcation diagram sweeps one parameter of a system and, for each value,
4
+ plots the "surviving" long-term values of some observable after discarding
5
+ an initial transient: a single point for a stable fixed point, a handful of
6
+ points for a period-``k`` cycle, and a dense band for chaos. The core
7
+ :func:`bifurcation_diagram` function is deliberately sampling-agnostic (it
8
+ just calls a user-supplied ``sample_fn(param) -> ndarray`` for each parameter
9
+ value), so it works equally well for:
10
+
11
+ - **Discrete maps** (:class:`physicskit.chaos.core.base_system.DiscreteMap`), where
12
+ the natural "sample" is the tail of the map's own iterated trajectory --
13
+ see :func:`map_bifurcation_sampler`.
14
+ - **Continuous, periodically-forced flows**
15
+ (:class:`physicskit.chaos.core.base_system.DynamicalSystem`), where the natural
16
+ "sample" is a *stroboscopic* Poincare section: the state sampled once per
17
+ forcing period, which turns the continuous flow into an effective discrete
18
+ map -- see :func:`stroboscopic_bifurcation_sampler`.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from collections.abc import Callable
24
+ from concurrent.futures import ThreadPoolExecutor
25
+ from typing import Any, cast
26
+
27
+ import matplotlib.pyplot as plt
28
+ import numpy as np
29
+ from matplotlib.axes import Axes
30
+ from matplotlib.figure import Figure
31
+ from numpy.typing import ArrayLike, NDArray
32
+ from scipy.integrate import solve_ivp
33
+ from tqdm.auto import tqdm
34
+
35
+ from physicskit.chaos.core.base_system import DiscreteMap, DynamicalSystem
36
+ from physicskit.chaos.visualizers import theme
37
+
38
+ #: A function mapping one parameter value to the array of long-term
39
+ #: "surviving" observable values sampled for that parameter.
40
+ SampleFunc = Callable[[float], NDArray[np.float64]]
41
+
42
+
43
+ def bifurcation_diagram(
44
+ param_values: ArrayLike,
45
+ sample_fn: SampleFunc,
46
+ n_jobs: int = 1,
47
+ show_progress: bool = False,
48
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
49
+ """Generate raw bifurcation-diagram data by sweeping a parameter.
50
+
51
+ Parameters
52
+ ----------
53
+ param_values : array_like of float, shape (n_params,)
54
+ Parameter values to sweep.
55
+ sample_fn : callable
56
+ Function ``sample_fn(param) -> ndarray`` returning the array of
57
+ long-term ("surviving", post-transient) observable values for one
58
+ parameter value. See :func:`map_bifurcation_sampler` and
59
+ :func:`stroboscopic_bifurcation_sampler` for ready-made samplers.
60
+ n_jobs : int, default 1
61
+ Number of worker threads to evaluate `sample_fn` with, via
62
+ :class:`concurrent.futures.ThreadPoolExecutor`. Each parameter value
63
+ is fully independent, so this is embarrassingly parallel -- but the
64
+ speedup depends on `sample_fn` releasing the GIL while it runs:
65
+ :func:`stroboscopic_bifurcation_sampler` does (via
66
+ :func:`scipy.integrate.solve_ivp`), and so does
67
+ :func:`map_bifurcation_sampler` (the underlying Numba kernels are
68
+ compiled with ``nogil=True``); an arbitrary pure-Python `sample_fn`
69
+ will not see a speedup. ``1`` (the default) runs sequentially with no
70
+ thread-pool overhead.
71
+ show_progress : bool, default False
72
+ Display a `tqdm` progress bar over parameter values.
73
+
74
+ Returns
75
+ -------
76
+ params : ndarray of float, shape (N,)
77
+ Parameter value repeated once per sample.
78
+ samples : ndarray of float, shape (N,)
79
+ The concatenated samples from every parameter value, aligned with
80
+ `params` (``N`` is the total sample count summed across all
81
+ parameter values, not necessarily ``n_params`` times a fixed count).
82
+ """
83
+ params = np.atleast_1d(np.asarray(param_values, dtype=np.float64))
84
+
85
+ if n_jobs == 1:
86
+ results = [sample_fn(float(p)) for p in tqdm(params, disable=not show_progress, desc="sweeping")]
87
+ else:
88
+ with ThreadPoolExecutor(max_workers=n_jobs) as pool:
89
+ results = list(
90
+ tqdm(
91
+ pool.map(lambda p: sample_fn(float(p)), params),
92
+ total=params.shape[0],
93
+ disable=not show_progress,
94
+ desc="sweeping",
95
+ )
96
+ )
97
+
98
+ all_params = []
99
+ all_samples = []
100
+ for p, raw_samples in zip(params, results):
101
+ samples = np.atleast_1d(np.asarray(raw_samples, dtype=np.float64))
102
+ all_params.append(np.full(samples.shape[0], p, dtype=np.float64))
103
+ all_samples.append(samples)
104
+ return np.concatenate(all_params), np.concatenate(all_samples)
105
+
106
+
107
+ def map_bifurcation_sampler(
108
+ make_map: Callable[[float], DiscreteMap],
109
+ state0: ArrayLike,
110
+ n_transient: int,
111
+ n_keep: int,
112
+ component: int = 0,
113
+ ) -> SampleFunc:
114
+ """Build a :func:`bifurcation_diagram` sampler for a :class:`~physicskit.chaos.core.base_system.DiscreteMap`.
115
+
116
+ Parameters
117
+ ----------
118
+ make_map : callable
119
+ Function ``make_map(param) -> DiscreteMap`` constructing a fresh map
120
+ instance for one parameter value.
121
+ state0 : array_like of float, shape (dim,)
122
+ Initial state to iterate from, for every parameter value.
123
+ n_transient : int
124
+ Number of initial iterations to discard.
125
+ n_keep : int
126
+ Number of iterations to keep (and return) after the transient.
127
+ component : int, default 0
128
+ Index of the state component to sample.
129
+
130
+ Returns
131
+ -------
132
+ callable
133
+ A function ``sample_fn(param) -> ndarray`` of shape ``(n_keep,)``,
134
+ suitable as the `sample_fn` argument to :func:`bifurcation_diagram`.
135
+ """
136
+
137
+ def sample_fn(param: float) -> NDArray[np.float64]:
138
+ system = make_map(param)
139
+ trajectory = system.trajectory(np.asarray(state0, dtype=np.float64), n_transient + n_keep)
140
+ return trajectory[n_transient:, component]
141
+
142
+ return sample_fn
143
+
144
+
145
+ def stroboscopic_bifurcation_sampler(
146
+ make_system: Callable[[float], DynamicalSystem],
147
+ state0: ArrayLike,
148
+ sample_period: float,
149
+ n_transient_periods: int,
150
+ n_keep_periods: int,
151
+ component: int = 0,
152
+ dt: float = 0.01,
153
+ rtol: float = 1e-9,
154
+ atol: float = 1e-11,
155
+ ) -> SampleFunc:
156
+ """Build a :func:`bifurcation_diagram` sampler for a periodically-forced continuous flow.
157
+
158
+ Integrates the system and records its state stroboscopically -- once
159
+ every `sample_period` (typically the forcing period of a driven
160
+ oscillator such as :class:`physicskit.chaos.systems.continuous.Duffing`) -- which
161
+ turns the continuous flow into an effective discrete map for bifurcation
162
+ purposes. Uses :func:`scipy.integrate.solve_ivp` against the system's
163
+ plain-Python :meth:`~physicskit.chaos.core.base_system.DynamicalSystem.rhs`
164
+ (rather than the Numba-jitted low-level integrators, which require a
165
+ jitted right-hand side that arbitrary systems may not expose), evaluated
166
+ exactly at the stroboscopic sample times.
167
+
168
+ Parameters
169
+ ----------
170
+ make_system : callable
171
+ Function ``make_system(param) -> DynamicalSystem`` constructing a
172
+ fresh system instance for one parameter value.
173
+ state0 : array_like of float, shape (dim,)
174
+ Initial state to integrate from, for every parameter value.
175
+ sample_period : float
176
+ Time between stroboscopic samples (typically the forcing period).
177
+ n_transient_periods : int
178
+ Number of initial stroboscopic samples to discard.
179
+ n_keep_periods : int
180
+ Number of stroboscopic samples to keep (and return) after the
181
+ transient.
182
+ component : int, default 0
183
+ Index of the state component to sample.
184
+ dt : float, default 0.01
185
+ Approximate internal integration step size passed to `solve_ivp` as
186
+ `max_step` (the adaptive solver still lands exactly on each
187
+ stroboscopic sample time via `t_eval`). A parameter sweep calls this
188
+ once per parameter value, so loosening `dt`, `rtol`, and `atol` can
189
+ matter a great deal for the total sweep's running time.
190
+ rtol, atol : float, default 1e-9, 1e-11
191
+ Relative and absolute error tolerances passed to `solve_ivp`.
192
+
193
+ Returns
194
+ -------
195
+ callable
196
+ A function ``sample_fn(param) -> ndarray`` of shape
197
+ ``(n_keep_periods,)``, suitable as the `sample_fn` argument to
198
+ :func:`bifurcation_diagram`.
199
+ """
200
+ n_periods = n_transient_periods + n_keep_periods
201
+ sample_times = sample_period * np.arange(1, n_periods + 1)
202
+
203
+ def sample_fn(param: float) -> NDArray[np.float64]:
204
+ system = make_system(param)
205
+
206
+ def _rhs(t: float, state: NDArray[np.float64]) -> NDArray[np.float64]:
207
+ return system.rhs(state, t)
208
+
209
+ solution = solve_ivp(
210
+ _rhs,
211
+ (0.0, sample_times[-1]),
212
+ np.asarray(state0, dtype=np.float64),
213
+ t_eval=sample_times,
214
+ max_step=dt,
215
+ rtol=rtol,
216
+ atol=atol,
217
+ )
218
+ stroboscopic = solution.y[component]
219
+ return np.asarray(stroboscopic[n_transient_periods:], dtype=np.float64)
220
+
221
+ return sample_fn
222
+
223
+
224
+ def plot_bifurcation_diagram(
225
+ param_values: ArrayLike,
226
+ sample_fn: SampleFunc,
227
+ ax: Axes | None = None,
228
+ n_jobs: int = 1,
229
+ show_progress: bool = False,
230
+ **scatter_kwargs: Any,
231
+ ) -> tuple[Figure, Axes]:
232
+ """Plot a bifurcation diagram.
233
+
234
+ Parameters
235
+ ----------
236
+ param_values : array_like of float, shape (n_params,)
237
+ Parameter values to sweep.
238
+ sample_fn : callable
239
+ Function ``sample_fn(param) -> ndarray`` returning the long-term
240
+ surviving observable values for one parameter value; see
241
+ :func:`map_bifurcation_sampler` / :func:`stroboscopic_bifurcation_sampler`.
242
+ ax : matplotlib.axes.Axes, optional
243
+ Axes to draw into; a new figure and axes are created if omitted.
244
+ n_jobs : int, default 1
245
+ See :func:`bifurcation_diagram`.
246
+ show_progress : bool, default False
247
+ Display a `tqdm` progress bar over parameter values.
248
+ **scatter_kwargs
249
+ Additional keyword arguments forwarded to ``ax.plot``.
250
+
251
+ Returns
252
+ -------
253
+ fig : matplotlib.figure.Figure
254
+ ax : matplotlib.axes.Axes
255
+ """
256
+ params, samples = bifurcation_diagram(param_values, sample_fn, n_jobs=n_jobs, show_progress=show_progress)
257
+ param_array = np.asarray(param_values, dtype=np.float64)
258
+
259
+ if ax is None:
260
+ fig, ax = plt.subplots(figsize=(10, 6))
261
+ else:
262
+ fig = cast(Figure, ax.figure)
263
+
264
+ kwargs: dict[str, Any] = {
265
+ "marker": ",",
266
+ "linestyle": "none",
267
+ "color": theme.STRUCTURE,
268
+ "alpha": 0.6,
269
+ }
270
+ kwargs.update(scatter_kwargs)
271
+ ax.plot(params, samples, **kwargs)
272
+ ax.set_xlim(float(np.min(param_array)), float(np.max(param_array)))
273
+ return fig, ax
@@ -0,0 +1,374 @@
1
+ """Live trajectory-separation tracking to visually demonstrate sensitivity to
2
+ initial conditions (a positive largest Lyapunov exponent)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, cast
7
+
8
+ import matplotlib.pyplot as plt
9
+ import numpy as np
10
+ from matplotlib.axes import Axes
11
+ from matplotlib.figure import Figure
12
+ from numpy.typing import ArrayLike, NDArray
13
+ from scipy.integrate import solve_ivp
14
+
15
+ from physicskit.chaos.core.base_system import BilliardSystem, DynamicalSystem
16
+ from physicskit.chaos.utils.metrics import lyapunov_exponent_from_divergence
17
+ from physicskit.chaos.visualizers import theme
18
+
19
+
20
+ def trajectory_divergence(
21
+ system: DynamicalSystem,
22
+ state0: ArrayLike | None = None,
23
+ delta_0: float = 1e-8,
24
+ t_max: float = 100.0,
25
+ n_points: int = 2000,
26
+ direction: ArrayLike | None = None,
27
+ seed: int | None = None,
28
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
29
+ """Integrate two nearby trajectories and track their separation over time.
30
+
31
+ Parameters
32
+ ----------
33
+ system : DynamicalSystem
34
+ System to integrate.
35
+ state0 : array_like of float, shape (dim,), optional
36
+ Initial state of the reference trajectory; defaults to
37
+ ``system.initial_state()``.
38
+ delta_0 : float, default 1e-8
39
+ Initial separation between the reference and perturbed trajectories.
40
+ t_max : float, default 100.0
41
+ Total integration time.
42
+ n_points : int, default 2000
43
+ Number of evenly-spaced time points to evaluate.
44
+ direction : array_like of float, shape (dim,), optional
45
+ Direction of the initial perturbation; a random unit vector is used
46
+ if omitted (normalized internally regardless).
47
+ seed : int, optional
48
+ Seed for the random direction generator, for reproducibility.
49
+
50
+ Returns
51
+ -------
52
+ t : ndarray of float, shape (n_points,)
53
+ Time points.
54
+ delta_t : ndarray of float, shape (n_points,)
55
+ Euclidean separation between the two trajectories at each time in `t`.
56
+ """
57
+ state0 = system.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
58
+ rng = np.random.default_rng(seed)
59
+ direction = rng.normal(size=system.dim) if direction is None else np.asarray(direction, dtype=np.float64)
60
+ direction = direction / float(np.linalg.norm(direction))
61
+ state0_perturbed = state0 + delta_0 * direction
62
+
63
+ t_eval = np.linspace(0.0, t_max, n_points)
64
+
65
+ def _rhs(t: float, state: NDArray[np.float64]) -> NDArray[np.float64]:
66
+ return system.rhs(state, t)
67
+
68
+ sol1 = solve_ivp(_rhs, (0.0, t_max), state0, t_eval=t_eval, rtol=1e-9, atol=1e-11)
69
+ sol2 = solve_ivp(_rhs, (0.0, t_max), state0_perturbed, t_eval=t_eval, rtol=1e-9, atol=1e-11)
70
+
71
+ delta_t = np.asarray(np.linalg.norm(sol1.y - sol2.y, axis=0), dtype=np.float64)
72
+ return sol1.t, delta_t
73
+
74
+
75
+ def plot_lyapunov_divergence(
76
+ system: DynamicalSystem,
77
+ delta_0: float = 1e-8,
78
+ t_max: float = 100.0,
79
+ n_points: int = 2000,
80
+ state0: ArrayLike | None = None,
81
+ fit_fraction: float = 0.5,
82
+ ax: Axes | None = None,
83
+ seed: int | None = None,
84
+ ) -> tuple[Figure, Axes, float]:
85
+ """Plot ``ln(delta_t / delta_0)`` vs. `t` for two nearby trajectories.
86
+
87
+ A straight, positively-sloped line demonstrates exponential divergence
88
+ (``lambda_max > 0``); the slope is estimated by a linear fit over the
89
+ first `fit_fraction` of the time range, before the separation saturates
90
+ at the attractor's scale.
91
+
92
+ Parameters
93
+ ----------
94
+ system : DynamicalSystem
95
+ System to integrate.
96
+ delta_0 : float, default 1e-8
97
+ Initial separation between the reference and perturbed trajectories.
98
+ t_max : float, default 100.0
99
+ Total integration time.
100
+ n_points : int, default 2000
101
+ Number of evenly-spaced time points to evaluate.
102
+ state0 : array_like of float, shape (dim,), optional
103
+ Initial state of the reference trajectory; defaults to
104
+ ``system.initial_state()``.
105
+ fit_fraction : float, default 0.5
106
+ Fraction of the time range (from ``t=0``) used to fit the
107
+ exponential-growth slope.
108
+ ax : matplotlib.axes.Axes, optional
109
+ Axes to draw into; a new figure and axes are created if omitted.
110
+ seed : int, optional
111
+ Seed for the random perturbation direction, for reproducibility.
112
+
113
+ Returns
114
+ -------
115
+ fig : matplotlib.figure.Figure
116
+ ax : matplotlib.axes.Axes
117
+ lambda_max : float
118
+ Estimated largest Lyapunov exponent from the fitted slope.
119
+ """
120
+ t, delta_t = trajectory_divergence(system, state0=state0, delta_0=delta_0, t_max=t_max, n_points=n_points, seed=seed)
121
+ log_ratio = np.log(np.clip(delta_t, 1e-300, None) / delta_0)
122
+
123
+ n_fit = max(2, int(fit_fraction * len(t)))
124
+ lam = lyapunov_exponent_from_divergence(t[:n_fit], delta_t[:n_fit] / delta_0)
125
+
126
+ if ax is None:
127
+ fig, ax = plt.subplots(figsize=(7, 5))
128
+ else:
129
+ fig = cast(Figure, ax.figure)
130
+
131
+ ax.plot(t, log_ratio, lw=1.2, color=theme.PRIMARY, label=r"$\ln(\delta_t/\delta_0)$")
132
+ ax.plot(
133
+ t[:n_fit],
134
+ lam * t[:n_fit],
135
+ "--",
136
+ color=theme.MUTED,
137
+ label=rf"fit: $\lambda_{{max}} \approx {lam:.3f}$",
138
+ )
139
+ ax.set_xlabel("t")
140
+ ax.set_ylabel(r"$\ln(\delta_t / \delta_0)$")
141
+ ax.set_title(f"{system.__class__.__name__} trajectory divergence")
142
+ ax.legend()
143
+ return fig, ax, lam
144
+
145
+
146
+ def trajectory_ensemble(
147
+ system: DynamicalSystem,
148
+ state0: ArrayLike | None = None,
149
+ n_members: int = 40,
150
+ spread: float = 1e-4,
151
+ dt: float = 0.01,
152
+ n_steps: int = 10000,
153
+ seed: int | None = None,
154
+ ) -> NDArray[np.float64]:
155
+ """Integrate an ensemble of trajectories launched from a small ball
156
+ around a shared initial state.
157
+
158
+ Visualizing the result (e.g. with :func:`plot_trajectory_ensemble`)
159
+ makes sensitivity to initial conditions directly visible: a regular
160
+ orbit's members stay bundled together for the whole run, while a
161
+ chaotic orbit's members visibly fan out, in contrast to
162
+ :func:`trajectory_divergence`, which only tracks a *single* pair's
163
+ separation as a number.
164
+
165
+ Parameters
166
+ ----------
167
+ system : DynamicalSystem
168
+ System to integrate.
169
+ state0 : array_like of float, shape (dim,), optional
170
+ Center of the initial ball; defaults to ``system.initial_state()``.
171
+ n_members : int, default 40
172
+ Number of trajectories in the ensemble.
173
+ spread : float, default 1e-4
174
+ Radius of the ball each member's initial state is drawn from.
175
+ dt : float, default 0.01
176
+ Integration step size.
177
+ n_steps : int, default 10000
178
+ Number of integration steps per trajectory.
179
+ seed : int, optional
180
+ Seed for the random initial-state offsets, for reproducibility.
181
+
182
+ Returns
183
+ -------
184
+ ndarray of float, shape (n_members, n_steps + 1, dim)
185
+ Every member's full trajectory.
186
+ """
187
+ state0 = system.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
188
+ rng = np.random.default_rng(seed)
189
+
190
+ all_states = np.empty((n_members, n_steps + 1, system.dim))
191
+ for i in range(n_members):
192
+ offset = rng.normal(size=system.dim)
193
+ offset *= spread / float(np.linalg.norm(offset))
194
+ _, states = system.trajectory(state0=state0 + offset, dt=dt, n_steps=n_steps)
195
+ all_states[i] = states
196
+ return all_states
197
+
198
+
199
+ def plot_trajectory_ensemble(
200
+ system: DynamicalSystem,
201
+ state0: ArrayLike | None = None,
202
+ n_members: int = 40,
203
+ spread: float = 1e-4,
204
+ dt: float = 0.01,
205
+ n_steps: int = 10000,
206
+ x_index: int = 0,
207
+ y_index: int = 1,
208
+ seed: int | None = None,
209
+ ax: Axes | None = None,
210
+ **line_kwargs: Any,
211
+ ) -> tuple[Figure, Axes]:
212
+ """Plot an ensemble of nearby trajectories overlaid on the same axes.
213
+
214
+ Parameters
215
+ ----------
216
+ system : DynamicalSystem
217
+ System to integrate.
218
+ state0 : array_like of float, shape (dim,), optional
219
+ Center of the initial ball; defaults to ``system.initial_state()``.
220
+ n_members : int, default 40
221
+ Number of trajectories in the ensemble.
222
+ spread : float, default 1e-4
223
+ Radius of the ball each member's initial state is drawn from.
224
+ dt : float, default 0.01
225
+ Integration step size.
226
+ n_steps : int, default 10000
227
+ Number of integration steps per trajectory.
228
+ x_index, y_index : int, default 0, 1
229
+ State indices to use as the plotted x/y coordinates.
230
+ seed : int, optional
231
+ Seed for the random initial-state offsets, for reproducibility.
232
+ ax : matplotlib.axes.Axes, optional
233
+ Axes to draw into; a new figure and axes are created if omitted.
234
+ **line_kwargs
235
+ Additional keyword arguments forwarded to every ``ax.plot`` call.
236
+
237
+ Returns
238
+ -------
239
+ fig : matplotlib.figure.Figure
240
+ ax : matplotlib.axes.Axes
241
+ """
242
+ all_states = trajectory_ensemble(system, state0=state0, n_members=n_members, spread=spread, dt=dt, n_steps=n_steps, seed=seed)
243
+
244
+ if ax is None:
245
+ fig, ax = plt.subplots(figsize=(7, 6))
246
+ else:
247
+ fig = cast(Figure, ax.figure)
248
+
249
+ kwargs: dict[str, Any] = {"color": theme.PRIMARY, "lw": 0.5, "alpha": 0.5}
250
+ kwargs.update(line_kwargs)
251
+ for states in all_states:
252
+ ax.plot(states[:, x_index], states[:, y_index], **kwargs)
253
+ ax.set_title(f"{system.__class__.__name__} trajectory ensemble ({n_members} members, spread={spread:g})")
254
+ return fig, ax
255
+
256
+
257
+ def billiard_trajectory_divergence(
258
+ billiard: BilliardSystem,
259
+ pos: ArrayLike,
260
+ vel: ArrayLike,
261
+ delta_0: float = 1e-8,
262
+ n_bounces: int = 200,
263
+ ) -> tuple[NDArray[np.int64], NDArray[np.float64]]:
264
+ """Trace two nearby billiard rays and track their bounce-by-bounce separation.
265
+
266
+ Unlike :func:`trajectory_divergence` (continuous flows, compared at
267
+ matched *times*), two billiard trajectories are naturally compared at
268
+ matched *bounce number*: the reference ray and a ray launched at a
269
+ slightly perturbed angle both hit the boundary `n_bounces` times, and
270
+ the Euclidean distance between their `k`-th hit points is tracked as `k`
271
+ increases.
272
+
273
+ Parameters
274
+ ----------
275
+ billiard : BilliardSystem
276
+ Billiard to trace both rays within.
277
+ pos : array_like of float, shape (2,)
278
+ Shared initial position ``(x, y)``; must lie in the billiard's
279
+ interior.
280
+ vel : array_like of float, shape (2,)
281
+ Initial velocity direction ``(vx, vy)`` of the reference ray;
282
+ normalized internally.
283
+ delta_0 : float, default 1e-8
284
+ Initial angular perturbation (radians) applied to `vel` to launch
285
+ the second ray.
286
+ n_bounces : int, default 200
287
+ Number of reflections to trace for each ray.
288
+
289
+ Returns
290
+ -------
291
+ bounce : ndarray of int, shape (n_bounces,)
292
+ Bounce index, ``1, 2, ..., n_bounces``.
293
+ separation : ndarray of float, shape (n_bounces,)
294
+ Euclidean distance between the two rays' hit points at each bounce.
295
+ """
296
+ vel_ref = np.asarray(vel, dtype=np.float64)
297
+ vel_ref = vel_ref / np.linalg.norm(vel_ref)
298
+ angle = float(np.arctan2(vel_ref[1], vel_ref[0])) + delta_0
299
+ vel_perturbed = np.array([np.cos(angle), np.sin(angle)])
300
+
301
+ result = billiard.simulate(pos, vel_ref, n_bounces)
302
+ result_perturbed = billiard.simulate(pos, vel_perturbed, n_bounces)
303
+
304
+ separation = np.hypot(result["x"] - result_perturbed["x"], result["y"] - result_perturbed["y"])
305
+ bounce = np.arange(1, n_bounces + 1)
306
+ return bounce, separation
307
+
308
+
309
+ def plot_billiard_divergence(
310
+ billiard: BilliardSystem,
311
+ pos: ArrayLike,
312
+ vel: ArrayLike,
313
+ delta_0: float = 1e-8,
314
+ n_bounces: int = 200,
315
+ fit_fraction: float = 0.5,
316
+ ax: Axes | None = None,
317
+ ) -> tuple[Figure, Axes, float]:
318
+ """Plot ``ln(separation / delta_0)`` vs. bounce number for two nearby billiard rays.
319
+
320
+ A straight, positively-sloped line demonstrates exponential divergence
321
+ per bounce; the slope is estimated by a linear fit over the first
322
+ `fit_fraction` of the bounces, before the separation saturates at the
323
+ table's scale.
324
+
325
+ Parameters
326
+ ----------
327
+ billiard : BilliardSystem
328
+ Billiard to trace both rays within.
329
+ pos : array_like of float, shape (2,)
330
+ Shared initial position ``(x, y)``; must lie in the billiard's
331
+ interior.
332
+ vel : array_like of float, shape (2,)
333
+ Initial velocity direction ``(vx, vy)`` of the reference ray.
334
+ delta_0 : float, default 1e-8
335
+ Initial angular perturbation (radians) applied to `vel`.
336
+ n_bounces : int, default 200
337
+ Number of reflections to trace for each ray.
338
+ fit_fraction : float, default 0.5
339
+ Fraction of the bounces (from the first) used to fit the
340
+ exponential-growth slope.
341
+ ax : matplotlib.axes.Axes, optional
342
+ Axes to draw into; a new figure and axes are created if omitted.
343
+
344
+ Returns
345
+ -------
346
+ fig : matplotlib.figure.Figure
347
+ ax : matplotlib.axes.Axes
348
+ lambda_per_bounce : float
349
+ Estimated divergence rate, in nats per bounce.
350
+ """
351
+ bounce, separation = billiard_trajectory_divergence(billiard, pos, vel, delta_0=delta_0, n_bounces=n_bounces)
352
+ log_ratio = np.log(np.clip(separation, 1e-300, None) / delta_0)
353
+
354
+ n_fit = max(2, int(fit_fraction * len(bounce)))
355
+ lam = lyapunov_exponent_from_divergence(bounce[:n_fit].astype(np.float64), separation[:n_fit] / delta_0)
356
+
357
+ if ax is None:
358
+ fig, ax = plt.subplots(figsize=(7, 5))
359
+ else:
360
+ fig = cast(Figure, ax.figure)
361
+
362
+ ax.plot(bounce, log_ratio, lw=1.2, color=theme.PRIMARY, label=r"$\ln(\delta_k/\delta_0)$")
363
+ ax.plot(
364
+ bounce[:n_fit],
365
+ lam * bounce[:n_fit],
366
+ "--",
367
+ color=theme.MUTED,
368
+ label=rf"fit: $\lambda \approx {lam:.3f}$ / bounce",
369
+ )
370
+ ax.set_xlabel("bounce number k")
371
+ ax.set_ylabel(r"$\ln(\delta_k / \delta_0)$")
372
+ ax.set_title(f"{billiard.__class__.__name__} trajectory divergence")
373
+ ax.legend()
374
+ return fig, ax, lam