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.
- physicskit/__init__.py +51 -0
- physicskit/astro/__init__.py +66 -0
- physicskit/astro/cosmic_web.py +395 -0
- physicskit/astro/galactic_dynamics.py +137 -0
- physicskit/astro/nbody.py +208 -0
- physicskit/astro/orbital_mechanics.py +220 -0
- physicskit/astro/stellar_dynamo.py +628 -0
- physicskit/astro/stellar_structure.py +198 -0
- physicskit/astro/tests/__init__.py +0 -0
- physicskit/astro/tests/conftest.py +7 -0
- physicskit/astro/tests/test_cosmic_web.py +159 -0
- physicskit/astro/tests/test_galactic_dynamics.py +46 -0
- physicskit/astro/tests/test_nbody.py +81 -0
- physicskit/astro/tests/test_orbital_mechanics.py +73 -0
- physicskit/astro/tests/test_stellar_dynamo.py +198 -0
- physicskit/astro/tests/test_stellar_structure.py +52 -0
- physicskit/astro/tests/test_visualizers.py +159 -0
- physicskit/astro/visualizers.py +628 -0
- physicskit/chaos/__init__.py +55 -0
- physicskit/chaos/core/__init__.py +3 -0
- physicskit/chaos/core/base_system.py +342 -0
- physicskit/chaos/core/integrators.py +40 -0
- physicskit/chaos/exceptions.py +24 -0
- physicskit/chaos/quantum/__init__.py +38 -0
- physicskit/chaos/quantum/billiards.py +274 -0
- physicskit/chaos/quantum/husimi.py +85 -0
- physicskit/chaos/quantum/maps.py +366 -0
- physicskit/chaos/systems/__init__.py +38 -0
- physicskit/chaos/systems/billiards.py +1131 -0
- physicskit/chaos/systems/continuous.py +1276 -0
- physicskit/chaos/systems/maps.py +557 -0
- physicskit/chaos/tests/__init__.py +0 -0
- physicskit/chaos/tests/test_basins.py +76 -0
- physicskit/chaos/tests/test_bifurcation.py +132 -0
- physicskit/chaos/tests/test_billiards.py +115 -0
- physicskit/chaos/tests/test_continuous.py +224 -0
- physicskit/chaos/tests/test_dimension.py +69 -0
- physicskit/chaos/tests/test_divergence.py +90 -0
- physicskit/chaos/tests/test_dynamic_plots.py +269 -0
- physicskit/chaos/tests/test_integrators.py +96 -0
- physicskit/chaos/tests/test_io.py +41 -0
- physicskit/chaos/tests/test_maps.py +110 -0
- physicskit/chaos/tests/test_metrics.py +115 -0
- physicskit/chaos/tests/test_phase_space.py +73 -0
- physicskit/chaos/tests/test_properties.py +116 -0
- physicskit/chaos/tests/test_quantum_billiards.py +94 -0
- physicskit/chaos/tests/test_quantum_maps.py +144 -0
- physicskit/chaos/tests/test_recurrence.py +51 -0
- physicskit/chaos/tests/test_repr_and_api_consistency.py +134 -0
- physicskit/chaos/tests/test_section.py +49 -0
- physicskit/chaos/tests/test_spectral.py +48 -0
- physicskit/chaos/tests/test_streamlit_app.py +45 -0
- physicskit/chaos/tests/test_timeseries.py +131 -0
- physicskit/chaos/tests/test_viewer3d.py +41 -0
- physicskit/chaos/utils/__init__.py +46 -0
- physicskit/chaos/utils/dimension.py +139 -0
- physicskit/chaos/utils/io.py +95 -0
- physicskit/chaos/utils/metrics.py +304 -0
- physicskit/chaos/utils/recurrence.py +122 -0
- physicskit/chaos/utils/spectral.py +69 -0
- physicskit/chaos/utils/timeseries.py +303 -0
- physicskit/chaos/visualizers/__init__.py +85 -0
- physicskit/chaos/visualizers/basins.py +185 -0
- physicskit/chaos/visualizers/bifurcation.py +273 -0
- physicskit/chaos/visualizers/divergence.py +374 -0
- physicskit/chaos/visualizers/dynamic_plots.py +1372 -0
- physicskit/chaos/visualizers/phase_space.py +133 -0
- physicskit/chaos/visualizers/quantum.py +299 -0
- physicskit/chaos/visualizers/recurrence.py +91 -0
- physicskit/chaos/visualizers/section.py +161 -0
- physicskit/chaos/visualizers/theme.py +31 -0
- physicskit/chaos/visualizers/viewer3d.py +82 -0
- physicskit/classical/__init__.py +55 -0
- physicskit/classical/core/__init__.py +17 -0
- physicskit/classical/core/base_system.py +510 -0
- physicskit/classical/core/integrators.py +397 -0
- physicskit/classical/systems/__init__.py +32 -0
- physicskit/classical/systems/chains.py +334 -0
- physicskit/classical/systems/hamiltonian.py +215 -0
- physicskit/classical/systems/lagrangian.py +308 -0
- physicskit/classical/systems/newtonian.py +536 -0
- physicskit/classical/systems/rotations.py +614 -0
- physicskit/classical/tests/__init__.py +0 -0
- physicskit/classical/tests/conftest.py +7 -0
- physicskit/classical/tests/test_base_system.py +113 -0
- physicskit/classical/tests/test_chains.py +84 -0
- physicskit/classical/tests/test_conservation.py +244 -0
- physicskit/classical/tests/test_conservation_utils.py +47 -0
- physicskit/classical/tests/test_hamiltonian.py +44 -0
- physicskit/classical/tests/test_integrators.py +123 -0
- physicskit/classical/tests/test_lagrangian.py +69 -0
- physicskit/classical/tests/test_newtonian.py +45 -0
- physicskit/classical/tests/test_rotations.py +160 -0
- physicskit/classical/tests/test_stepsize.py +149 -0
- physicskit/classical/tests/test_symbolic.py +125 -0
- physicskit/classical/tests/test_visualizers.py +172 -0
- physicskit/classical/utils/__init__.py +20 -0
- physicskit/classical/utils/conservation.py +126 -0
- physicskit/classical/utils/stepsize.py +187 -0
- physicskit/classical/utils/symbolic.py +235 -0
- physicskit/classical/visualizers/__init__.py +25 -0
- physicskit/classical/visualizers/animations.py +508 -0
- physicskit/classical/visualizers/interactive.py +113 -0
- physicskit/classical/visualizers/modal_analysis.py +97 -0
- physicskit/classical/visualizers/phase_space.py +214 -0
- physicskit/condensed/__init__.py +122 -0
- physicskit/condensed/anderson_localization.py +155 -0
- physicskit/condensed/correlated.py +284 -0
- physicskit/condensed/ginzburg_landau.py +220 -0
- physicskit/condensed/landau_levels.py +246 -0
- physicskit/condensed/laughlin.py +229 -0
- physicskit/condensed/models.py +627 -0
- physicskit/condensed/tests/__init__.py +0 -0
- physicskit/condensed/tests/test_condensed.py +518 -0
- physicskit/condensed/tests/test_visualizers.py +87 -0
- physicskit/condensed/tight_binding.py +445 -0
- physicskit/condensed/topological_insulator_3d.py +178 -0
- physicskit/condensed/topology.py +217 -0
- physicskit/condensed/visualizers.py +296 -0
- physicskit/condensed/weyl.py +180 -0
- physicskit/constants.py +184 -0
- physicskit/fields/__init__.py +111 -0
- physicskit/fields/electrodynamics.py +671 -0
- physicskit/fields/quantum_fields.py +510 -0
- physicskit/fields/solitons.py +527 -0
- physicskit/fields/tests/__init__.py +0 -0
- physicskit/fields/tests/conftest.py +7 -0
- physicskit/fields/tests/test_field_animations.py +154 -0
- physicskit/fields/tests/test_fields.py +439 -0
- physicskit/fields/tests/test_visualizers.py +101 -0
- physicskit/fields/visualizers.py +503 -0
- physicskit/fluids/__init__.py +134 -0
- physicskit/fluids/core/__init__.py +23 -0
- physicskit/fluids/core/grid.py +165 -0
- physicskit/fluids/core/timestepping.py +231 -0
- physicskit/fluids/exceptions.py +26 -0
- physicskit/fluids/systems/__init__.py +70 -0
- physicskit/fluids/systems/compressible_flow.py +281 -0
- physicskit/fluids/systems/instabilities.py +343 -0
- physicskit/fluids/systems/navier_stokes.py +221 -0
- physicskit/fluids/systems/potential_flow.py +440 -0
- physicskit/fluids/systems/viscous_flow.py +425 -0
- physicskit/fluids/systems/vortex_dynamics.py +304 -0
- physicskit/fluids/tests/__init__.py +0 -0
- physicskit/fluids/tests/conftest.py +7 -0
- physicskit/fluids/tests/test_compressible_flow.py +66 -0
- physicskit/fluids/tests/test_instabilities.py +115 -0
- physicskit/fluids/tests/test_navier_stokes.py +67 -0
- physicskit/fluids/tests/test_potential_flow.py +92 -0
- physicskit/fluids/tests/test_static_visualizers.py +91 -0
- physicskit/fluids/tests/test_utils.py +100 -0
- physicskit/fluids/tests/test_viscous_flow.py +110 -0
- physicskit/fluids/tests/test_visualizers.py +34 -0
- physicskit/fluids/tests/test_vortex_dynamics.py +88 -0
- physicskit/fluids/utils/__init__.py +18 -0
- physicskit/fluids/utils/dimensionless.py +239 -0
- physicskit/fluids/utils/spectral_analysis.py +142 -0
- physicskit/fluids/visualizers/__init__.py +14 -0
- physicskit/fluids/visualizers/compressible.py +39 -0
- physicskit/fluids/visualizers/flow_fields.py +252 -0
- physicskit/fluids/visualizers/potential_flow.py +51 -0
- physicskit/fluids/visualizers/spectra.py +62 -0
- physicskit/fluids/visualizers/theme.py +38 -0
- physicskit/integrators/__init__.py +52 -0
- physicskit/integrators/adaptive.py +199 -0
- physicskit/integrators/fixed_step.py +360 -0
- physicskit/integrators/tests/__init__.py +0 -0
- physicskit/integrators/tests/test_integrators.py +101 -0
- physicskit/optics/__init__.py +124 -0
- physicskit/optics/_compat.py +5 -0
- physicskit/optics/gaussian.py +382 -0
- physicskit/optics/quantum_optics.py +428 -0
- physicskit/optics/ray.py +469 -0
- physicskit/optics/tests/__init__.py +0 -0
- physicskit/optics/tests/test_diffraction_animation.py +38 -0
- physicskit/optics/tests/test_gaussian.py +170 -0
- physicskit/optics/tests/test_quantum_optics.py +147 -0
- physicskit/optics/tests/test_ray.py +177 -0
- physicskit/optics/tests/test_visualizers.py +58 -0
- physicskit/optics/tests/test_wave.py +146 -0
- physicskit/optics/visualizers.py +264 -0
- physicskit/optics/wave.py +365 -0
- physicskit/particle/__init__.py +152 -0
- physicskit/particle/collider.py +382 -0
- physicskit/particle/confinement.py +125 -0
- physicskit/particle/decays.py +357 -0
- physicskit/particle/electroweak.py +300 -0
- physicskit/particle/kinematics.py +271 -0
- physicskit/particle/neutrinos.py +84 -0
- physicskit/particle/nuclear.py +100 -0
- physicskit/particle/scattering.py +159 -0
- physicskit/particle/tests/__init__.py +0 -0
- physicskit/particle/tests/conftest.py +7 -0
- physicskit/particle/tests/test_collider.py +119 -0
- physicskit/particle/tests/test_confinement.py +42 -0
- physicskit/particle/tests/test_decays.py +112 -0
- physicskit/particle/tests/test_electroweak.py +97 -0
- physicskit/particle/tests/test_kinematics.py +138 -0
- physicskit/particle/tests/test_neutrinos.py +36 -0
- physicskit/particle/tests/test_nuclear.py +45 -0
- physicskit/particle/tests/test_scattering.py +64 -0
- physicskit/particle/tests/test_visualizers.py +214 -0
- physicskit/particle/visualizers/__init__.py +38 -0
- physicskit/particle/visualizers/animations.py +705 -0
- physicskit/particle/visualizers/static.py +101 -0
- physicskit/plasma/__init__.py +176 -0
- physicskit/plasma/acceleration.py +152 -0
- physicskit/plasma/instabilities.py +417 -0
- physicskit/plasma/kinetic.py +588 -0
- physicskit/plasma/mhd.py +463 -0
- physicskit/plasma/single_particle.py +562 -0
- physicskit/plasma/tests/__init__.py +0 -0
- physicskit/plasma/tests/conftest.py +7 -0
- physicskit/plasma/tests/test_new_time_domain_features.py +222 -0
- physicskit/plasma/tests/test_plasma.py +232 -0
- physicskit/plasma/tests/test_visualizers.py +79 -0
- physicskit/plasma/turbulence.py +219 -0
- physicskit/plasma/visualizers.py +868 -0
- physicskit/plasma/waves.py +549 -0
- physicskit/quantum/__init__.py +180 -0
- physicskit/quantum/_compat.py +20 -0
- physicskit/quantum/chapters/__init__.py +0 -0
- physicskit/quantum/chapters/entanglement.py +489 -0
- physicskit/quantum/chapters/harmonic_spin.py +517 -0
- physicskit/quantum/chapters/hydrogen_am.py +292 -0
- physicskit/quantum/chapters/perturbation.py +350 -0
- physicskit/quantum/chapters/potentials.py +997 -0
- physicskit/quantum/chapters/spin.py +306 -0
- physicskit/quantum/chapters/wave_packets.py +541 -0
- physicskit/quantum/core/__init__.py +0 -0
- physicskit/quantum/core/eigensolvers.py +311 -0
- physicskit/quantum/core/operators.py +296 -0
- physicskit/quantum/core/solvers.py +271 -0
- physicskit/quantum/tests/__init__.py +0 -0
- physicskit/quantum/tests/conftest.py +7 -0
- physicskit/quantum/tests/test_animations.py +225 -0
- physicskit/quantum/tests/test_compat.py +43 -0
- physicskit/quantum/tests/test_eigensolvers.py +24 -0
- physicskit/quantum/tests/test_entanglement.py +127 -0
- physicskit/quantum/tests/test_harmonic_spin.py +140 -0
- physicskit/quantum/tests/test_hydrogen_am.py +48 -0
- physicskit/quantum/tests/test_measure.py +110 -0
- physicskit/quantum/tests/test_operators.py +105 -0
- physicskit/quantum/tests/test_perturbation.py +112 -0
- physicskit/quantum/tests/test_physics_checks.py +131 -0
- physicskit/quantum/tests/test_potentials.py +191 -0
- physicskit/quantum/tests/test_solvers.py +70 -0
- physicskit/quantum/tests/test_spin.py +22 -0
- physicskit/quantum/tests/test_unitarity.py +84 -0
- physicskit/quantum/tests/test_wave_packets.py +53 -0
- physicskit/quantum/utils/__init__.py +0 -0
- physicskit/quantum/utils/measure.py +253 -0
- physicskit/quantum/visualizers/__init__.py +0 -0
- physicskit/quantum/visualizers/bloch_sphere.py +205 -0
- physicskit/quantum/visualizers/entanglement.py +58 -0
- physicskit/quantum/visualizers/orbitals.py +187 -0
- physicskit/quantum/visualizers/phase_space.py +179 -0
- physicskit/quantum/visualizers/wavefunctions.py +230 -0
- physicskit/relativity/__init__.py +61 -0
- physicskit/relativity/chapters/__init__.py +33 -0
- physicskit/relativity/chapters/cosmology.py +190 -0
- physicskit/relativity/chapters/gw_merger.py +399 -0
- physicskit/relativity/chapters/kerr.py +371 -0
- physicskit/relativity/chapters/lensing.py +196 -0
- physicskit/relativity/chapters/neutron_star.py +149 -0
- physicskit/relativity/chapters/schwarzschild.py +412 -0
- physicskit/relativity/chapters/timekeeping.py +133 -0
- physicskit/relativity/core/__init__.py +12 -0
- physicskit/relativity/core/geodesics.py +216 -0
- physicskit/relativity/core/kerr_raytracer.py +259 -0
- physicskit/relativity/core/raytracer.py +241 -0
- physicskit/relativity/core/tensors.py +468 -0
- physicskit/relativity/tests/__init__.py +0 -0
- physicskit/relativity/tests/test_conservations.py +54 -0
- physicskit/relativity/tests/test_constants.py +34 -0
- physicskit/relativity/tests/test_cosmology.py +61 -0
- physicskit/relativity/tests/test_gw_merger.py +117 -0
- physicskit/relativity/tests/test_kerr.py +116 -0
- physicskit/relativity/tests/test_kerr_raytracer.py +103 -0
- physicskit/relativity/tests/test_lensing.py +75 -0
- physicskit/relativity/tests/test_neutron_star.py +48 -0
- physicskit/relativity/tests/test_raytracer.py +67 -0
- physicskit/relativity/tests/test_schwarzschild.py +126 -0
- physicskit/relativity/tests/test_shadow_render.py +81 -0
- physicskit/relativity/tests/test_tensors.py +86 -0
- physicskit/relativity/tests/test_timekeeping.py +51 -0
- physicskit/relativity/tests/test_visualizers_misc.py +115 -0
- physicskit/relativity/utils/__init__.py +1 -0
- physicskit/relativity/utils/constants.py +232 -0
- physicskit/relativity/visualizers/__init__.py +46 -0
- physicskit/relativity/visualizers/interactive.py +127 -0
- physicskit/relativity/visualizers/shadow_render.py +367 -0
- physicskit/relativity/visualizers/spacetime_3d.py +92 -0
- physicskit/relativity/visualizers/spacetime_diagrams.py +198 -0
- physicskit/relativity/visualizers/wave_plots.py +185 -0
- physicskit/rmt/__init__.py +8 -0
- physicskit/rmt/ensembles/__init__.py +108 -0
- physicskit/rmt/ensembles/banded.py +114 -0
- physicskit/rmt/ensembles/base.py +135 -0
- physicskit/rmt/ensembles/bdg.py +218 -0
- physicskit/rmt/ensembles/chiral.py +308 -0
- physicskit/rmt/ensembles/circular.py +104 -0
- physicskit/rmt/ensembles/crossover.py +82 -0
- physicskit/rmt/ensembles/density_matrix.py +148 -0
- physicskit/rmt/ensembles/effective_hamiltonian.py +230 -0
- physicskit/rmt/ensembles/embedded.py +207 -0
- physicskit/rmt/ensembles/gaussian.py +153 -0
- physicskit/rmt/ensembles/ginibre.py +110 -0
- physicskit/rmt/ensembles/girko.py +190 -0
- physicskit/rmt/ensembles/graph_laplacian.py +92 -0
- physicskit/rmt/ensembles/haar.py +57 -0
- physicskit/rmt/ensembles/jacobi.py +182 -0
- physicskit/rmt/ensembles/poisson.py +77 -0
- physicskit/rmt/ensembles/polynomial.py +139 -0
- physicskit/rmt/ensembles/pt_symmetric.py +120 -0
- physicskit/rmt/ensembles/single_ring.py +170 -0
- physicskit/rmt/ensembles/sparse.py +95 -0
- physicskit/rmt/ensembles/syk.py +170 -0
- physicskit/rmt/ensembles/truncated_unitary.py +80 -0
- physicskit/rmt/ensembles/universality.py +92 -0
- physicskit/rmt/ensembles/wishart.py +88 -0
- physicskit/rmt/py.typed +0 -0
- physicskit/rmt/spectrum.py +82 -0
- physicskit/rmt/stats/__init__.py +160 -0
- physicskit/rmt/stats/characteristic_polynomial.py +77 -0
- physicskit/rmt/stats/circular_law.py +54 -0
- physicskit/rmt/stats/correlations.py +162 -0
- physicskit/rmt/stats/density.py +70 -0
- physicskit/rmt/stats/entanglement.py +36 -0
- physicskit/rmt/stats/free_probability.py +202 -0
- physicskit/rmt/stats/localization.py +338 -0
- physicskit/rmt/stats/marchenko_pastur.py +55 -0
- physicskit/rmt/stats/pt_symmetric.py +265 -0
- physicskit/rmt/stats/ratios.py +110 -0
- physicskit/rmt/stats/real_ginibre.py +134 -0
- physicskit/rmt/stats/rigidity.py +272 -0
- physicskit/rmt/stats/single_ring.py +95 -0
- physicskit/rmt/stats/spacing.py +150 -0
- physicskit/rmt/stats/tracy_widom.py +175 -0
- physicskit/rmt/stats/truncated_unitary.py +40 -0
- physicskit/rmt/stats/unfolding.py +37 -0
- physicskit/rmt/stats/wachter.py +67 -0
- physicskit/rmt/tests/__init__.py +0 -0
- physicskit/rmt/tests/cache_utils.py +107 -0
- physicskit/rmt/tests/conftest.py +8 -0
- physicskit/rmt/tests/test_banded.py +86 -0
- physicskit/rmt/tests/test_base.py +29 -0
- physicskit/rmt/tests/test_bdg.py +133 -0
- physicskit/rmt/tests/test_characteristic_polynomial.py +50 -0
- physicskit/rmt/tests/test_chiral.py +273 -0
- physicskit/rmt/tests/test_circular.py +225 -0
- physicskit/rmt/tests/test_crossover.py +72 -0
- physicskit/rmt/tests/test_density.py +23 -0
- physicskit/rmt/tests/test_density_matrix.py +181 -0
- physicskit/rmt/tests/test_effective_hamiltonian.py +140 -0
- physicskit/rmt/tests/test_embedded.py +169 -0
- physicskit/rmt/tests/test_free_probability.py +81 -0
- physicskit/rmt/tests/test_gaussian_ensembles.py +22 -0
- physicskit/rmt/tests/test_gaussian_semicircle.py +127 -0
- physicskit/rmt/tests/test_ginibre_circular_law.py +131 -0
- physicskit/rmt/tests/test_girko.py +178 -0
- physicskit/rmt/tests/test_graph_laplacian.py +102 -0
- physicskit/rmt/tests/test_haar.py +118 -0
- physicskit/rmt/tests/test_jacobi_wachter.py +136 -0
- physicskit/rmt/tests/test_localization.py +240 -0
- physicskit/rmt/tests/test_poisson.py +149 -0
- physicskit/rmt/tests/test_polynomial.py +107 -0
- physicskit/rmt/tests/test_pt_symmetric.py +178 -0
- physicskit/rmt/tests/test_random_state.py +19 -0
- physicskit/rmt/tests/test_real_ginibre.py +112 -0
- physicskit/rmt/tests/test_rigidity_universality.py +167 -0
- physicskit/rmt/tests/test_single_ring.py +132 -0
- physicskit/rmt/tests/test_spacing_and_ratios.py +122 -0
- physicskit/rmt/tests/test_sparse.py +71 -0
- physicskit/rmt/tests/test_spectrum.py +27 -0
- physicskit/rmt/tests/test_syk.py +136 -0
- physicskit/rmt/tests/test_tracy_widom.py +113 -0
- physicskit/rmt/tests/test_tridiagonal.py +41 -0
- physicskit/rmt/tests/test_truncated_unitary.py +80 -0
- physicskit/rmt/tests/test_validation_base.py +25 -0
- physicskit/rmt/tests/test_validation_misc.py +67 -0
- physicskit/rmt/tests/test_wishart_marchenko_pastur.py +120 -0
- physicskit/rmt/utils/__init__.py +0 -0
- physicskit/rmt/utils/haar.py +128 -0
- physicskit/rmt/utils/random_state.py +24 -0
- physicskit/rmt/utils/tridiagonal.py +167 -0
- physicskit/rmt/validation/__init__.py +36 -0
- physicskit/rmt/validation/base.py +126 -0
- physicskit/rmt/validation/circular_law.py +80 -0
- physicskit/rmt/validation/marchenko_pastur.py +53 -0
- physicskit/rmt/validation/ratio_distribution.py +53 -0
- physicskit/rmt/validation/real_ginibre.py +60 -0
- physicskit/rmt/validation/sine_kernel.py +57 -0
- physicskit/rmt/validation/single_ring.py +68 -0
- physicskit/rmt/validation/tracy_widom.py +89 -0
- physicskit/rmt/validation/universality.py +100 -0
- physicskit/rmt/validation/wachter.py +52 -0
- physicskit/rmt/validation/wigner_semicircle.py +40 -0
- physicskit/rmt/validation/wigner_surmise.py +58 -0
- physicskit/semiclassical/__init__.py +94 -0
- physicskit/semiclassical/_compat.py +5 -0
- physicskit/semiclassical/core/__init__.py +41 -0
- physicskit/semiclassical/core/gutzwiller.py +215 -0
- physicskit/semiclassical/core/path_integral.py +594 -0
- physicskit/semiclassical/core/propagators.py +579 -0
- physicskit/semiclassical/core/wkb.py +308 -0
- physicskit/semiclassical/systems/__init__.py +13 -0
- physicskit/semiclassical/systems/scarring.py +278 -0
- physicskit/semiclassical/tests/__init__.py +0 -0
- physicskit/semiclassical/tests/test_gutzwiller.py +35 -0
- physicskit/semiclassical/tests/test_path_integral.py +194 -0
- physicskit/semiclassical/tests/test_propagators.py +82 -0
- physicskit/semiclassical/tests/test_scarring.py +43 -0
- physicskit/semiclassical/tests/test_visualizers.py +89 -0
- physicskit/semiclassical/tests/test_wkb.py +30 -0
- physicskit/semiclassical/visualizers/__init__.py +17 -0
- physicskit/semiclassical/visualizers/gutzwiller.py +55 -0
- physicskit/semiclassical/visualizers/path_integral.py +195 -0
- physicskit/semiclassical/visualizers/propagators.py +89 -0
- physicskit/semiclassical/visualizers/scarring.py +146 -0
- physicskit/semiclassical/visualizers/wkb.py +55 -0
- physicskit/statphys/__init__.py +80 -0
- physicskit/statphys/chapters/__init__.py +43 -0
- physicskit/statphys/chapters/ehrenfest_urn.py +112 -0
- physicskit/statphys/chapters/ising_lattice.py +431 -0
- physicskit/statphys/chapters/kpz_growth.py +129 -0
- physicskit/statphys/chapters/molecular_dynamics.py +258 -0
- physicskit/statphys/chapters/nonequilibrium_work.py +146 -0
- physicskit/statphys/chapters/percolation.py +305 -0
- physicskit/statphys/chapters/random_walk.py +142 -0
- physicskit/statphys/chapters/renormalization.py +148 -0
- physicskit/statphys/chapters/sandpile.py +109 -0
- physicskit/statphys/chapters/spin_glass.py +275 -0
- physicskit/statphys/core/__init__.py +10 -0
- physicskit/statphys/core/kpz_engine.py +53 -0
- physicskit/statphys/core/md_engine.py +157 -0
- physicskit/statphys/core/monte_carlo.py +462 -0
- physicskit/statphys/core/random_walk_engine.py +68 -0
- physicskit/statphys/core/sandpile_engine.py +52 -0
- physicskit/statphys/tests/__init__.py +0 -0
- physicskit/statphys/tests/test_dynamics.py +66 -0
- physicskit/statphys/tests/test_ehrenfest_urn.py +44 -0
- physicskit/statphys/tests/test_finite_size_scaling.py +62 -0
- physicskit/statphys/tests/test_interactive.py +33 -0
- physicskit/statphys/tests/test_ising.py +68 -0
- physicskit/statphys/tests/test_kpz.py +55 -0
- physicskit/statphys/tests/test_landau_theory.py +49 -0
- physicskit/statphys/tests/test_md_engine.py +31 -0
- physicskit/statphys/tests/test_molecular_dynamics.py +72 -0
- physicskit/statphys/tests/test_nonequilibrium_work.py +33 -0
- physicskit/statphys/tests/test_partition_function.py +26 -0
- physicskit/statphys/tests/test_percolation.py +116 -0
- physicskit/statphys/tests/test_potts_model.py +46 -0
- physicskit/statphys/tests/test_random_walk.py +50 -0
- physicskit/statphys/tests/test_renormalization.py +44 -0
- physicskit/statphys/tests/test_sandpile.py +44 -0
- physicskit/statphys/tests/test_spin_glass.py +73 -0
- physicskit/statphys/tests/test_thermodynamics.py +121 -0
- physicskit/statphys/tests/test_xy_model.py +45 -0
- physicskit/statphys/utils/__init__.py +1 -0
- physicskit/statphys/utils/dynamics.py +100 -0
- physicskit/statphys/utils/finite_size_scaling.py +171 -0
- physicskit/statphys/utils/landau_theory.py +151 -0
- physicskit/statphys/utils/partition_function.py +109 -0
- physicskit/statphys/utils/thermodynamics.py +356 -0
- physicskit/statphys/visualizers/__init__.py +87 -0
- physicskit/statphys/visualizers/interactive.py +161 -0
- physicskit/statphys/visualizers/jarzynski_render.py +44 -0
- physicskit/statphys/visualizers/kpz_render.py +65 -0
- physicskit/statphys/visualizers/lattice_render.py +212 -0
- physicskit/statphys/visualizers/particle_render.py +121 -0
- physicskit/statphys/visualizers/random_walk_render.py +106 -0
- physicskit/statphys/visualizers/rg_render.py +45 -0
- physicskit/statphys/visualizers/sandpile_render.py +65 -0
- physicskit/statphys/visualizers/spin_glass_render.py +40 -0
- physicskit/statphys/visualizers/urn_render.py +38 -0
- physicskit/statphys/visualizers/vortex_render.py +84 -0
- physicskit/tests/__init__.py +0 -0
- physicskit/tests/test_constants.py +38 -0
- physicskit-0.1.0.dist-info/METADATA +196 -0
- physicskit-0.1.0.dist-info/RECORD +484 -0
- physicskit-0.1.0.dist-info/WHEEL +5 -0
- physicskit-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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])
|