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,1131 @@
1
+ """2D billiard systems with Numba-accelerated ray-boundary collision and specular reflection.
2
+
3
+ Every billiard's boundary is represented as a set of straight *segments*
4
+ (``(x1, y1, x2, y2)`` rows) and circular *arcs* (``(cx, cy, r, theta1, theta2)``
5
+ rows, always swept counter-clockwise from ``theta1`` to ``theta2`` modulo
6
+ ``2*pi``). Walls are always constructed in traversal order matching the
7
+ direction of increasing boundary arclength ``s``, which is what lets a single
8
+ shared, jitted ray-tracer serve every billiard shape (Circle, Rectangle, Sinai,
9
+ Bunimovich Stadium, Truncated Circle, Ellipse) without per-shape collision
10
+ code. The Ellipse billiard is the one shape without an exact representation
11
+ in this scheme (there is no closed-form ray-ellipse intersection kernel here)
12
+ and is instead approximated as a fine closed polygon of straight segments.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any
18
+
19
+ import numpy as np
20
+ from numba import njit, prange
21
+ from numpy.typing import ArrayLike, NDArray
22
+
23
+ from physicskit.chaos.core.base_system import BilliardSystem
24
+ from physicskit.chaos.exceptions import InvalidParameterError
25
+
26
+ TWO_PI: float = 2.0 * np.pi
27
+ _EPS: float = 1e-9
28
+ _NO_HIT: float = 1e18
29
+
30
+ #: A boundary wall specification consumed by ``_finalize_boundary``: either
31
+ #: ``("seg", (x1, y1, x2, y2))`` or ``("arc", (cx, cy, r, theta1, theta2),
32
+ #: is_full_circle)``.
33
+ WallSpec = tuple[str, tuple[float, ...]] | tuple[str, tuple[float, ...], bool]
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Low-level Numba kernels
37
+ # ---------------------------------------------------------------------------
38
+
39
+
40
+ @njit(cache=True)
41
+ def reflect(vx: float, vy: float, nx: float, ny: float) -> tuple[float, float]:
42
+ """Specular reflection of a velocity about a wall normal.
43
+
44
+ Computes ``v' = v - 2 (v . n) n``. The sign of `nx`, `ny` is irrelevant:
45
+ flipping the normal leaves the result unchanged.
46
+
47
+ Parameters
48
+ ----------
49
+ vx, vy : float
50
+ Incoming velocity components.
51
+ nx, ny : float
52
+ Unit normal components (need not be outward-facing).
53
+
54
+ Returns
55
+ -------
56
+ rvx, rvy : float
57
+ Reflected (outgoing) velocity components.
58
+ """
59
+ d = vx * nx + vy * ny
60
+ return vx - 2.0 * d * nx, vy - 2.0 * d * ny
61
+
62
+
63
+ @njit(cache=True)
64
+ def _segment_hit(px: float, py: float, vx: float, vy: float, x1: float, y1: float, x2: float, y2: float) -> float:
65
+ """Ray-segment intersection distance.
66
+
67
+ Parameters
68
+ ----------
69
+ px, py : float
70
+ Ray origin.
71
+ vx, vy : float
72
+ Ray direction (need not be unit length).
73
+ x1, y1, x2, y2 : float
74
+ Segment endpoints.
75
+
76
+ Returns
77
+ -------
78
+ float
79
+ The intersection distance `t` such that ``(px + t*vx, py + t*vy)``
80
+ lies on the segment, or the ``_NO_HIT`` sentinel if there is none.
81
+ """
82
+ ex, ey = x2 - x1, y2 - y1
83
+ denom = vx * ey - vy * ex
84
+ if abs(denom) < 1e-14:
85
+ return _NO_HIT
86
+ t = ((x1 - px) * ey - (y1 - py) * ex) / denom
87
+ if t < _EPS:
88
+ return _NO_HIT
89
+ if abs(ex) > abs(ey):
90
+ u = (px + t * vx - x1) / ex
91
+ else:
92
+ u = (py + t * vy - y1) / ey
93
+ if u < -_EPS or u > 1.0 + _EPS:
94
+ return _NO_HIT
95
+ return t
96
+
97
+
98
+ @njit(cache=True)
99
+ def _angle_in_range(ang: float, theta1: float, theta2: float) -> bool:
100
+ """Whether angle `ang` lies on the counter-clockwise sweep from `theta1` to `theta2`.
101
+
102
+ Parameters
103
+ ----------
104
+ ang : float
105
+ Angle to test, in radians (any range).
106
+ theta1, theta2 : float
107
+ Sweep start and end angles, in radians (any range); the sweep is
108
+ always interpreted as going counter-clockwise from `theta1` to
109
+ `theta2`, modulo ``2*pi``.
110
+
111
+ Returns
112
+ -------
113
+ bool
114
+ ``True`` if `ang` lies within the swept range.
115
+ """
116
+ a = ang % TWO_PI
117
+ lo = theta1 % TWO_PI
118
+ hi = theta2 % TWO_PI
119
+ if lo <= hi:
120
+ return (lo - 1e-7) <= a <= (hi + 1e-7)
121
+ return a >= (lo - 1e-7) or a <= (hi + 1e-7)
122
+
123
+
124
+ @njit(cache=True)
125
+ def _arc_hit(
126
+ px: float,
127
+ py: float,
128
+ vx: float,
129
+ vy: float,
130
+ cx: float,
131
+ cy: float,
132
+ r: float,
133
+ theta1: float,
134
+ theta2: float,
135
+ full_circle: bool,
136
+ ) -> float:
137
+ """Nearest positive ray-circle intersection distance, respecting the arc's angular span.
138
+
139
+ Parameters
140
+ ----------
141
+ px, py : float
142
+ Ray origin.
143
+ vx, vy : float
144
+ Ray direction (need not be unit length).
145
+ cx, cy, r : float
146
+ Circle center and radius.
147
+ theta1, theta2 : float
148
+ Arc sweep start and end angles, in radians; ignored if `full_circle`.
149
+ full_circle : bool
150
+ If ``True``, treat the circle as a full circle and skip the angular
151
+ range check.
152
+
153
+ Returns
154
+ -------
155
+ float
156
+ The nearest intersection distance `t` (with ``t > 0``), or the
157
+ ``_NO_HIT`` sentinel if there is none within the arc's span.
158
+ """
159
+ fx, fy = px - cx, py - cy
160
+ a = vx * vx + vy * vy
161
+ b = 2.0 * (fx * vx + fy * vy)
162
+ c = fx * fx + fy * fy - r * r
163
+ disc = b * b - 4.0 * a * c
164
+ if disc < 0.0:
165
+ return _NO_HIT
166
+ sq = np.sqrt(disc)
167
+ best = _NO_HIT
168
+ for t in ((-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)):
169
+ if t > _EPS and t < best:
170
+ if full_circle:
171
+ best = t
172
+ else:
173
+ hx, hy = px + t * vx - cx, py + t * vy - cy
174
+ ang = np.arctan2(hy, hx)
175
+ if _angle_in_range(ang, theta1, theta2):
176
+ best = t
177
+ return best
178
+
179
+
180
+ @njit(cache=True)
181
+ def _find_collision(
182
+ px: float,
183
+ py: float,
184
+ vx: float,
185
+ vy: float,
186
+ segments: NDArray[np.float64],
187
+ arcs: NDArray[np.float64],
188
+ arc_full: NDArray[np.bool_],
189
+ ) -> tuple[float, int, int, float, int, int]:
190
+ """Find the nearest (and second-nearest) boundary wall hit by a ray.
191
+
192
+ The second-nearest hit lets :func:`bounce` detect exact corner hits,
193
+ where two walls tie for nearest and both must contribute to the
194
+ reflection.
195
+
196
+ Parameters
197
+ ----------
198
+ px, py : float
199
+ Ray origin.
200
+ vx, vy : float
201
+ Ray direction (need not be unit length).
202
+ segments : ndarray of float, shape (n_segments, 4)
203
+ Straight walls, each row ``(x1, y1, x2, y2)``.
204
+ arcs : ndarray of float, shape (n_arcs, 5)
205
+ Circular-arc walls, each row ``(cx, cy, r, theta1, theta2)``.
206
+ arc_full : ndarray of bool, shape (n_arcs,)
207
+ Whether each arc is a full circle.
208
+
209
+ Returns
210
+ -------
211
+ t : float
212
+ Distance to the nearest hit (``_NO_HIT`` if the boundary is empty).
213
+ wall_type : int
214
+ ``0`` if the nearest hit is a segment, ``1`` if an arc, ``-1`` if none.
215
+ wall_idx : int
216
+ Row index of the nearest wall into `segments` or `arcs`, ``-1`` if none.
217
+ t2 : float
218
+ Distance to the second-nearest hit (``_NO_HIT`` if there is none).
219
+ wall_type2 : int
220
+ Type of the second-nearest wall, ``-1`` if none.
221
+ wall_idx2 : int
222
+ Row index of the second-nearest wall, ``-1`` if none.
223
+ """
224
+ best_t = _NO_HIT
225
+ best_type = -1
226
+ best_idx = -1
227
+ second_t = _NO_HIT
228
+ second_type = -1
229
+ second_idx = -1
230
+ for i in range(segments.shape[0]):
231
+ t = _segment_hit(
232
+ px,
233
+ py,
234
+ vx,
235
+ vy,
236
+ segments[i, 0],
237
+ segments[i, 1],
238
+ segments[i, 2],
239
+ segments[i, 3],
240
+ )
241
+ if t < best_t:
242
+ second_t, second_type, second_idx = best_t, best_type, best_idx
243
+ best_t, best_type, best_idx = t, 0, i
244
+ elif t < second_t:
245
+ second_t, second_type, second_idx = t, 0, i
246
+ for j in range(arcs.shape[0]):
247
+ t = _arc_hit(
248
+ px,
249
+ py,
250
+ vx,
251
+ vy,
252
+ arcs[j, 0],
253
+ arcs[j, 1],
254
+ arcs[j, 2],
255
+ arcs[j, 3],
256
+ arcs[j, 4],
257
+ arc_full[j],
258
+ )
259
+ if t < best_t:
260
+ second_t, second_type, second_idx = best_t, best_type, best_idx
261
+ best_t, best_type, best_idx = t, 1, j
262
+ elif t < second_t:
263
+ second_t, second_type, second_idx = t, 1, j
264
+ return best_t, best_type, best_idx, second_t, second_type, second_idx
265
+
266
+
267
+ @njit(cache=True)
268
+ def _wall_normal(
269
+ hx: float,
270
+ hy: float,
271
+ wall_type: int,
272
+ wall_idx: int,
273
+ segments: NDArray[np.float64],
274
+ arcs: NDArray[np.float64],
275
+ ) -> tuple[float, float]:
276
+ """Unit normal to a wall at a given hit point (sign is arbitrary).
277
+
278
+ Parameters
279
+ ----------
280
+ hx, hy : float
281
+ Point on the wall at which to evaluate the normal.
282
+ wall_type : int
283
+ ``0`` for a segment, ``1`` for an arc.
284
+ wall_idx : int
285
+ Row index of the wall into `segments` or `arcs`.
286
+ segments : ndarray of float, shape (n_segments, 4)
287
+ Straight walls, each row ``(x1, y1, x2, y2)``.
288
+ arcs : ndarray of float, shape (n_arcs, 5)
289
+ Circular-arc walls, each row ``(cx, cy, r, theta1, theta2)``.
290
+
291
+ Returns
292
+ -------
293
+ nx, ny : float
294
+ Unit normal components at ``(hx, hy)``.
295
+ """
296
+ if wall_type == 0:
297
+ x1, y1, x2, y2 = (
298
+ segments[wall_idx, 0],
299
+ segments[wall_idx, 1],
300
+ segments[wall_idx, 2],
301
+ segments[wall_idx, 3],
302
+ )
303
+ ex, ey = x2 - x1, y2 - y1
304
+ elen = np.sqrt(ex * ex + ey * ey)
305
+ return -ey / elen, ex / elen
306
+ cx, cy = arcs[wall_idx, 0], arcs[wall_idx, 1]
307
+ nx, ny = hx - cx, hy - cy
308
+ nlen = np.sqrt(nx * nx + ny * ny)
309
+ return nx / nlen, ny / nlen
310
+
311
+
312
+ @njit(cache=True)
313
+ def bounce(
314
+ px: float,
315
+ py: float,
316
+ vx: float,
317
+ vy: float,
318
+ segments: NDArray[np.float64],
319
+ arcs: NDArray[np.float64],
320
+ arc_full: NDArray[np.bool_],
321
+ ) -> tuple[float, float, float, float, int, int]:
322
+ """Advance a ray to its next boundary collision and specularly reflect it.
323
+
324
+ If the ray lands (near-)exactly on a corner where two walls meet, both
325
+ walls' normals contribute (sequential reflection) -- unless the two walls
326
+ share the same normal (a smooth tangential join, e.g. where a Bunimovich
327
+ stadium's straight edge meets its semicircle), in which case reflecting
328
+ twice would spuriously cancel out and only one reflection is applied.
329
+
330
+ Parameters
331
+ ----------
332
+ px, py : float
333
+ Current position.
334
+ vx, vy : float
335
+ Current (unit) velocity.
336
+ segments : ndarray of float, shape (n_segments, 4)
337
+ Straight walls, each row ``(x1, y1, x2, y2)``.
338
+ arcs : ndarray of float, shape (n_arcs, 5)
339
+ Circular-arc walls, each row ``(cx, cy, r, theta1, theta2)``.
340
+ arc_full : ndarray of bool, shape (n_arcs,)
341
+ Whether each arc is a full circle.
342
+
343
+ Returns
344
+ -------
345
+ hx, hy : float
346
+ Position of the collision (hit) point.
347
+ rvx, rvy : float
348
+ Outgoing (post-reflection) velocity.
349
+ wall_type : int
350
+ ``0`` if a segment was hit, ``1`` if an arc.
351
+ wall_idx : int
352
+ Row index of the hit wall into `segments` or `arcs`.
353
+ """
354
+ t, wtype, widx, t2, wtype2, widx2 = _find_collision(px, py, vx, vy, segments, arcs, arc_full)
355
+ hx, hy = px + t * vx, py + t * vy
356
+ nx, ny = _wall_normal(hx, hy, wtype, widx, segments, arcs)
357
+ rvx, rvy = reflect(vx, vy, nx, ny)
358
+ if wtype2 != -1 and (t2 - t) < 1e-7:
359
+ nx2, ny2 = _wall_normal(hx, hy, wtype2, widx2, segments, arcs)
360
+ if abs(nx * nx2 + ny * ny2) < 0.999:
361
+ rvx, rvy = reflect(rvx, rvy, nx2, ny2)
362
+ return hx, hy, rvx, rvy, wtype, widx
363
+
364
+
365
+ @njit(cache=True)
366
+ def simulate_bounces(
367
+ px: float,
368
+ py: float,
369
+ vx: float,
370
+ vy: float,
371
+ segments: NDArray[np.float64],
372
+ arcs: NDArray[np.float64],
373
+ arc_full: NDArray[np.bool_],
374
+ n_bounces: int,
375
+ ) -> tuple[
376
+ NDArray[np.float64],
377
+ NDArray[np.float64],
378
+ NDArray[np.float64],
379
+ NDArray[np.float64],
380
+ NDArray[np.int64],
381
+ NDArray[np.int64],
382
+ ]:
383
+ """Trace a ray through ``n_bounces`` specular reflections.
384
+
385
+ Parameters
386
+ ----------
387
+ px, py : float
388
+ Initial position.
389
+ vx, vy : float
390
+ Initial (unit) velocity.
391
+ segments : ndarray of float, shape (n_segments, 4)
392
+ Straight walls, each row ``(x1, y1, x2, y2)``.
393
+ arcs : ndarray of float, shape (n_arcs, 5)
394
+ Circular-arc walls, each row ``(cx, cy, r, theta1, theta2)``.
395
+ arc_full : ndarray of bool, shape (n_arcs,)
396
+ Whether each arc is a full circle.
397
+ n_bounces : int
398
+ Number of reflections to simulate.
399
+
400
+ Returns
401
+ -------
402
+ xs, ys : ndarray of float, shape (n_bounces,)
403
+ Hit-point coordinates at each bounce.
404
+ vxs, vys : ndarray of float, shape (n_bounces,)
405
+ Outgoing velocity components at each bounce.
406
+ wtypes : ndarray of int64, shape (n_bounces,)
407
+ Wall type (``0`` segment, ``1`` arc) struck at each bounce.
408
+ widxs : ndarray of int64, shape (n_bounces,)
409
+ Wall row index struck at each bounce.
410
+ """
411
+ xs = np.empty(n_bounces)
412
+ ys = np.empty(n_bounces)
413
+ vxs = np.empty(n_bounces)
414
+ vys = np.empty(n_bounces)
415
+ wtypes = np.empty(n_bounces, dtype=np.int64)
416
+ widxs = np.empty(n_bounces, dtype=np.int64)
417
+ for i in range(n_bounces):
418
+ px, py, vx, vy, wt, wi = bounce(px, py, vx, vy, segments, arcs, arc_full)
419
+ xs[i] = px
420
+ ys[i] = py
421
+ vxs[i] = vx
422
+ vys[i] = vy
423
+ wtypes[i] = wt
424
+ widxs[i] = wi
425
+ return xs, ys, vxs, vys, wtypes, widxs
426
+
427
+
428
+ @njit(cache=True, parallel=True)
429
+ def simulate_bounces_many(
430
+ px: float,
431
+ py: float,
432
+ angles: NDArray[np.float64],
433
+ segments: NDArray[np.float64],
434
+ arcs: NDArray[np.float64],
435
+ arc_full: NDArray[np.bool_],
436
+ n_bounces: int,
437
+ ) -> tuple[
438
+ NDArray[np.float64],
439
+ NDArray[np.float64],
440
+ NDArray[np.float64],
441
+ NDArray[np.float64],
442
+ NDArray[np.int64],
443
+ NDArray[np.int64],
444
+ ]:
445
+ """Trace many independent rays from one point, in parallel across rays.
446
+
447
+ Same physics as :func:`simulate_bounces`, run once per entry of `angles`
448
+ (all launched from the same ``(px, py)``) and parallelized across rays
449
+ with Numba's ``prange`` -- the embarrassingly parallel workload behind a
450
+ Poincare section (:func:`physicskit.chaos.visualizers.phase_space.plot_poincare_section`),
451
+ where each ray is fully independent of every other.
452
+
453
+ Parameters
454
+ ----------
455
+ px, py : float
456
+ Shared initial position for every ray.
457
+ angles : ndarray of float, shape (n_rays,)
458
+ Initial launch angle (radians) of each ray.
459
+ segments : ndarray of float, shape (n_segments, 4)
460
+ Straight walls, each row ``(x1, y1, x2, y2)``.
461
+ arcs : ndarray of float, shape (n_arcs, 5)
462
+ Circular-arc walls, each row ``(cx, cy, r, theta1, theta2)``.
463
+ arc_full : ndarray of bool, shape (n_arcs,)
464
+ Whether each arc is a full circle.
465
+ n_bounces : int
466
+ Number of reflections to simulate per ray.
467
+
468
+ Returns
469
+ -------
470
+ xs, ys : ndarray of float, shape (n_rays, n_bounces)
471
+ Hit-point coordinates at each bounce of each ray.
472
+ vxs, vys : ndarray of float, shape (n_rays, n_bounces)
473
+ Outgoing velocity components at each bounce of each ray.
474
+ wtypes : ndarray of int64, shape (n_rays, n_bounces)
475
+ Wall type (``0`` segment, ``1`` arc) struck at each bounce.
476
+ widxs : ndarray of int64, shape (n_rays, n_bounces)
477
+ Wall row index struck at each bounce.
478
+ """
479
+ n_rays = angles.shape[0]
480
+ xs = np.empty((n_rays, n_bounces))
481
+ ys = np.empty((n_rays, n_bounces))
482
+ vxs = np.empty((n_rays, n_bounces))
483
+ vys = np.empty((n_rays, n_bounces))
484
+ wtypes = np.empty((n_rays, n_bounces), dtype=np.int64)
485
+ widxs = np.empty((n_rays, n_bounces), dtype=np.int64)
486
+ for r in prange(n_rays): # type: ignore[attr-defined] # numba lacks type stubs for prange
487
+ x, y = px, py
488
+ vx, vy = np.cos(angles[r]), np.sin(angles[r])
489
+ for i in range(n_bounces):
490
+ x, y, vx, vy, wt, wi = bounce(x, y, vx, vy, segments, arcs, arc_full)
491
+ xs[r, i] = x
492
+ ys[r, i] = y
493
+ vxs[r, i] = vx
494
+ vys[r, i] = vy
495
+ wtypes[r, i] = wt
496
+ widxs[r, i] = wi
497
+ return xs, ys, vxs, vys, wtypes, widxs
498
+
499
+
500
+ # ---------------------------------------------------------------------------
501
+ # BilliardSystem base implementation
502
+ # ---------------------------------------------------------------------------
503
+
504
+
505
+ class _WallBoundaryMixin:
506
+ """Shared arclength (``s``) bookkeeping on top of a boundary built from an
507
+ ordered list of walls (the order defines the direction of increasing ``s``).
508
+ """
509
+
510
+ def _finalize_boundary(self, walls: list[WallSpec]) -> None:
511
+ """Build the segment/arc arrays and cumulative arclength offsets.
512
+
513
+ Parameters
514
+ ----------
515
+ walls : list of tuple
516
+ Wall specifications in traversal order (the direction of
517
+ increasing arclength ``s``): each is either
518
+ ``("seg", (x1, y1, x2, y2))`` or
519
+ ``("arc", (cx, cy, r, theta1, theta2), is_full_circle)``.
520
+ """
521
+ seg_rows: list[tuple[float, ...]] = []
522
+ arc_rows: list[tuple[float, ...]] = []
523
+ arc_full: list[bool] = []
524
+ order: list[tuple[str, int]] = []
525
+ for wall in walls:
526
+ kind = wall[0]
527
+ if kind == "seg":
528
+ seg_rows.append(wall[1])
529
+ order.append(("seg", len(seg_rows) - 1))
530
+ else:
531
+ arc_rows.append(wall[1])
532
+ arc_full.append(bool(wall[2]) if len(wall) > 2 else False)
533
+ order.append(("arc", len(arc_rows) - 1))
534
+
535
+ segments = np.asarray(seg_rows, dtype=np.float64).reshape(-1, 4)
536
+ arcs = np.asarray(arc_rows, dtype=np.float64).reshape(-1, 5)
537
+ arc_full_arr = np.asarray(arc_full, dtype=np.bool_)
538
+
539
+ seg_lengths = np.hypot(segments[:, 2] - segments[:, 0], segments[:, 3] - segments[:, 1]) if segments.shape[0] else np.empty(0)
540
+ if arcs.shape[0]:
541
+ sweep = (arcs[:, 4] - arcs[:, 3]) % TWO_PI
542
+ sweep = np.where(arc_full_arr, TWO_PI, sweep)
543
+ arc_lengths = arcs[:, 2] * sweep
544
+ else:
545
+ arc_lengths = np.empty(0)
546
+
547
+ s_offsets_seg = np.zeros(segments.shape[0])
548
+ s_offsets_arc = np.zeros(arcs.shape[0])
549
+ running = 0.0
550
+ for kind, idx in order:
551
+ if kind == "seg":
552
+ s_offsets_seg[idx] = running
553
+ running += seg_lengths[idx]
554
+ else:
555
+ s_offsets_arc[idx] = running
556
+ running += arc_lengths[idx]
557
+
558
+ self._segments = segments
559
+ self._arcs = arcs
560
+ self._arc_full = arc_full_arr
561
+ self._seg_lengths = seg_lengths
562
+ self._arc_lengths = arc_lengths
563
+ self._s_offsets_seg = s_offsets_seg
564
+ self._s_offsets_arc = s_offsets_arc
565
+ self._perimeter = float(running)
566
+
567
+ def boundary_coordinate(
568
+ self,
569
+ hit_x: float,
570
+ hit_y: float,
571
+ wall_type: int,
572
+ wall_idx: int,
573
+ out_vx: float,
574
+ out_vy: float,
575
+ ) -> tuple[float, float]:
576
+ """Map a boundary hit point to Poincare-section coordinates ``(s, sin_phi)``.
577
+
578
+ Parameters
579
+ ----------
580
+ hit_x, hit_y : float
581
+ Coordinates of the boundary hit point.
582
+ wall_type : int
583
+ ``0`` if `hit_x`, `hit_y` lies on a segment, ``1`` if on an arc.
584
+ wall_idx : int
585
+ Row index of the wall hit into ``self._segments`` or ``self._arcs``.
586
+ out_vx, out_vy : float
587
+ Outgoing (post-reflection) unit velocity at the hit point.
588
+
589
+ Returns
590
+ -------
591
+ s : float
592
+ Boundary arclength coordinate of the hit point.
593
+ sin_phi : float
594
+ Sine of the angle between the outgoing velocity and the local
595
+ boundary tangent (the standard Birkhoff coordinate).
596
+ """
597
+ if wall_type == 0:
598
+ x1, y1, x2, y2 = self._segments[wall_idx]
599
+ ex, ey = x2 - x1, y2 - y1
600
+ elen = np.hypot(ex, ey)
601
+ u = ((hit_x - x1) * ex + (hit_y - y1) * ey) / (elen * elen)
602
+ u = min(max(u, 0.0), 1.0)
603
+ s = self._s_offsets_seg[wall_idx] + u * elen
604
+ tx, ty = ex / elen, ey / elen
605
+ else:
606
+ cx, cy, r, theta1 = self._arcs[wall_idx, :4]
607
+ ang = np.arctan2(hit_y - cy, hit_x - cx)
608
+ sweep_pos = (ang - theta1) % TWO_PI
609
+ arc_len = self._arc_lengths[wall_idx]
610
+ local_s = min(max(sweep_pos * r, 0.0), arc_len)
611
+ s = self._s_offsets_arc[wall_idx] + local_s
612
+ tx, ty = -np.sin(ang), np.cos(ang)
613
+ sin_phi = out_vx * tx + out_vy * ty
614
+ return s, sin_phi
615
+
616
+ def poincare_coordinates(
617
+ self,
618
+ xs: NDArray[np.float64],
619
+ ys: NDArray[np.float64],
620
+ wtypes: NDArray[np.int64],
621
+ widxs: NDArray[np.int64],
622
+ out_vxs: NDArray[np.float64],
623
+ out_vys: NDArray[np.float64],
624
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
625
+ """Map arrays of bounce data to arrays of Poincare-section coordinates.
626
+
627
+ Parameters
628
+ ----------
629
+ xs, ys : ndarray of float, shape (n,)
630
+ Boundary hit-point coordinates.
631
+ wtypes : ndarray of int, shape (n,)
632
+ Wall type struck at each hit (``0`` segment, ``1`` arc).
633
+ widxs : ndarray of int, shape (n,)
634
+ Wall row index struck at each hit.
635
+ out_vxs, out_vys : ndarray of float, shape (n,)
636
+ Outgoing (post-reflection) unit velocity at each hit.
637
+
638
+ Returns
639
+ -------
640
+ s : ndarray of float, shape (n,)
641
+ Boundary arclength coordinate of each hit.
642
+ sin_phi : ndarray of float, shape (n,)
643
+ Sine of the reflection angle at each hit.
644
+ """
645
+ n = xs.shape[0]
646
+ s = np.empty(n)
647
+ sin_phi = np.empty(n)
648
+ for i in range(n):
649
+ s[i], sin_phi[i] = self.boundary_coordinate(xs[i], ys[i], int(wtypes[i]), int(widxs[i]), out_vxs[i], out_vys[i])
650
+ return s, sin_phi
651
+
652
+
653
+ class _RayTracingBilliard(BilliardSystem, _WallBoundaryMixin):
654
+ """Concrete ray-tracing behavior shared by every billiard shape below."""
655
+
656
+ def simulate(self, pos: ArrayLike, vel: ArrayLike, n_bounces: int) -> dict[str, NDArray[Any]]:
657
+ """Trace ``n_bounces`` specular reflections from an initial ray.
658
+
659
+ Parameters
660
+ ----------
661
+ pos : array_like of float, shape (2,)
662
+ Initial position ``(x, y)``; must lie in the billiard's interior.
663
+ vel : array_like of float, shape (2,)
664
+ Initial velocity direction ``(vx, vy)``; normalized internally.
665
+ n_bounces : int
666
+ Number of reflections to simulate.
667
+
668
+ Returns
669
+ -------
670
+ dict
671
+ Dictionary with keys:
672
+
673
+ - ``x``, ``y`` : ndarray of float, shape (n_bounces,) -- hit-point
674
+ coordinates.
675
+ - ``vx``, ``vy`` : ndarray of float, shape (n_bounces,) -- outgoing
676
+ velocity components.
677
+ - ``s`` : ndarray of float, shape (n_bounces,) -- boundary
678
+ arclength coordinate at each hit.
679
+ - ``sin_phi`` : ndarray of float, shape (n_bounces,) -- sine of
680
+ the reflection angle at each hit.
681
+ - ``wall_type`` : ndarray of int64, shape (n_bounces,) -- ``0``
682
+ for a segment, ``1`` for an arc.
683
+ - ``wall_idx`` : ndarray of int64, shape (n_bounces,) -- row index
684
+ of the wall struck.
685
+ """
686
+ pos = np.asarray(pos, dtype=np.float64)
687
+ vel = np.asarray(vel, dtype=np.float64)
688
+ vel = vel / float(np.linalg.norm(vel))
689
+ xs, ys, vxs, vys, wtypes, widxs = simulate_bounces(
690
+ pos[0],
691
+ pos[1],
692
+ vel[0],
693
+ vel[1],
694
+ self._segments,
695
+ self._arcs,
696
+ self._arc_full,
697
+ n_bounces,
698
+ )
699
+ s, sin_phi = self.poincare_coordinates(xs, ys, wtypes, widxs, vxs, vys)
700
+ return {
701
+ "x": xs,
702
+ "y": ys,
703
+ "vx": vxs,
704
+ "vy": vys,
705
+ "s": s,
706
+ "sin_phi": sin_phi,
707
+ "wall_type": wtypes,
708
+ "wall_idx": widxs,
709
+ }
710
+
711
+ def simulate_many_rays(self, pos: ArrayLike, angles: ArrayLike, n_bounces: int) -> dict[str, NDArray[Any]]:
712
+ """Trace many independent rays from one point, in parallel.
713
+
714
+ Same result as calling :meth:`simulate` once per angle in `angles`
715
+ and pooling the outputs, but launches all rays in a single
716
+ ``prange``-parallelized Numba kernel (:func:`simulate_bounces_many`)
717
+ instead of a Python loop -- the batch entry point behind
718
+ :func:`physicskit.chaos.visualizers.phase_space.plot_poincare_section`.
719
+
720
+ Parameters
721
+ ----------
722
+ pos : array_like of float, shape (2,)
723
+ Shared initial position ``(x, y)`` for every ray; must lie in the
724
+ billiard's interior.
725
+ angles : array_like of float, shape (n_rays,)
726
+ Initial launch angle (radians) of each ray.
727
+ n_bounces : int
728
+ Number of reflections to simulate per ray.
729
+
730
+ Returns
731
+ -------
732
+ dict
733
+ Same keys as :meth:`simulate`, but each value is the
734
+ concatenation (in ray order, then bounce order) of every ray's
735
+ results, shape ``(n_rays * n_bounces,)``.
736
+ """
737
+ pos = np.asarray(pos, dtype=np.float64)
738
+ angles = np.asarray(angles, dtype=np.float64)
739
+ xs, ys, vxs, vys, wtypes, widxs = simulate_bounces_many(
740
+ pos[0],
741
+ pos[1],
742
+ angles,
743
+ self._segments,
744
+ self._arcs,
745
+ self._arc_full,
746
+ n_bounces,
747
+ )
748
+ xs, ys, vxs, vys = xs.ravel(), ys.ravel(), vxs.ravel(), vys.ravel()
749
+ wtypes, widxs = wtypes.ravel(), widxs.ravel()
750
+ s, sin_phi = self.poincare_coordinates(xs, ys, wtypes, widxs, vxs, vys)
751
+ return {
752
+ "x": xs,
753
+ "y": ys,
754
+ "vx": vxs,
755
+ "vy": vys,
756
+ "s": s,
757
+ "sin_phi": sin_phi,
758
+ "wall_type": wtypes,
759
+ "wall_idx": widxs,
760
+ }
761
+
762
+ def trajectory_segments(self, pos: ArrayLike, vel: ArrayLike, n_bounces: int) -> tuple[NDArray[np.float64], dict[str, NDArray[Any]]]:
763
+ """Trace a trajectory and return its full real-space polyline.
764
+
765
+ Parameters
766
+ ----------
767
+ pos : array_like of float, shape (2,)
768
+ Initial position ``(x, y)``; must lie in the billiard's interior.
769
+ vel : array_like of float, shape (2,)
770
+ Initial velocity direction ``(vx, vy)``; normalized internally.
771
+ n_bounces : int
772
+ Number of reflections to simulate.
773
+
774
+ Returns
775
+ -------
776
+ path : ndarray of float, shape (n_bounces + 1, 2)
777
+ Positions visited, including the initial position as row 0.
778
+ result : dict
779
+ The same dictionary returned by :meth:`simulate`.
780
+ """
781
+ result = self.simulate(pos, vel, n_bounces)
782
+ pos = np.asarray(pos, dtype=np.float64)
783
+ xs = np.concatenate(([pos[0]], result["x"]))
784
+ ys = np.concatenate(([pos[1]], result["y"]))
785
+ return np.column_stack([xs, ys]), result
786
+
787
+ def boundary_polyline(self, points_per_arc: int = 200) -> NDArray[np.float64]:
788
+ """Trace the boundary as one or more closed polylines, suitable for plotting.
789
+
790
+ Most billiards' boundaries are a single closed loop, but some (e.g.
791
+ :class:`SinaiBilliard`, whose boundary is the outer square *plus* the
792
+ separate, disjoint scatterer circle) have more than one closed
793
+ component. A full-circle arc is never chained to a neighboring wall
794
+ (it is already closed on its own), so it always starts a new
795
+ component; every other wall is chained to its neighbors and closes
796
+ back on itself once the chain returns to its own starting wall.
797
+
798
+ Parameters
799
+ ----------
800
+ points_per_arc : int, default 200
801
+ Number of points used to sample each circular arc.
802
+
803
+ Returns
804
+ -------
805
+ ndarray of float, shape (M, 2)
806
+ Points tracing the boundary in traversal (increasing-``s``)
807
+ order, with each closed component's own first point repeated at
808
+ its end. If the boundary has more than one closed component,
809
+ they are separated by a row of ``NaN`` -- the standard
810
+ Matplotlib/Plotly convention for a line break, so a single
811
+ ``ax.plot(boundary[:, 0], boundary[:, 1])`` call still draws
812
+ every component correctly, with no spurious connecting line
813
+ between them.
814
+ """
815
+ walls: list[tuple[float, str, int]] = []
816
+ for idx in range(self._segments.shape[0]):
817
+ walls.append((self._s_offsets_seg[idx], "seg", idx))
818
+ for idx in range(self._arcs.shape[0]):
819
+ walls.append((self._s_offsets_arc[idx], "arc", idx))
820
+ walls.sort(key=lambda w: w[0])
821
+
822
+ components: list[list[Any]] = []
823
+ current: list[Any] = []
824
+ for _, kind, idx in walls:
825
+ if kind == "seg":
826
+ x1, y1 = self._segments[idx, 0], self._segments[idx, 1]
827
+ current.append([x1, y1])
828
+ elif self._arc_full[idx]:
829
+ # A full circle is already closed and never connects to a
830
+ # neighboring wall, so it is always its own component.
831
+ if current:
832
+ components.append(current)
833
+ current = []
834
+ cx, cy, r, theta1, _theta2 = self._arcs[idx]
835
+ angles = theta1 + np.linspace(0.0, TWO_PI, points_per_arc, endpoint=False)
836
+ components.append(np.column_stack([cx + r * np.cos(angles), cy + r * np.sin(angles)]).tolist())
837
+ else:
838
+ cx, cy, r, theta1, theta2 = self._arcs[idx]
839
+ sweep = (theta2 - theta1) % TWO_PI
840
+ angles = theta1 + np.linspace(0.0, sweep, points_per_arc, endpoint=False)
841
+ current.extend(np.column_stack([cx + r * np.cos(angles), cy + r * np.sin(angles)]).tolist())
842
+ if current:
843
+ components.append(current)
844
+
845
+ rows: list[Any] = []
846
+ for i, component in enumerate(components):
847
+ if i > 0:
848
+ rows.append([np.nan, np.nan])
849
+ rows.extend(component)
850
+ rows.append(component[0])
851
+ return np.asarray(rows)
852
+
853
+
854
+ # ---------------------------------------------------------------------------
855
+ # Concrete billiard shapes
856
+ # ---------------------------------------------------------------------------
857
+
858
+
859
+ class CircleBilliard(_RayTracingBilliard):
860
+ """Integrable circular billiard centered at the origin.
861
+
862
+ Parameters
863
+ ----------
864
+ radius : float, default 1.0
865
+ Circle radius.
866
+
867
+ Attributes
868
+ ----------
869
+ radius : float
870
+ Circle radius.
871
+ """
872
+
873
+ def __init__(self, radius: float = 1.0):
874
+ self.radius = float(radius)
875
+ super().__init__()
876
+
877
+ def _build_boundary(self) -> None:
878
+ self._finalize_boundary([("arc", (0.0, 0.0, self.radius, 0.0, TWO_PI), True)])
879
+
880
+ def sample_interior_point(self) -> NDArray[np.float64]:
881
+ return np.array([0.0, 0.0])
882
+
883
+
884
+ class RectangleBilliard(_RayTracingBilliard):
885
+ """Integrable rectangular billiard centered at the origin.
886
+
887
+ Parameters
888
+ ----------
889
+ width : float, default 2.0
890
+ Full extent along ``x``.
891
+ height : float, default 1.0
892
+ Full extent along ``y``.
893
+
894
+ Attributes
895
+ ----------
896
+ width : float
897
+ Full extent along ``x``.
898
+ height : float
899
+ Full extent along ``y``.
900
+ """
901
+
902
+ def __init__(self, width: float = 2.0, height: float = 1.0):
903
+ self.width = float(width)
904
+ self.height = float(height)
905
+ super().__init__()
906
+
907
+ def _build_boundary(self) -> None:
908
+ w, h = self.width / 2.0, self.height / 2.0
909
+ corners = [(-w, -h), (w, -h), (w, h), (-w, h)]
910
+ walls: list[WallSpec] = [
911
+ (
912
+ "seg",
913
+ (corners[i][0], corners[i][1], corners[(i + 1) % 4][0], corners[(i + 1) % 4][1]),
914
+ )
915
+ for i in range(4)
916
+ ]
917
+ self._finalize_boundary(walls)
918
+
919
+ def sample_interior_point(self) -> NDArray[np.float64]:
920
+ return np.array([0.0, 0.0])
921
+
922
+
923
+ class SinaiBilliard(_RayTracingBilliard):
924
+ """Chaotic (defocusing) Sinai billiard.
925
+
926
+ A square cell with a circular scatterer removed from its center.
927
+
928
+ Parameters
929
+ ----------
930
+ cell_size : float, default 2.0
931
+ Full side length of the square cell.
932
+ scatterer_radius : float, default 0.5
933
+ Radius of the central circular scatterer; must be smaller than half
934
+ of `cell_size`.
935
+
936
+ Attributes
937
+ ----------
938
+ cell_size : float
939
+ Full side length of the square cell.
940
+ scatterer_radius : float
941
+ Radius of the central circular scatterer.
942
+
943
+ Raises
944
+ ------
945
+ ValueError
946
+ If `scatterer_radius` is not smaller than half of `cell_size`.
947
+ """
948
+
949
+ def __init__(self, cell_size: float = 2.0, scatterer_radius: float = 0.5):
950
+ if scatterer_radius >= cell_size / 2.0:
951
+ raise InvalidParameterError("scatterer_radius must be smaller than half the cell_size")
952
+ self.cell_size = float(cell_size)
953
+ self.scatterer_radius = float(scatterer_radius)
954
+ super().__init__()
955
+
956
+ def _build_boundary(self) -> None:
957
+ h = self.cell_size / 2.0
958
+ corners = [(-h, -h), (h, -h), (h, h), (-h, h)]
959
+ walls: list[WallSpec] = [
960
+ (
961
+ "seg",
962
+ (corners[i][0], corners[i][1], corners[(i + 1) % 4][0], corners[(i + 1) % 4][1]),
963
+ )
964
+ for i in range(4)
965
+ ]
966
+ walls.append(("arc", (0.0, 0.0, self.scatterer_radius, 0.0, TWO_PI), True))
967
+ self._finalize_boundary(walls)
968
+
969
+ def sample_interior_point(self) -> NDArray[np.float64]:
970
+ h = self.cell_size / 2.0
971
+ return np.array([(h + self.scatterer_radius) / 2.0, 0.0])
972
+
973
+
974
+ class BunimovichStadium(_RayTracingBilliard):
975
+ """Chaotic (defocusing) Bunimovich stadium billiard.
976
+
977
+ Two semicircles joined by straight edges.
978
+
979
+ Parameters
980
+ ----------
981
+ radius : float, default 1.0
982
+ Radius of the two semicircular end-caps.
983
+ straight_length : float, default 2.0
984
+ Length of the straight edges joining the semicircles.
985
+
986
+ Attributes
987
+ ----------
988
+ radius : float
989
+ Radius of the two semicircular end-caps.
990
+ straight_length : float
991
+ Length of the straight edges joining the semicircles.
992
+ """
993
+
994
+ def __init__(self, radius: float = 1.0, straight_length: float = 2.0):
995
+ self.radius = float(radius)
996
+ self.straight_length = float(straight_length)
997
+ super().__init__()
998
+
999
+ def _build_boundary(self) -> None:
1000
+ r, a = self.radius, self.straight_length / 2.0
1001
+ walls: list[WallSpec] = [
1002
+ ("seg", (-a, -r, a, -r)), # bottom edge
1003
+ ("arc", (a, 0.0, r, -np.pi / 2.0, np.pi / 2.0), False), # right semicircle
1004
+ ("seg", (a, r, -a, r)), # top edge
1005
+ ("arc", (-a, 0.0, r, np.pi / 2.0, 3.0 * np.pi / 2.0), False), # left semicircle
1006
+ ]
1007
+ self._finalize_boundary(walls)
1008
+
1009
+ def sample_interior_point(self) -> NDArray[np.float64]:
1010
+ return np.array([0.0, 0.0])
1011
+
1012
+
1013
+ class TruncatedCircleBilliard(_RayTracingBilliard):
1014
+ """A disk truncated by a straight chord.
1015
+
1016
+ The region ``x <= radius - cut`` of the disk of the given `radius` is
1017
+ kept.
1018
+
1019
+ Parameters
1020
+ ----------
1021
+ radius : float, default 1.0
1022
+ Disk radius.
1023
+ cut : float, default 0.3
1024
+ Distance the chord is cut in from the disk's edge; must satisfy
1025
+ ``0 < cut < radius``.
1026
+
1027
+ Attributes
1028
+ ----------
1029
+ radius : float
1030
+ Disk radius.
1031
+ cut : float
1032
+ Distance the chord is cut in from the disk's edge.
1033
+
1034
+ Raises
1035
+ ------
1036
+ ValueError
1037
+ If `cut` does not satisfy ``0 < cut < radius``.
1038
+ """
1039
+
1040
+ def __init__(self, radius: float = 1.0, cut: float = 0.3):
1041
+ if not 0.0 < cut < radius:
1042
+ raise InvalidParameterError("cut must satisfy 0 < cut < radius")
1043
+ self.radius = float(radius)
1044
+ self.cut = float(cut)
1045
+ super().__init__()
1046
+
1047
+ def _build_boundary(self) -> None:
1048
+ r = self.radius
1049
+ x_cut = r - self.cut
1050
+ y_cut = np.sqrt(r * r - x_cut * x_cut)
1051
+ theta_cut = np.arctan2(y_cut, x_cut)
1052
+ walls: list[WallSpec] = [
1053
+ # Major arc through pi, from theta_cut to (2*pi - theta_cut).
1054
+ ("arc", (0.0, 0.0, r, theta_cut, TWO_PI - theta_cut), False),
1055
+ # Chord closing the loop, continuing CCW.
1056
+ ("seg", (x_cut, -y_cut, x_cut, y_cut)),
1057
+ ]
1058
+ self._finalize_boundary(walls)
1059
+
1060
+ def sample_interior_point(self) -> NDArray[np.float64]:
1061
+ return np.array([-self.radius / 2.0, 0.0])
1062
+
1063
+
1064
+ class EllipseBilliard(_RayTracingBilliard):
1065
+ """Integrable elliptical billiard, centered at the origin.
1066
+
1067
+ The ellipse billiard is integrable: every trajectory remains tangent to a
1068
+ single confocal caustic for all time -- either a confocal ellipse (for
1069
+ trajectories that never cross the segment joining the two foci) or a
1070
+ confocal hyperbola (for trajectories that do) -- so the Poincare section
1071
+ is foliated by smooth invariant curves, like the Circle and Rectangle
1072
+ billiards. Unlike those two, its boundary is not built from exact circular
1073
+ arcs, so it is represented as a fine closed polygon of `n_segments`
1074
+ straight edges sampled from the ellipse's parametric form; the resulting
1075
+ discretization error in the physics is negligible at the default
1076
+ resolution (perimeter error ``O(1/n_segments^2)``).
1077
+
1078
+ Parameters
1079
+ ----------
1080
+ semi_major : float, default 1.5
1081
+ Semi-major axis, along ``x``.
1082
+ semi_minor : float, default 1.0
1083
+ Semi-minor axis, along ``y``; must be smaller than `semi_major`.
1084
+ n_segments : int, default 2000
1085
+ Number of straight edges used to approximate the smooth ellipse.
1086
+
1087
+ Attributes
1088
+ ----------
1089
+ semi_major, semi_minor : float
1090
+ Ellipse semi-axes.
1091
+ n_segments : int
1092
+ Polygon-approximation resolution.
1093
+
1094
+ Raises
1095
+ ------
1096
+ ValueError
1097
+ If `semi_major` or `semi_minor` is not positive, or `semi_major` does
1098
+ not exceed `semi_minor`.
1099
+ """
1100
+
1101
+ def __init__(self, semi_major: float = 1.5, semi_minor: float = 1.0, n_segments: int = 2000):
1102
+ if semi_major <= 0.0 or semi_minor <= 0.0:
1103
+ raise InvalidParameterError("semi_major and semi_minor must be positive")
1104
+ if semi_major <= semi_minor:
1105
+ raise InvalidParameterError("semi_major must be greater than semi_minor")
1106
+ self.semi_major = float(semi_major)
1107
+ self.semi_minor = float(semi_minor)
1108
+ self.n_segments = int(n_segments)
1109
+ super().__init__()
1110
+
1111
+ def _build_boundary(self) -> None:
1112
+ a, b, n = self.semi_major, self.semi_minor, self.n_segments
1113
+ theta = np.linspace(0.0, TWO_PI, n, endpoint=False)
1114
+ xs = a * np.cos(theta)
1115
+ ys = b * np.sin(theta)
1116
+ walls: list[WallSpec] = [("seg", (xs[i], ys[i], xs[(i + 1) % n], ys[(i + 1) % n])) for i in range(n)]
1117
+ self._finalize_boundary(walls)
1118
+
1119
+ def sample_interior_point(self) -> NDArray[np.float64]:
1120
+ return np.array([0.0, 0.0])
1121
+
1122
+ def foci(self) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
1123
+ """The two foci of the ellipse, at ``(+-c, 0)`` with ``c = sqrt(a^2 - b^2)``.
1124
+
1125
+ Returns
1126
+ -------
1127
+ f1, f2 : ndarray of float, shape (2,)
1128
+ The two focal points.
1129
+ """
1130
+ c = np.sqrt(self.semi_major**2 - self.semi_minor**2)
1131
+ return np.array([c, 0.0]), np.array([-c, 0.0])