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,172 @@
1
+ """Tests for physicskit.classical.visualizers, previously untested (the whole package
2
+ had only manual, un-persisted smoke checks during development). These
3
+ run headlessly (see conftest.py's Agg backend) and check both "does it
4
+ run without raising" and, where practical, that the output is actually
5
+ correct (e.g. Poincare-section crossings at known locations), not just
6
+ that a figure object came back.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import numpy as np
12
+ import pytest
13
+ from matplotlib.animation import PillowWriter
14
+
15
+ from physicskit.classical.systems.chains import FPUTChain
16
+ from physicskit.classical.systems.hamiltonian import HenonHeilesSystem
17
+ from physicskit.classical.systems.lagrangian import DoublePendulum, ElasticPendulum
18
+ from physicskit.classical.systems.newtonian import KeplerSystem
19
+ from physicskit.classical.systems.rotations import EulersDisk, EulerTop, Rattleback
20
+ from physicskit.classical.visualizers.animations import (
21
+ SideBySideAnimator,
22
+ animate_elastic_pendulum,
23
+ animate_eulers_disk,
24
+ animate_rattleback,
25
+ animate_rigid_body_tumble,
26
+ orbit_trace_animation,
27
+ pendulum_animation,
28
+ )
29
+ from physicskit.classical.visualizers.interactive import interactive_orbit, interactive_so3_momentum_sphere
30
+ from physicskit.classical.visualizers.modal_analysis import animate_modal_energies, plot_modal_energy_bars
31
+ from physicskit.classical.visualizers.phase_space import (
32
+ plot_phase_portrait,
33
+ plot_phase_swarm,
34
+ plot_poincare_section,
35
+ plot_so3_momentum_sphere,
36
+ poincare_section,
37
+ )
38
+
39
+
40
+ def test_poincare_section_finds_known_crossings():
41
+ """Synthetic q(t) = sin(t), p(t) = cos(t): q crosses zero going
42
+ upward (direction=+1, p>0) exactly at t = 0, 2*pi, 4*pi, ..."""
43
+ t = np.linspace(0, 4 * np.pi, 20_000)
44
+ q = np.sin(t)
45
+ p = np.cos(t)
46
+
47
+ cq, cp = poincare_section(q, p, section_q=q, section_p=p, value=0.0, direction=1)
48
+
49
+ assert len(cq) == 2 # crossings at t=0 (endpoint, not detected by the interior scan) is excluded; t=2pi, 4pi found
50
+ assert np.allclose(cq, 0.0, atol=1e-3)
51
+ assert np.allclose(cp, 1.0, atol=1e-3)
52
+
53
+
54
+ def test_plot_phase_portrait_1d_and_2d():
55
+ q1 = np.linspace(0, 1, 50)
56
+ p1 = np.cos(q1)
57
+ ax = plot_phase_portrait(q1, p1)
58
+ assert ax is not None
59
+
60
+ q2 = np.column_stack([q1, q1 * 2])
61
+ p2 = np.column_stack([p1, -p1])
62
+ ax2 = plot_phase_portrait(q2, p2, label="test")
63
+ assert len(ax2.lines) >= 2
64
+
65
+
66
+ def test_plot_phase_swarm_and_poincare_wrapper():
67
+ q = np.random.default_rng(0).normal(size=200)
68
+ p = np.random.default_rng(1).normal(size=200)
69
+ ax = plot_phase_swarm(q, p)
70
+ assert ax is not None
71
+
72
+ hh = HenonHeilesSystem(np.array([0.0, 0.3]), np.array([0.3, 0.0]))
73
+ result = hh.integrate((0, 100), dt=0.01, method="yoshida4")
74
+ ax2 = plot_poincare_section(result.q[:, 0], result.p[:, 0], result.q[:, 1], result.p[:, 1], value=0.0, direction=1)
75
+ assert ax2 is not None
76
+
77
+
78
+ def test_plot_so3_momentum_sphere():
79
+ top = EulerTop([0.01, 1.0, 0.01], I1=1.0, I2=2.0, I3=3.0)
80
+ result = top.integrate((0, 10), dt=1e-3, method="implicit_midpoint")
81
+ ax = plot_so3_momentum_sphere(result.y[:, :3], I1=1.0, I2=2.0, I3=3.0)
82
+ assert ax is not None
83
+
84
+
85
+ @pytest.mark.slow
86
+ def test_modal_energy_bars_and_animation(tmp_path):
87
+ chain = FPUTChain(n=16, beta=0.5, mode=1, amplitude=0.5)
88
+ result = chain.integrate((0, 5), dt=0.05, method="yoshida4") # short: this only needs a few frames
89
+
90
+ ax = plot_modal_energy_bars(chain, result.q[0], result.p[0])
91
+ assert len(ax.patches) == 16
92
+
93
+ anim = animate_modal_energies(chain, result, stride=10)
94
+ out = tmp_path / "modal_energy.gif"
95
+ anim.save(out, writer=PillowWriter(fps=10))
96
+ assert out.exists() and out.stat().st_size > 0
97
+
98
+
99
+ @pytest.mark.slow
100
+ def test_side_by_side_animator_pendulum_and_orbit(tmp_path):
101
+ dp = DoublePendulum([2.0, 1.0], [0.5, -0.3])
102
+ result = dp.integrate((0, 0.02), dt=1e-4, method="implicit_midpoint") # short: just needs a few frames
103
+ anim = pendulum_animation(result, dp.positions, mode="phase", stride=10)
104
+ assert isinstance(anim, SideBySideAnimator)
105
+ fig = anim.build()
106
+ assert fig is not None
107
+ out1 = tmp_path / "pendulum.gif"
108
+ anim.save(out1, writer=PillowWriter(fps=10))
109
+ assert out1.exists() and out1.stat().st_size > 0
110
+
111
+ kep = KeplerSystem.from_orbital_elements(a=1.0, e=0.4)
112
+ result2 = kep.integrate((0, 0.5), dt=1e-3, method="yoshida4")
113
+ anim2 = orbit_trace_animation(result2, mode="energy", stride=50)
114
+ out2 = tmp_path / "orbit.gif"
115
+ anim2.save(out2, writer=PillowWriter(fps=10))
116
+ assert out2.exists() and out2.stat().st_size > 0
117
+
118
+
119
+ @pytest.mark.slow
120
+ def test_animate_elastic_pendulum_saves_gif(tmp_path):
121
+ system = ElasticPendulum([0.1, 0.05], [0.0, 0.0])
122
+ # short: animate_elastic_pendulum has no stride and renders one frame
123
+ # per sample, so this only needs enough samples for a valid animation
124
+ result = system.integrate((0, 0.1), dt=1e-3, method="implicit_midpoint")
125
+ anim = animate_elastic_pendulum(system, result)
126
+ out = tmp_path / "elastic_pendulum.gif"
127
+ anim.save(out, writer=PillowWriter(fps=10))
128
+ assert out.exists() and out.stat().st_size > 0
129
+
130
+
131
+ @pytest.mark.slow
132
+ def test_animate_rigid_body_tumble_saves_gif(tmp_path):
133
+ top = EulerTop([0.01, 1.0, 0.01], I1=1.0, I2=2.0, I3=3.0)
134
+ result = top.integrate((0, 2.0), dt=1e-3, method="implicit_midpoint")
135
+ anim = animate_rigid_body_tumble(top, result, stride=20)
136
+ out = tmp_path / "rigid_body_tumble.gif"
137
+ anim.save(out, writer=PillowWriter(fps=10))
138
+ assert out.exists() and out.stat().st_size > 0
139
+
140
+
141
+ @pytest.mark.slow
142
+ def test_animate_eulers_disk_saves_gif(tmp_path):
143
+ disk = EulersDisk(0.5, decay_rate=0.02, precession_const=1.0)
144
+ # short: animate_eulers_disk has no stride and renders one frame per
145
+ # sample (collapse time here is t_f=6.25, so 0.3 stays far from it)
146
+ result = disk.integrate((0.0, 0.15), dt=1e-3, method="rk4")
147
+ anim = animate_eulers_disk(disk, result)
148
+ out = tmp_path / "eulers_disk.gif"
149
+ anim.save(out, writer=PillowWriter(fps=10))
150
+ assert out.exists() and out.stat().st_size > 0
151
+
152
+
153
+ @pytest.mark.slow
154
+ def test_animate_rattleback_saves_gif(tmp_path):
155
+ system = Rattleback([0.01, 0.01, 3.0])
156
+ result = system.integrate((0.0, 8.0), dt=5e-3, method="rk4")
157
+ anim = animate_rattleback(system, result, stride=20)
158
+ out = tmp_path / "rattleback.gif"
159
+ anim.save(out, writer=PillowWriter(fps=10))
160
+ assert out.exists() and out.stat().st_size > 0
161
+
162
+
163
+ def test_interactive_visualizers_return_populated_figures():
164
+ top = EulerTop([0.01, 1.0, 0.01], I1=1.0, I2=2.0, I3=3.0)
165
+ result = top.integrate((0, 10), dt=1e-3, method="implicit_midpoint")
166
+ fig = interactive_so3_momentum_sphere(result.y[:, :3], I1=1.0, I2=2.0, I3=3.0)
167
+ assert len(fig.data) == 3 # sphere surface + trajectory + start marker
168
+
169
+ kep = KeplerSystem.from_orbital_elements(a=1.0, e=0.3, c_pn=0.01)
170
+ result2 = kep.integrate((0, 50), dt=1e-3, method="yoshida4")
171
+ fig2 = interactive_orbit(result2, system=kep)
172
+ assert len(fig2.data) == 4 # orbit + focus + 2 LRL arrows
@@ -0,0 +1,20 @@
1
+ from physicskit.classical.utils.conservation import (
2
+ angular_momentum_2d,
3
+ angular_momentum_drift,
4
+ energy_drift,
5
+ lrl_drift,
6
+ relative_energy_drift,
7
+ )
8
+ from physicskit.classical.utils.stepsize import StepSizeEstimate, estimate_dt
9
+ from physicskit.classical.utils.symbolic import LagrangianEngine
10
+
11
+ __all__ = [
12
+ "LagrangianEngine",
13
+ "energy_drift",
14
+ "relative_energy_drift",
15
+ "angular_momentum_2d",
16
+ "angular_momentum_drift",
17
+ "lrl_drift",
18
+ "StepSizeEstimate",
19
+ "estimate_dt",
20
+ ]
@@ -0,0 +1,126 @@
1
+ """Conservation-law diagnostics: energy drift, angular momentum, and the
2
+ Laplace-Runge-Lenz (LRL) vector.
3
+
4
+ These are thin, system-agnostic helpers that operate on the arrays
5
+ already produced by ``system.integrate(...)`` (a
6
+ :class:`physicskit.classical.core.base_system.SimulationResult`), used both by the
7
+ test suite (``tests/test_conservation.py``) and by the visualizers to
8
+ annotate plots with drift metrics.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import numpy as np
14
+
15
+ __all__ = [
16
+ "energy_drift",
17
+ "relative_energy_drift",
18
+ "angular_momentum_2d",
19
+ "angular_momentum_drift",
20
+ "lrl_drift",
21
+ ]
22
+
23
+
24
+ def energy_drift(energy: np.ndarray) -> np.ndarray:
25
+ """Absolute energy deviation from the initial value.
26
+
27
+ Parameters
28
+ ----------
29
+ energy : ndarray, shape (n_steps + 1,)
30
+ Energy trajectory, e.g. ``result.energy``.
31
+
32
+ Returns
33
+ -------
34
+ ndarray
35
+ ``|H(t) - H(0)|`` at each sample.
36
+ """
37
+ energy = np.asarray(energy, dtype=np.float64)
38
+ return np.abs(energy - energy[0])
39
+
40
+
41
+ def relative_energy_drift(energy: np.ndarray) -> np.ndarray:
42
+ """Relative energy deviation from the initial value.
43
+
44
+ This is the quantity conventionally checked against a tolerance
45
+ (e.g. < 1e-6) to certify a symplectic integrator is behaving
46
+ correctly over long integrations.
47
+
48
+ Parameters
49
+ ----------
50
+ energy : ndarray, shape (n_steps + 1,)
51
+ Energy trajectory, e.g. ``result.energy``.
52
+
53
+ Returns
54
+ -------
55
+ ndarray
56
+ ``|H(t) - H(0)| / |H(0)|`` at each sample; ``inf`` everywhere
57
+ if ``H(0)`` is exactly zero.
58
+ """
59
+ energy = np.asarray(energy, dtype=np.float64)
60
+ e0 = energy[0]
61
+ if e0 == 0.0:
62
+ return np.full_like(energy, np.inf)
63
+ return np.abs(energy - e0) / np.abs(e0)
64
+
65
+
66
+ def angular_momentum_2d(q: np.ndarray, p: np.ndarray) -> np.ndarray:
67
+ """Out-of-plane angular momentum for a planar trajectory.
68
+
69
+ Parameters
70
+ ----------
71
+ q, p : ndarray, shape (n_steps, 2)
72
+ Position and momentum trajectories.
73
+
74
+ Returns
75
+ -------
76
+ ndarray, shape (n_steps,)
77
+ ``L = x*py - y*px`` at each sample.
78
+ """
79
+ q = np.asarray(q, dtype=np.float64)
80
+ p = np.asarray(p, dtype=np.float64)
81
+ return q[:, 0] * p[:, 1] - q[:, 1] * p[:, 0]
82
+
83
+
84
+ def angular_momentum_drift(q: np.ndarray, p: np.ndarray) -> np.ndarray:
85
+ """Absolute drift of the planar angular momentum from its initial value.
86
+
87
+ Parameters
88
+ ----------
89
+ q, p : ndarray, shape (n_steps, 2)
90
+ Position and momentum trajectories.
91
+
92
+ Returns
93
+ -------
94
+ ndarray, shape (n_steps,)
95
+ ``|L(t) - L(0)|`` at each sample.
96
+ """
97
+ L = angular_momentum_2d(q, p)
98
+ return np.abs(L - L[0])
99
+
100
+
101
+ def lrl_drift(system, q: np.ndarray, p: np.ndarray) -> np.ndarray:
102
+ """Drift of the Laplace-Runge-Lenz vector's magnitude across a trajectory.
103
+
104
+ For the unperturbed 1/r potential the LRL vector is exactly
105
+ conserved (both magnitude and direction); its direction sweeping
106
+ out an angle over time is the visual signature of apsidal
107
+ precession once a perturbation is present, so magnitude drift
108
+ remaining small while direction changes is the expected, physically
109
+ correct behavior for a perturbed orbit.
110
+
111
+ Parameters
112
+ ----------
113
+ system : object
114
+ A :class:`physicskit.classical.systems.newtonian.KeplerSystem`-like object
115
+ exposing ``lrl_vector(q, p) -> array``.
116
+ q, p : ndarray, shape (n_steps, 2)
117
+ Position and momentum trajectories.
118
+
119
+ Returns
120
+ -------
121
+ ndarray, shape (n_steps,)
122
+ ``||A(t)| - |A(0)||`` at each sample.
123
+ """
124
+ vecs = np.array([system.lrl_vector(qi, pi) for qi, pi in zip(q, p)])
125
+ mags = np.linalg.norm(vecs, axis=1)
126
+ return np.abs(mags - mags[0])
@@ -0,0 +1,187 @@
1
+ """Automated timestep selection.
2
+
3
+ None of physicskit.classical's integrators are adaptive -- Verlet, Yoshida4, and
4
+ implicit midpoint all take a single fixed ``dt`` for the whole run.
5
+ Choosing that ``dt`` by hand (integrate a trial run, check the energy
6
+ drift, halve or double, repeat) is exactly the kind of "black art" a
7
+ numerical library should not leave to its users. :func:`estimate_dt`
8
+ automates that search.
9
+
10
+ For a quasi-periodic system (e.g. an unperturbed Kepler orbit), the
11
+ characteristic energy-drift amplitude at a given ``dt`` is essentially
12
+ independent of how many periods you run for, so a short probe
13
+ integration is enough. **That is not true for chaotic systems**: a
14
+ short probe can find a ``dt`` that looks fine over a few hundred steps
15
+ but fails badly once the trajectory (e.g. a double pendulum) later
16
+ swings through a more demanding configuration than the probe window
17
+ happened to sample. To guard against exactly that,
18
+ :func:`estimate_dt` always re-verifies its candidate at the full
19
+ intended step count (``target_steps``) and shrinks ``dt`` further if
20
+ that longer run doesn't also meet the tolerance -- it does not just
21
+ trust the fast probe.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from collections.abc import Callable
27
+ from dataclasses import dataclass
28
+
29
+ import numpy as np
30
+
31
+ __all__ = ["StepSizeEstimate", "estimate_dt"]
32
+
33
+
34
+ @dataclass
35
+ class StepSizeEstimate:
36
+ """Result of :func:`estimate_dt`.
37
+
38
+ Attributes
39
+ ----------
40
+ dt : float
41
+ The recommended, verified step size.
42
+ achieved_drift : float
43
+ Relative energy drift measured at ``dt`` over ``verified_at_steps``.
44
+ verified_at_steps : int
45
+ Step count the verification stage actually confirmed.
46
+ n_evaluations : int
47
+ Total number of trial integrations run during the search.
48
+ """
49
+
50
+ dt: float
51
+ achieved_drift: float
52
+ verified_at_steps: int
53
+ n_evaluations: int
54
+
55
+
56
+ def _drift_for(system_factory, method, dt, n_steps) -> float:
57
+ system = system_factory()
58
+ e0 = system.energy()
59
+ result = system.integrate((0.0, n_steps * dt), dt=dt, method=method)
60
+ denom = abs(e0) if e0 != 0.0 else 1.0
61
+ return float(np.max(np.abs(result.energy - e0)) / denom)
62
+
63
+
64
+ def estimate_dt(
65
+ system_factory: Callable[[], object],
66
+ method: str,
67
+ tol: float = 1e-6,
68
+ target_steps: int = 100_000,
69
+ probe_steps: int = 2000,
70
+ dt_initial: float = 1e-2,
71
+ growth: float = 2.0,
72
+ rel_precision: float = 0.05,
73
+ max_iter: int = 40,
74
+ ) -> StepSizeEstimate:
75
+ """Find the largest ``dt`` for which ``target_steps`` of ``method``
76
+ keeps the relative energy drift below ``tol``.
77
+
78
+ A fast bracket-and-bisect search over a short ``probe_steps``
79
+ window finds an initial candidate; that candidate is then always
80
+ re-checked at the full ``target_steps`` and shrunk further (by
81
+ ``growth``) if the longer run doesn't also clear ``tol`` -- see the
82
+ module docstring for why this verification stage matters for
83
+ chaotic systems.
84
+
85
+ Parameters
86
+ ----------
87
+ system_factory : callable
88
+ Zero-argument callable returning a *fresh* system instance in
89
+ its initial state (a new instance each call, since
90
+ ``integrate`` mutates state) -- e.g.
91
+ ``lambda: KeplerSystem.from_orbital_elements(a=1.0, e=0.5)``.
92
+ method : str
93
+ Integration method to pass to ``system.integrate`` (e.g.
94
+ ``"yoshida4"`` or ``"implicit_midpoint"``).
95
+ tol : float
96
+ Target relative energy-drift tolerance,
97
+ ``max|H(t) - H(0)| / |H(0)|``.
98
+ target_steps : int
99
+ The step count you actually intend to integrate for; the
100
+ returned ``dt`` is verified to meet ``tol`` over this many
101
+ steps, not just over the fast probe.
102
+ probe_steps : int
103
+ Length of each *fast* bracketing/bisection trial, in steps.
104
+ Should cover at least a few characteristic oscillation periods.
105
+ dt_initial : float
106
+ Starting guess for the fast probe stage.
107
+ growth : float
108
+ Factor used both to bracket a pass/fail boundary during the
109
+ fast probe stage, and to shrink ``dt`` during verification.
110
+ rel_precision : float
111
+ Stop bisecting once the probe-stage bracket
112
+ ``[dt_good, dt_bad]`` has ``(dt_bad - dt_good) / dt_good``
113
+ below this.
114
+ max_iter : int
115
+ Safety cap on iterations, applied separately to the
116
+ bracketing, bisection, and verification stages.
117
+
118
+ Returns
119
+ -------
120
+ StepSizeEstimate
121
+ ``.dt`` is the recommended, *verified* step size;
122
+ ``.achieved_drift`` is the relative drift measured at that
123
+ ``dt`` over ``target_steps`` (i.e. what verification actually
124
+ confirmed, not the fast-probe estimate).
125
+ """
126
+ n_evals = 0
127
+
128
+ def probe_drift(dt: float) -> float:
129
+ nonlocal n_evals
130
+ n_evals += 1
131
+ return _drift_for(system_factory, method, dt, probe_steps)
132
+
133
+ def target_drift(dt: float) -> float:
134
+ nonlocal n_evals
135
+ n_evals += 1
136
+ return _drift_for(system_factory, method, dt, target_steps)
137
+
138
+ # Stage 1: fast bracket + bisect over the short probe window.
139
+ dt = dt_initial
140
+ drift = probe_drift(dt)
141
+
142
+ if drift < tol:
143
+ dt_good, dt_bad = dt, None
144
+ for _ in range(max_iter):
145
+ dt *= growth
146
+ drift = probe_drift(dt)
147
+ if drift >= tol:
148
+ dt_bad = dt
149
+ break
150
+ dt_good = dt
151
+ else:
152
+ dt_bad = dt # never failed within max_iter growth steps; use the last value as the bracket edge
153
+ else:
154
+ dt_bad = dt
155
+ dt_good = None
156
+ for _ in range(max_iter):
157
+ dt /= growth
158
+ drift = probe_drift(dt)
159
+ if drift < tol:
160
+ dt_good = dt
161
+ break
162
+ dt_bad = dt
163
+ else:
164
+ raise RuntimeError(f"Could not find a dt meeting tol={tol:.1e} within {max_iter} shrink steps")
165
+
166
+ lo, hi = dt_good, dt_bad
167
+ for _ in range(max_iter):
168
+ if (hi - lo) / lo < rel_precision:
169
+ break
170
+ mid = np.sqrt(lo * hi)
171
+ if probe_drift(mid) < tol:
172
+ lo = mid
173
+ else:
174
+ hi = mid
175
+
176
+ # Stage 2: verify (and if necessary shrink further) at the full target_steps.
177
+ candidate = lo
178
+ for _ in range(max_iter):
179
+ drift = target_drift(candidate)
180
+ if drift < tol:
181
+ return StepSizeEstimate(dt=candidate, achieved_drift=drift, verified_at_steps=target_steps, n_evaluations=n_evals)
182
+ candidate /= growth
183
+ raise RuntimeError(
184
+ f"Could not find a dt meeting tol={tol:.1e} over target_steps={target_steps} within {max_iter} "
185
+ "verification shrink steps -- this system's worst-case error may need a much smaller dt than "
186
+ "usual (common for chaotic systems); try a smaller dt_initial."
187
+ )