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,1372 @@
|
|
|
1
|
+
"""Matplotlib animation helpers, trajectory color-coding, and interactive Plotly figures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Protocol, cast
|
|
6
|
+
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import numpy as np
|
|
9
|
+
import plotly.graph_objects as go
|
|
10
|
+
from matplotlib.animation import FuncAnimation
|
|
11
|
+
from matplotlib.axes import Axes
|
|
12
|
+
from matplotlib.collections import LineCollection
|
|
13
|
+
from matplotlib.figure import Figure
|
|
14
|
+
from numpy.typing import ArrayLike, NDArray
|
|
15
|
+
|
|
16
|
+
from physicskit.chaos.core.base_system import BilliardSystem, DiscreteMap
|
|
17
|
+
from physicskit.chaos.systems.continuous import DoublePendulum, DrivenPendulum, MagneticPendulum, RestrictedThreeBody, lagrange_points
|
|
18
|
+
from physicskit.chaos.systems.maps import BakersMap
|
|
19
|
+
from physicskit.chaos.visualizers import theme
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _TrajectorySystem(Protocol):
|
|
23
|
+
"""Structural type for a :class:`~physicskit.chaos.core.base_system.DynamicalSystem`
|
|
24
|
+
that also implements its own ``trajectory`` method (every concrete
|
|
25
|
+
continuous system in physicskit.chaos does, but the base class itself does not
|
|
26
|
+
declare it, so it isn't part of ``DynamicalSystem``'s static type)."""
|
|
27
|
+
|
|
28
|
+
dim: int
|
|
29
|
+
|
|
30
|
+
def trajectory(
|
|
31
|
+
self,
|
|
32
|
+
state0: NDArray[np.float64] | None = ...,
|
|
33
|
+
t0: float = ...,
|
|
34
|
+
dt: float = ...,
|
|
35
|
+
n_steps: int = ...,
|
|
36
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def animate_billiard_trajectory(
|
|
40
|
+
billiard: BilliardSystem,
|
|
41
|
+
pos: ArrayLike,
|
|
42
|
+
vel: ArrayLike,
|
|
43
|
+
n_bounces: int = 100,
|
|
44
|
+
interval: int = 50,
|
|
45
|
+
trail: int = 200,
|
|
46
|
+
) -> FuncAnimation:
|
|
47
|
+
"""Animate a trajectory bouncing inside a billiard's boundary.
|
|
48
|
+
|
|
49
|
+
Draws the real-space trajectory (left panel) alongside its
|
|
50
|
+
live-updating Poincare section ``(s, sin phi)`` (right panel).
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
billiard : BilliardSystem
|
|
55
|
+
Billiard to animate.
|
|
56
|
+
pos : array_like of float, shape (2,)
|
|
57
|
+
Initial position ``(x, y)``; must lie in the billiard's interior.
|
|
58
|
+
vel : array_like of float, shape (2,)
|
|
59
|
+
Initial velocity direction ``(vx, vy)``; normalized internally.
|
|
60
|
+
n_bounces : int, default 100
|
|
61
|
+
Number of reflections to trace and animate.
|
|
62
|
+
interval : int, default 50
|
|
63
|
+
Delay between animation frames, in milliseconds.
|
|
64
|
+
trail : int, default 200
|
|
65
|
+
Number of most-recent trajectory points to keep visible as a trail.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
matplotlib.animation.FuncAnimation
|
|
70
|
+
The animation object; assign it to a variable to keep it alive, and
|
|
71
|
+
display it with ``plt.show()`` or save it with ``anim.save(...)``.
|
|
72
|
+
"""
|
|
73
|
+
boundary = billiard.boundary_polyline()
|
|
74
|
+
path, result = billiard.trajectory_segments(pos, vel, n_bounces)
|
|
75
|
+
s, sin_phi = result["s"], result["sin_phi"]
|
|
76
|
+
|
|
77
|
+
fig, (ax_space, ax_poincare) = plt.subplots(1, 2, figsize=(12, 5.5))
|
|
78
|
+
|
|
79
|
+
ax_space.plot(boundary[:, 0], boundary[:, 1], color=theme.STRUCTURE, lw=1.5)
|
|
80
|
+
ax_space.set_aspect("equal")
|
|
81
|
+
ax_space.set_title(f"{billiard.__class__.__name__} trajectory")
|
|
82
|
+
(trail_line,) = ax_space.plot([], [], color=theme.ACCENT, lw=0.8)
|
|
83
|
+
(ball,) = ax_space.plot([], [], "o", color=theme.ACCENT, markersize=5)
|
|
84
|
+
|
|
85
|
+
ax_poincare.set_xlim(0.0, billiard.perimeter())
|
|
86
|
+
ax_poincare.set_ylim(-1.0, 1.0)
|
|
87
|
+
ax_poincare.set_xlabel("s")
|
|
88
|
+
ax_poincare.set_ylabel(r"$\sin\varphi$")
|
|
89
|
+
ax_poincare.set_title("Poincare section (live)")
|
|
90
|
+
poincare_scatter = ax_poincare.scatter([], [], s=6, color=theme.PRIMARY)
|
|
91
|
+
|
|
92
|
+
def init() -> tuple:
|
|
93
|
+
trail_line.set_data([], [])
|
|
94
|
+
ball.set_data([], [])
|
|
95
|
+
poincare_scatter.set_offsets(np.empty((0, 2)))
|
|
96
|
+
return trail_line, ball, poincare_scatter
|
|
97
|
+
|
|
98
|
+
def update(frame: int) -> tuple:
|
|
99
|
+
lo = max(0, frame - trail)
|
|
100
|
+
trail_line.set_data(path[lo : frame + 1, 0], path[lo : frame + 1, 1])
|
|
101
|
+
ball.set_data([path[frame, 0]], [path[frame, 1]])
|
|
102
|
+
n_hits = max(0, frame)
|
|
103
|
+
poincare_scatter.set_offsets(np.column_stack([s[:n_hits], sin_phi[:n_hits]]))
|
|
104
|
+
return trail_line, ball, poincare_scatter
|
|
105
|
+
|
|
106
|
+
anim = FuncAnimation(fig, update, frames=path.shape[0], init_func=init, interval=interval, blit=False)
|
|
107
|
+
return anim
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def animate_billiard_divergence(
|
|
111
|
+
billiard: BilliardSystem,
|
|
112
|
+
pos: ArrayLike,
|
|
113
|
+
vel: ArrayLike,
|
|
114
|
+
delta_0: float = 1e-8,
|
|
115
|
+
n_bounces: int = 100,
|
|
116
|
+
interval: int = 50,
|
|
117
|
+
trail: int = 200,
|
|
118
|
+
) -> FuncAnimation:
|
|
119
|
+
"""Animate two initially-nearby billiard rays bouncing side by side, diverging.
|
|
120
|
+
|
|
121
|
+
Both rays start at the same position and are launched at angles
|
|
122
|
+
`delta_0` radians apart; watch them bounce together, indistinguishably,
|
|
123
|
+
until the perturbation is amplified enough (a single bounce off a
|
|
124
|
+
defocusing or truncated wall can be all it takes) to send them down
|
|
125
|
+
completely different paths -- sensitive dependence on initial
|
|
126
|
+
conditions, seen directly rather than only in a divergence plot.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
billiard : BilliardSystem
|
|
131
|
+
Billiard to animate both rays within.
|
|
132
|
+
pos : array_like of float, shape (2,)
|
|
133
|
+
Shared initial position ``(x, y)``; must lie in the billiard's
|
|
134
|
+
interior.
|
|
135
|
+
vel : array_like of float, shape (2,)
|
|
136
|
+
Initial velocity direction ``(vx, vy)`` of the reference ray;
|
|
137
|
+
normalized internally.
|
|
138
|
+
delta_0 : float, default 1e-8
|
|
139
|
+
Initial angular perturbation (radians) applied to `vel` to launch
|
|
140
|
+
the second ray.
|
|
141
|
+
n_bounces : int, default 100
|
|
142
|
+
Number of reflections to trace and animate for each ray.
|
|
143
|
+
interval : int, default 50
|
|
144
|
+
Delay between animation frames, in milliseconds.
|
|
145
|
+
trail : int, default 200
|
|
146
|
+
Number of most-recent trajectory points to keep visible as a trail,
|
|
147
|
+
for each ray.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
matplotlib.animation.FuncAnimation
|
|
152
|
+
The animation object; assign it to a variable to keep it alive, and
|
|
153
|
+
display it with ``plt.show()`` or save it with ``anim.save(...)``.
|
|
154
|
+
"""
|
|
155
|
+
vel_ref = np.asarray(vel, dtype=np.float64)
|
|
156
|
+
vel_ref = vel_ref / np.linalg.norm(vel_ref)
|
|
157
|
+
angle = float(np.arctan2(vel_ref[1], vel_ref[0])) + delta_0
|
|
158
|
+
vel_perturbed = np.array([np.cos(angle), np.sin(angle)])
|
|
159
|
+
|
|
160
|
+
boundary = billiard.boundary_polyline()
|
|
161
|
+
path_ref, _ = billiard.trajectory_segments(pos, vel_ref, n_bounces)
|
|
162
|
+
path_pert, _ = billiard.trajectory_segments(pos, vel_perturbed, n_bounces)
|
|
163
|
+
|
|
164
|
+
fig, ax = plt.subplots(figsize=(6.5, 6.5))
|
|
165
|
+
ax.plot(boundary[:, 0], boundary[:, 1], color=theme.STRUCTURE, lw=1.5)
|
|
166
|
+
ax.set_aspect("equal")
|
|
167
|
+
ax.set_title(f"{billiard.__class__.__name__}: two rays {delta_0:.0e} rad apart")
|
|
168
|
+
|
|
169
|
+
(trail_ref,) = ax.plot([], [], color=theme.PRIMARY, lw=0.8)
|
|
170
|
+
(ball_ref,) = ax.plot([], [], "o", color=theme.PRIMARY, markersize=5, label="reference")
|
|
171
|
+
(trail_pert,) = ax.plot([], [], color=theme.ACCENT, lw=0.8)
|
|
172
|
+
(ball_pert,) = ax.plot([], [], "o", color=theme.ACCENT, markersize=5, label="perturbed")
|
|
173
|
+
ax.legend(loc="upper right")
|
|
174
|
+
|
|
175
|
+
def init() -> tuple:
|
|
176
|
+
for artist in (trail_ref, ball_ref, trail_pert, ball_pert):
|
|
177
|
+
artist.set_data([], [])
|
|
178
|
+
return trail_ref, ball_ref, trail_pert, ball_pert
|
|
179
|
+
|
|
180
|
+
def update(frame: int) -> tuple:
|
|
181
|
+
lo = max(0, frame - trail)
|
|
182
|
+
trail_ref.set_data(path_ref[lo : frame + 1, 0], path_ref[lo : frame + 1, 1])
|
|
183
|
+
ball_ref.set_data([path_ref[frame, 0]], [path_ref[frame, 1]])
|
|
184
|
+
trail_pert.set_data(path_pert[lo : frame + 1, 0], path_pert[lo : frame + 1, 1])
|
|
185
|
+
ball_pert.set_data([path_pert[frame, 0]], [path_pert[frame, 1]])
|
|
186
|
+
return trail_ref, ball_ref, trail_pert, ball_pert
|
|
187
|
+
|
|
188
|
+
anim = FuncAnimation(fig, update, frames=path_ref.shape[0], init_func=init, interval=interval, blit=False)
|
|
189
|
+
return anim
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def plotly_billiard_trajectory(billiard: BilliardSystem, pos: ArrayLike, vel: ArrayLike, n_bounces: int = 100) -> go.Figure:
|
|
193
|
+
"""Build an interactive Plotly figure of a trajectory inside a billiard.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
billiard : BilliardSystem
|
|
198
|
+
Billiard to trace the trajectory within.
|
|
199
|
+
pos : array_like of float, shape (2,)
|
|
200
|
+
Initial position ``(x, y)``; must lie in the billiard's interior.
|
|
201
|
+
vel : array_like of float, shape (2,)
|
|
202
|
+
Initial velocity direction ``(vx, vy)``; normalized internally.
|
|
203
|
+
n_bounces : int, default 100
|
|
204
|
+
Number of reflections to trace.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
plotly.graph_objects.Figure
|
|
209
|
+
Interactive figure with the boundary and trajectory as line traces.
|
|
210
|
+
"""
|
|
211
|
+
boundary = billiard.boundary_polyline()
|
|
212
|
+
path, _ = billiard.trajectory_segments(pos, vel, n_bounces)
|
|
213
|
+
|
|
214
|
+
fig = go.Figure()
|
|
215
|
+
fig.add_trace(
|
|
216
|
+
go.Scatter(
|
|
217
|
+
x=boundary[:, 0],
|
|
218
|
+
y=boundary[:, 1],
|
|
219
|
+
mode="lines",
|
|
220
|
+
line={"color": theme.STRUCTURE, "width": 2},
|
|
221
|
+
name="boundary",
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
fig.add_trace(
|
|
225
|
+
go.Scatter(
|
|
226
|
+
x=path[:, 0],
|
|
227
|
+
y=path[:, 1],
|
|
228
|
+
mode="lines",
|
|
229
|
+
line={"color": theme.ACCENT, "width": 1},
|
|
230
|
+
name="trajectory",
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
fig.update_yaxes(scaleanchor="x", scaleratio=1)
|
|
234
|
+
fig.update_layout(
|
|
235
|
+
title=f"{billiard.__class__.__name__} trajectory",
|
|
236
|
+
xaxis_title="x",
|
|
237
|
+
yaxis_title="y",
|
|
238
|
+
)
|
|
239
|
+
return fig
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def plotly_poincare_section(billiard: BilliardSystem, n_rays: int = 50, n_bounces: int = 200, seed: int | None = None) -> go.Figure:
|
|
243
|
+
"""Build an interactive Plotly scatter of a billiard's boundary phase space.
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
billiard : BilliardSystem
|
|
248
|
+
Billiard to sample.
|
|
249
|
+
n_rays : int, default 50
|
|
250
|
+
Number of independent trajectories to launch.
|
|
251
|
+
n_bounces : int, default 200
|
|
252
|
+
Number of reflections to trace per trajectory.
|
|
253
|
+
seed : int, optional
|
|
254
|
+
Seed for the random angle generator, for reproducibility.
|
|
255
|
+
|
|
256
|
+
Returns
|
|
257
|
+
-------
|
|
258
|
+
plotly.graph_objects.Figure
|
|
259
|
+
Interactive scatter figure of ``(s, sin phi)`` boundary hits.
|
|
260
|
+
"""
|
|
261
|
+
rng = np.random.default_rng(seed)
|
|
262
|
+
interior = billiard.sample_interior_point()
|
|
263
|
+
angles = rng.uniform(0.0, 2.0 * np.pi, size=n_rays)
|
|
264
|
+
result = billiard.simulate_many_rays(interior, angles, n_bounces)
|
|
265
|
+
s, sin_phi = result["s"], result["sin_phi"]
|
|
266
|
+
|
|
267
|
+
fig = go.Figure(
|
|
268
|
+
go.Scattergl(
|
|
269
|
+
x=s,
|
|
270
|
+
y=sin_phi,
|
|
271
|
+
mode="markers",
|
|
272
|
+
marker={"size": 3, "color": theme.PRIMARY, "opacity": 0.6},
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
fig.update_layout(
|
|
276
|
+
title=f"{billiard.__class__.__name__} Poincare section",
|
|
277
|
+
xaxis_title="s (boundary arclength)",
|
|
278
|
+
yaxis_title="sin(phi)",
|
|
279
|
+
yaxis_range=[-1.0, 1.0],
|
|
280
|
+
xaxis_range=[0.0, billiard.perimeter()],
|
|
281
|
+
)
|
|
282
|
+
return fig
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def plot_colored_trajectory(
|
|
286
|
+
x: ArrayLike,
|
|
287
|
+
y: ArrayLike,
|
|
288
|
+
values: ArrayLike,
|
|
289
|
+
ax: Axes | None = None,
|
|
290
|
+
cmap: str = theme.SEQUENTIAL_CMAP,
|
|
291
|
+
colorbar: bool = True,
|
|
292
|
+
colorbar_label: str = "",
|
|
293
|
+
**line_kwargs: Any,
|
|
294
|
+
) -> tuple[Figure, Axes]:
|
|
295
|
+
"""Plot a 2D path colored segment-by-segment by an arbitrary scalar.
|
|
296
|
+
|
|
297
|
+
Useful for highlighting where a trajectory is "interesting" -- e.g. a
|
|
298
|
+
billiard trajectory colored by bounce index (to see how quickly it
|
|
299
|
+
explores the table), or a continuous trajectory colored by local speed or
|
|
300
|
+
an instantaneous divergence-rate estimate.
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
x, y : array_like of float, shape (n,)
|
|
305
|
+
Path coordinates.
|
|
306
|
+
values : array_like of float, shape (n,)
|
|
307
|
+
Scalar value at each point, used to color the path; each line
|
|
308
|
+
segment between consecutive points is colored by the value at its
|
|
309
|
+
starting point.
|
|
310
|
+
ax : matplotlib.axes.Axes, optional
|
|
311
|
+
Axes to draw into; a new figure and axes are created if omitted.
|
|
312
|
+
cmap : str, default "viridis"
|
|
313
|
+
Colormap name.
|
|
314
|
+
colorbar : bool, default True
|
|
315
|
+
Whether to add a colorbar.
|
|
316
|
+
colorbar_label : str, default ""
|
|
317
|
+
Label for the colorbar.
|
|
318
|
+
**line_kwargs
|
|
319
|
+
Additional keyword arguments forwarded to
|
|
320
|
+
``matplotlib.collections.LineCollection`` (e.g. ``linewidth``).
|
|
321
|
+
|
|
322
|
+
Returns
|
|
323
|
+
-------
|
|
324
|
+
fig : matplotlib.figure.Figure
|
|
325
|
+
ax : matplotlib.axes.Axes
|
|
326
|
+
"""
|
|
327
|
+
x = np.asarray(x, dtype=np.float64)
|
|
328
|
+
y = np.asarray(y, dtype=np.float64)
|
|
329
|
+
values = np.asarray(values, dtype=np.float64)
|
|
330
|
+
|
|
331
|
+
points = np.column_stack([x, y]).reshape(-1, 1, 2)
|
|
332
|
+
segments = np.concatenate([points[:-1], points[1:]], axis=1)
|
|
333
|
+
|
|
334
|
+
if ax is None:
|
|
335
|
+
fig, ax = plt.subplots(figsize=(7, 6))
|
|
336
|
+
else:
|
|
337
|
+
fig = cast(Figure, ax.figure)
|
|
338
|
+
|
|
339
|
+
kwargs: dict[str, Any] = {"linewidth": 1.5}
|
|
340
|
+
kwargs.update(line_kwargs)
|
|
341
|
+
line_collection = LineCollection(cast(Any, segments), cmap=cmap, **kwargs)
|
|
342
|
+
line_collection.set_array(values[:-1])
|
|
343
|
+
ax.add_collection(line_collection)
|
|
344
|
+
ax.set_xlim(x.min(), x.max())
|
|
345
|
+
ax.set_ylim(y.min(), y.max())
|
|
346
|
+
|
|
347
|
+
if colorbar:
|
|
348
|
+
fig.colorbar(line_collection, ax=ax, label=colorbar_label)
|
|
349
|
+
|
|
350
|
+
return fig, ax
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def plotly_3d_trajectory(
|
|
354
|
+
states: ArrayLike,
|
|
355
|
+
labels: tuple[str, str, str] = ("x", "y", "z"),
|
|
356
|
+
color_by: ArrayLike | None = None,
|
|
357
|
+
colorbar_label: str = "",
|
|
358
|
+
title: str | None = None,
|
|
359
|
+
) -> go.Figure:
|
|
360
|
+
"""Build an interactive, orbit/pan/zoom-able 3D Plotly trajectory figure.
|
|
361
|
+
|
|
362
|
+
Parameters
|
|
363
|
+
----------
|
|
364
|
+
states : array_like of float, shape (n, 3)
|
|
365
|
+
3D trajectory points (e.g. Lorenz/Rossler/Chua state history).
|
|
366
|
+
labels : tuple of str, default ("x", "y", "z")
|
|
367
|
+
Axis labels.
|
|
368
|
+
color_by : array_like of float, shape (n,), optional
|
|
369
|
+
Scalar value at each point used to color the trajectory (e.g. local
|
|
370
|
+
speed, or time); a single solid color is used if omitted.
|
|
371
|
+
colorbar_label : str, default ""
|
|
372
|
+
Label for the colorbar, if `color_by` is given.
|
|
373
|
+
title : str, optional
|
|
374
|
+
Figure title.
|
|
375
|
+
|
|
376
|
+
Returns
|
|
377
|
+
-------
|
|
378
|
+
plotly.graph_objects.Figure
|
|
379
|
+
"""
|
|
380
|
+
states = np.asarray(states, dtype=np.float64)
|
|
381
|
+
|
|
382
|
+
if color_by is None:
|
|
383
|
+
line = {"color": theme.PRIMARY, "width": 2}
|
|
384
|
+
else:
|
|
385
|
+
line = {
|
|
386
|
+
"color": np.asarray(color_by, dtype=np.float64),
|
|
387
|
+
"width": 3,
|
|
388
|
+
"colorscale": "Viridis",
|
|
389
|
+
"colorbar": {"title": colorbar_label},
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
fig = go.Figure(
|
|
393
|
+
go.Scatter3d(
|
|
394
|
+
x=states[:, 0],
|
|
395
|
+
y=states[:, 1],
|
|
396
|
+
z=states[:, 2],
|
|
397
|
+
mode="lines",
|
|
398
|
+
line=line,
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
fig.update_layout(
|
|
402
|
+
title=title,
|
|
403
|
+
scene={
|
|
404
|
+
"xaxis_title": labels[0],
|
|
405
|
+
"yaxis_title": labels[1],
|
|
406
|
+
"zaxis_title": labels[2],
|
|
407
|
+
},
|
|
408
|
+
)
|
|
409
|
+
return fig
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def animate_bakers_map(
|
|
413
|
+
system: BakersMap,
|
|
414
|
+
n_points: int = 10000,
|
|
415
|
+
n_iterations: int = 20,
|
|
416
|
+
frames_per_iteration: int = 15,
|
|
417
|
+
interval: int = 60,
|
|
418
|
+
stripe_colors: tuple[str, str] = (theme.PRIMARY, theme.ACCENT),
|
|
419
|
+
seed: int | None = 0,
|
|
420
|
+
) -> FuncAnimation:
|
|
421
|
+
"""Animate the baker's map's "stretch, cut, and stack" action, in three
|
|
422
|
+
explicit phases per iteration.
|
|
423
|
+
|
|
424
|
+
Starts from a regular grid of square markers, colored in exactly two
|
|
425
|
+
vertical bands (left half / right half) -- which, for the default
|
|
426
|
+
``alpha = 0.5``, lines up with the map's own cut point, so the first
|
|
427
|
+
cut slices exactly along the original color boundary. Each iteration is
|
|
428
|
+
then animated as three distinct, visually separated phases rather than
|
|
429
|
+
one smooth blend, so the mechanism itself is legible:
|
|
430
|
+
|
|
431
|
+
1. **Stretch** -- the square is squashed vertically by a factor
|
|
432
|
+
``alpha`` and stretched horizontally onto ``[0, 2)``: the left branch
|
|
433
|
+
(``x < alpha``) stretches onto ``[0, 1)`` in place, and the right
|
|
434
|
+
branch (``x >= alpha``) stretches onto its own ``[0, 1)`` slot, shown
|
|
435
|
+
offset to ``[1, 2)`` so the two pieces don't overlap mid-flight.
|
|
436
|
+
2. **Cut** -- a brief pause with a dashed guide line at ``x = 1``
|
|
437
|
+
marking where the stretched strip is about to be sliced in two.
|
|
438
|
+
3. **Stack** -- the right-hand piece slides left by 1 and up onto
|
|
439
|
+
``y in [alpha, 1)``, landing exactly on top of the left piece, which
|
|
440
|
+
has not moved since the stretch phase. The square is whole again, now
|
|
441
|
+
twice as finely interleaved.
|
|
442
|
+
|
|
443
|
+
Because every marker keeps its original color, the two bands are seen
|
|
444
|
+
getting sliced and restacked into progressively thinner, more numerous
|
|
445
|
+
stripes -- and because the whole grid is present from frame 0, the
|
|
446
|
+
increasing fineness of that interleaving (the mixing) is directly
|
|
447
|
+
visible, iteration after iteration.
|
|
448
|
+
|
|
449
|
+
Parameters
|
|
450
|
+
----------
|
|
451
|
+
system : BakersMap
|
|
452
|
+
Map to animate.
|
|
453
|
+
n_points : int, default 10000
|
|
454
|
+
Approximate number of square markers: laid out on a regular
|
|
455
|
+
``round(sqrt(n_points))``-by-``round(sqrt(n_points))`` grid so they
|
|
456
|
+
tile the unit square with no gaps at the start.
|
|
457
|
+
n_iterations : int, default 20
|
|
458
|
+
Number of map iterations to animate through.
|
|
459
|
+
frames_per_iteration : int, default 15
|
|
460
|
+
Number of frames used to animate each iteration's stretch, cut, and
|
|
461
|
+
stack phases combined.
|
|
462
|
+
interval : int, default 60
|
|
463
|
+
Delay between animation frames, in milliseconds.
|
|
464
|
+
stripe_colors : tuple of str, default (theme.PRIMARY, theme.ACCENT)
|
|
465
|
+
Colors for the left half and right half of the initial grid,
|
|
466
|
+
respectively.
|
|
467
|
+
seed : int, optional
|
|
468
|
+
Seed for a small random jitter applied to the grid positions. A
|
|
469
|
+
perfectly regular grid sits at exact dyadic rationals, which this
|
|
470
|
+
map's doubling action on the ``x`` coordinate degenerates in very
|
|
471
|
+
few iterations (the same finite-precision collapse documented on
|
|
472
|
+
:class:`~physicskit.chaos.systems.maps.BakersMap`); a tiny jitter, much
|
|
473
|
+
smaller than the grid spacing, avoids that without visibly
|
|
474
|
+
disturbing the tiling.
|
|
475
|
+
|
|
476
|
+
Returns
|
|
477
|
+
-------
|
|
478
|
+
matplotlib.animation.FuncAnimation
|
|
479
|
+
Assign it to a variable to keep it alive, and display it with
|
|
480
|
+
``plt.show()`` or save it with ``anim.save(...)``.
|
|
481
|
+
"""
|
|
482
|
+
alpha = system.alpha
|
|
483
|
+
n_side = max(2, round(np.sqrt(n_points)))
|
|
484
|
+
centers = (np.arange(n_side) + 0.5) / n_side
|
|
485
|
+
grid_x, grid_y = np.meshgrid(centers, centers)
|
|
486
|
+
rng = np.random.default_rng(seed)
|
|
487
|
+
jitter = rng.uniform(-0.15, 0.15, size=(2,) + grid_x.shape) / n_side
|
|
488
|
+
x0 = np.clip(grid_x + jitter[0], 0.0, 1.0 - 1e-12).ravel()
|
|
489
|
+
y0 = np.clip(grid_y + jitter[1], 0.0, 1.0 - 1e-12).ravel()
|
|
490
|
+
|
|
491
|
+
colors = np.where(x0 < 0.5, stripe_colors[0], stripe_colors[1])
|
|
492
|
+
|
|
493
|
+
fig, ax = plt.subplots(figsize=(8.0, 5.0))
|
|
494
|
+
ax.set_xlim(-0.05, 2.05)
|
|
495
|
+
ax.set_ylim(-0.05, 1.05)
|
|
496
|
+
ax.set_aspect("equal")
|
|
497
|
+
ax.set_xlabel("x")
|
|
498
|
+
ax.set_ylabel("y")
|
|
499
|
+
ax.add_patch(plt.Rectangle((0.0, 0.0), 1.0, 1.0, fill=False, edgecolor=theme.STRUCTURE, lw=1.2, zorder=3))
|
|
500
|
+
cut_line = ax.axvline(1.0, color=theme.STRUCTURE, ls="--", lw=1.2, zorder=3, visible=False)
|
|
501
|
+
title = ax.set_title("Baker's map: stretch (iteration 0 -> 1)")
|
|
502
|
+
|
|
503
|
+
# Marker size is chosen once, from the (fixed) data-to-pixel scale, so
|
|
504
|
+
# that markers exactly tile the resting unit square without gaps or
|
|
505
|
+
# overlaps -- `scatter`'s `s` is in fixed points^2, not data units, so
|
|
506
|
+
# this only works because the axis limits never change during the
|
|
507
|
+
# animation (a zooming camera would desync marker size from grid spacing).
|
|
508
|
+
fig.canvas.draw()
|
|
509
|
+
p0 = ax.transData.transform((0.0, 0.0))
|
|
510
|
+
p1 = ax.transData.transform((1.0 / n_side, 0.0))
|
|
511
|
+
marker_points = (p1[0] - p0[0]) * 72.0 / fig.dpi
|
|
512
|
+
scatter = ax.scatter(x0, y0, s=marker_points**2, c=colors, marker="s", linewidths=0, zorder=2)
|
|
513
|
+
|
|
514
|
+
# Frame budget within each iteration: a stretch phase, a single-frame
|
|
515
|
+
# pause on the cut, and a stack phase.
|
|
516
|
+
n_stretch = max(1, round(0.45 * frames_per_iteration))
|
|
517
|
+
n_pause = 1 if frames_per_iteration >= 3 else 0
|
|
518
|
+
n_stack = max(1, frames_per_iteration - n_stretch - n_pause)
|
|
519
|
+
frames_per_iteration = n_stretch + n_pause + n_stack
|
|
520
|
+
total_frames = n_iterations * frames_per_iteration
|
|
521
|
+
|
|
522
|
+
# `state` holds the grid's *resting* (post-stack) positions, carried over
|
|
523
|
+
# between iterations; the phase-1 and final targets for the iteration in
|
|
524
|
+
# progress are derived from it once, at that iteration's first sub-frame,
|
|
525
|
+
# and held fixed while sub-frames interpolate between them.
|
|
526
|
+
state: dict[str, Any] = {"x": x0.copy(), "y": y0.copy()}
|
|
527
|
+
|
|
528
|
+
def update(frame: int) -> tuple:
|
|
529
|
+
it, sub = divmod(frame, frames_per_iteration)
|
|
530
|
+
if sub == 0:
|
|
531
|
+
rx, ry = state["x"], state["y"]
|
|
532
|
+
left = rx < alpha
|
|
533
|
+
# Stretch target: left branch lands on its true final position
|
|
534
|
+
# directly; right branch stretches onto its own [0, 1) slot,
|
|
535
|
+
# offset to [1, 2) so it doesn't overlap the left branch.
|
|
536
|
+
x_stretch = np.where(left, rx / alpha, (rx - alpha) / (1.0 - alpha) + 1.0)
|
|
537
|
+
y_stretch = alpha * ry
|
|
538
|
+
x_final = np.where(left, rx / alpha, (rx - alpha) / (1.0 - alpha))
|
|
539
|
+
y_final = np.where(left, alpha * ry, alpha + (1.0 - alpha) * ry)
|
|
540
|
+
state["left"] = left
|
|
541
|
+
state["x_stretch"], state["y_stretch"] = x_stretch, y_stretch
|
|
542
|
+
state["x_final"], state["y_final"] = x_final, y_final
|
|
543
|
+
state["x_rest"], state["y_rest"] = rx, ry
|
|
544
|
+
|
|
545
|
+
left = state["left"]
|
|
546
|
+
rx, ry = state["x_rest"], state["y_rest"]
|
|
547
|
+
x_stretch, y_stretch = state["x_stretch"], state["y_stretch"]
|
|
548
|
+
x_final, y_final = state["x_final"], state["y_final"]
|
|
549
|
+
|
|
550
|
+
if sub < n_stretch:
|
|
551
|
+
t = (sub + 1) / n_stretch
|
|
552
|
+
x = rx + t * (x_stretch - rx)
|
|
553
|
+
y = ry + t * (y_stretch - ry)
|
|
554
|
+
cut_line.set_visible(True)
|
|
555
|
+
phase = "stretch"
|
|
556
|
+
elif sub < n_stretch + n_pause:
|
|
557
|
+
x, y = x_stretch, y_stretch
|
|
558
|
+
cut_line.set_visible(True)
|
|
559
|
+
phase = "cut"
|
|
560
|
+
else:
|
|
561
|
+
s = sub - n_stretch - n_pause
|
|
562
|
+
t = (s + 1) / n_stack
|
|
563
|
+
x = np.where(left, x_final, x_stretch + t * (x_final - x_stretch))
|
|
564
|
+
y = np.where(left, y_final, y_stretch + t * (y_final - y_stretch))
|
|
565
|
+
cut_line.set_visible(False)
|
|
566
|
+
phase = "stack"
|
|
567
|
+
|
|
568
|
+
scatter.set_offsets(np.column_stack([x, y]))
|
|
569
|
+
title.set_text(f"Baker's map: {phase} (iteration {it} -> {it + 1})")
|
|
570
|
+
if sub == frames_per_iteration - 1:
|
|
571
|
+
state["x"], state["y"] = x_final, y_final
|
|
572
|
+
return scatter, title, cut_line
|
|
573
|
+
|
|
574
|
+
anim = FuncAnimation(fig, update, frames=total_frames, interval=interval, blit=False)
|
|
575
|
+
return anim
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def animate_map_orbit(
|
|
579
|
+
system: DiscreteMap,
|
|
580
|
+
state0: ArrayLike | None = None,
|
|
581
|
+
n_iter: int = 4000,
|
|
582
|
+
points_per_frame: int = 8,
|
|
583
|
+
interval: int = 40,
|
|
584
|
+
color: str = theme.PRIMARY,
|
|
585
|
+
marker_size: float = 3.0,
|
|
586
|
+
labels: tuple[str, str] = ("x", "y"),
|
|
587
|
+
title: str | None = None,
|
|
588
|
+
) -> FuncAnimation:
|
|
589
|
+
"""Animate a 2D map's orbit being built up, iteration by iteration.
|
|
590
|
+
|
|
591
|
+
Useful for watching a strange attractor (e.g. the Henon map's) emerge
|
|
592
|
+
from a single seed point, rather than seeing only the finished scatter.
|
|
593
|
+
|
|
594
|
+
Parameters
|
|
595
|
+
----------
|
|
596
|
+
system : DiscreteMap
|
|
597
|
+
Map to iterate; must have ``dim == 2``.
|
|
598
|
+
state0 : array_like of float, shape (2,), optional
|
|
599
|
+
Initial state; defaults to ``system.initial_state()``.
|
|
600
|
+
n_iter : int, default 4000
|
|
601
|
+
Total number of iterations to animate.
|
|
602
|
+
points_per_frame : int, default 8
|
|
603
|
+
Number of new orbit points revealed per animation frame.
|
|
604
|
+
interval : int, default 40
|
|
605
|
+
Delay between animation frames, in milliseconds.
|
|
606
|
+
color : str, default theme.PRIMARY
|
|
607
|
+
Marker color.
|
|
608
|
+
marker_size : float, default 3.0
|
|
609
|
+
Scatter marker size.
|
|
610
|
+
labels : tuple of str, default ("x", "y")
|
|
611
|
+
Axis labels.
|
|
612
|
+
title : str, optional
|
|
613
|
+
Axes title; defaults to ``"<ClassName> orbit"``.
|
|
614
|
+
|
|
615
|
+
Returns
|
|
616
|
+
-------
|
|
617
|
+
matplotlib.animation.FuncAnimation
|
|
618
|
+
"""
|
|
619
|
+
state0 = system.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
|
|
620
|
+
traj = system.trajectory(state0, n_iter=n_iter)
|
|
621
|
+
|
|
622
|
+
fig, ax = plt.subplots(figsize=(7, 6))
|
|
623
|
+
pad_x = 0.05 * (traj[:, 0].max() - traj[:, 0].min() + 1e-12)
|
|
624
|
+
pad_y = 0.05 * (traj[:, 1].max() - traj[:, 1].min() + 1e-12)
|
|
625
|
+
ax.set_xlim(traj[:, 0].min() - pad_x, traj[:, 0].max() + pad_x)
|
|
626
|
+
ax.set_ylim(traj[:, 1].min() - pad_y, traj[:, 1].max() + pad_y)
|
|
627
|
+
ax.set_xlabel(labels[0])
|
|
628
|
+
ax.set_ylabel(labels[1])
|
|
629
|
+
ax.set_title(title or f"{system.__class__.__name__} orbit")
|
|
630
|
+
|
|
631
|
+
scatter = ax.scatter([], [], s=marker_size, color=color, alpha=0.6)
|
|
632
|
+
|
|
633
|
+
n_frames = int(np.ceil(traj.shape[0] / points_per_frame))
|
|
634
|
+
|
|
635
|
+
def update(frame: int) -> tuple:
|
|
636
|
+
end = min(traj.shape[0], (frame + 1) * points_per_frame)
|
|
637
|
+
scatter.set_offsets(traj[:end])
|
|
638
|
+
return (scatter,)
|
|
639
|
+
|
|
640
|
+
anim = FuncAnimation(fig, update, frames=n_frames, interval=interval, blit=False)
|
|
641
|
+
return anim
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def animate_multi_orbit_map(
|
|
645
|
+
trajectories: list[ArrayLike],
|
|
646
|
+
interval: int = 60,
|
|
647
|
+
colors: list[Any] | None = None,
|
|
648
|
+
xlim: tuple[float, float] | None = None,
|
|
649
|
+
ylim: tuple[float, float] | None = None,
|
|
650
|
+
labels: tuple[str, str] = ("x", "y"),
|
|
651
|
+
title: str | None = None,
|
|
652
|
+
marker_size: float = 2.0,
|
|
653
|
+
n_frames: int = 100,
|
|
654
|
+
) -> FuncAnimation:
|
|
655
|
+
"""Animate a family of 2D orbits growing together, iteration by iteration.
|
|
656
|
+
|
|
657
|
+
Useful for a phase-space portrait built from many initial conditions
|
|
658
|
+
(e.g. the standard map's islands-in-a-chaotic-sea picture): every orbit
|
|
659
|
+
grows one new point per frame (all together, not one whole orbit at a
|
|
660
|
+
time), so the picture keeps visibly filling in throughout the animation
|
|
661
|
+
rather than mostly settling within the first few frames.
|
|
662
|
+
|
|
663
|
+
Parameters
|
|
664
|
+
----------
|
|
665
|
+
trajectories : list of array_like of float, each shape (n_i, 2)
|
|
666
|
+
Orbits to grow together; need not all have the same length (each
|
|
667
|
+
stops advancing once it runs out of points).
|
|
668
|
+
interval : int, default 60
|
|
669
|
+
Delay between animation frames, in milliseconds.
|
|
670
|
+
colors : list, optional
|
|
671
|
+
Per-orbit colors; a `theme.SEQUENTIAL_CMAP` sweep is used if omitted.
|
|
672
|
+
xlim, ylim : tuple of float, optional
|
|
673
|
+
Axis limits; inferred from the data if omitted.
|
|
674
|
+
labels : tuple of str, default ("x", "y")
|
|
675
|
+
Axis labels.
|
|
676
|
+
title : str, optional
|
|
677
|
+
Axes title, updated each frame with the running iteration count.
|
|
678
|
+
marker_size : float, default 2.0
|
|
679
|
+
Marker size for each orbit's points (uses a real, visibly-sized
|
|
680
|
+
marker, unlike Matplotlib's 1-device-pixel ``","`` marker which
|
|
681
|
+
ignores this parameter entirely).
|
|
682
|
+
n_frames : int, default 100
|
|
683
|
+
Number of animation frames spanning every orbit's full length; each
|
|
684
|
+
frame reveals ``1 / n_frames`` of the points, evenly paced so growth
|
|
685
|
+
stays visible for the whole animation instead of being front-loaded.
|
|
686
|
+
|
|
687
|
+
Returns
|
|
688
|
+
-------
|
|
689
|
+
matplotlib.animation.FuncAnimation
|
|
690
|
+
"""
|
|
691
|
+
traj_arrays: list[NDArray[np.float64]] = [np.asarray(t, dtype=np.float64) for t in trajectories]
|
|
692
|
+
n_orbits = len(traj_arrays)
|
|
693
|
+
max_len = max(t.shape[0] for t in traj_arrays)
|
|
694
|
+
|
|
695
|
+
fig, ax = plt.subplots(figsize=(7, 6.5))
|
|
696
|
+
if xlim is None or ylim is None:
|
|
697
|
+
all_pts = np.concatenate(traj_arrays, axis=0)
|
|
698
|
+
xlim = xlim or (float(all_pts[:, 0].min()), float(all_pts[:, 0].max()))
|
|
699
|
+
ylim = ylim or (float(all_pts[:, 1].min()), float(all_pts[:, 1].max()))
|
|
700
|
+
ax.set_xlim(*xlim)
|
|
701
|
+
ax.set_ylim(*ylim)
|
|
702
|
+
ax.set_xlabel(labels[0])
|
|
703
|
+
ax.set_ylabel(labels[1])
|
|
704
|
+
|
|
705
|
+
cmap = plt.get_cmap(theme.SEQUENTIAL_CMAP)
|
|
706
|
+
orbit_colors = [cmap(i / max(1, n_orbits - 1)) for i in range(n_orbits)] if colors is None else list(colors)
|
|
707
|
+
|
|
708
|
+
lines = [ax.plot([], [], ".", color=orbit_colors[i], alpha=0.7, markersize=marker_size, lw=0)[0] for i in range(n_orbits)]
|
|
709
|
+
|
|
710
|
+
frame_ends = np.linspace(1, max_len, n_frames).astype(int)
|
|
711
|
+
|
|
712
|
+
def update(frame: int) -> tuple:
|
|
713
|
+
end = int(frame_ends[frame])
|
|
714
|
+
for line, traj in zip(lines, traj_arrays):
|
|
715
|
+
stop = min(end, traj.shape[0])
|
|
716
|
+
line.set_data(traj[:stop, 0], traj[:stop, 1])
|
|
717
|
+
if title:
|
|
718
|
+
ax.set_title(f"{title} (iteration {end}/{max_len})")
|
|
719
|
+
return tuple(lines)
|
|
720
|
+
|
|
721
|
+
anim = FuncAnimation(fig, update, frames=n_frames, interval=interval, blit=False)
|
|
722
|
+
return anim
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def animate_map_cobweb(
|
|
726
|
+
system: DiscreteMap,
|
|
727
|
+
x0: float | None = None,
|
|
728
|
+
n_iter: int = 30,
|
|
729
|
+
interval: int = 400,
|
|
730
|
+
x_range: tuple[float, float] = (0.0, 1.0),
|
|
731
|
+
n_curve: int = 400,
|
|
732
|
+
title: str | None = None,
|
|
733
|
+
) -> FuncAnimation:
|
|
734
|
+
"""Animate a cobweb (staircase) diagram for a 1D map ``x' = f(x)``.
|
|
735
|
+
|
|
736
|
+
Alternates vertical jumps (up to the curve, i.e. computing ``f(x)``) with
|
|
737
|
+
horizontal jumps (back down to the diagonal ``y = x``, i.e. feeding that
|
|
738
|
+
value back in as the next ``x``) -- the classic way to see fixed points,
|
|
739
|
+
cycles, and chaos geometrically for a 1D map.
|
|
740
|
+
|
|
741
|
+
Parameters
|
|
742
|
+
----------
|
|
743
|
+
system : DiscreteMap
|
|
744
|
+
Map to iterate; must have ``dim == 1``.
|
|
745
|
+
x0 : float, optional
|
|
746
|
+
Starting point; defaults to ``system.initial_state()[0]``.
|
|
747
|
+
n_iter : int, default 30
|
|
748
|
+
Number of map iterations (staircase steps) to animate.
|
|
749
|
+
interval : int, default 400
|
|
750
|
+
Delay between animation frames, in milliseconds.
|
|
751
|
+
x_range : tuple of float, default (0.0, 1.0)
|
|
752
|
+
Range over which to plot the map curve ``f(x)`` and diagonal.
|
|
753
|
+
n_curve : int, default 400
|
|
754
|
+
Number of points used to draw the smooth ``f(x)`` curve.
|
|
755
|
+
title : str, optional
|
|
756
|
+
Axes title.
|
|
757
|
+
|
|
758
|
+
Returns
|
|
759
|
+
-------
|
|
760
|
+
matplotlib.animation.FuncAnimation
|
|
761
|
+
"""
|
|
762
|
+
x0 = float(system.initial_state()[0]) if x0 is None else float(x0)
|
|
763
|
+
|
|
764
|
+
xs_curve = np.linspace(x_range[0], x_range[1], n_curve)
|
|
765
|
+
ys_curve = np.array([system.step(np.array([xv]))[0] for xv in xs_curve])
|
|
766
|
+
|
|
767
|
+
fig, ax = plt.subplots(figsize=(6.5, 6.5))
|
|
768
|
+
ax.plot(xs_curve, ys_curve, color=theme.PRIMARY, lw=1.5, label="f(x)")
|
|
769
|
+
ax.plot(x_range, x_range, color=theme.MUTED, lw=1.0, ls="--", label="y = x")
|
|
770
|
+
ax.set_xlim(*x_range)
|
|
771
|
+
ax.set_ylim(*x_range)
|
|
772
|
+
ax.set_aspect("equal")
|
|
773
|
+
ax.set_xlabel("x")
|
|
774
|
+
ax.set_ylabel("f(x)")
|
|
775
|
+
ax.set_title(title or f"{system.__class__.__name__} cobweb diagram")
|
|
776
|
+
ax.legend(loc="upper left")
|
|
777
|
+
|
|
778
|
+
(web,) = ax.plot([], [], color=theme.ACCENT, lw=1.0)
|
|
779
|
+
(point,) = ax.plot([], [], "o", color=theme.ACCENT, markersize=5)
|
|
780
|
+
|
|
781
|
+
web_x = [x0]
|
|
782
|
+
web_y = [x0]
|
|
783
|
+
x = x0
|
|
784
|
+
for _ in range(n_iter):
|
|
785
|
+
fx = float(system.step(np.array([x]))[0])
|
|
786
|
+
web_x += [x, fx]
|
|
787
|
+
web_y += [fx, fx]
|
|
788
|
+
x = fx
|
|
789
|
+
web_x_arr = np.array(web_x)
|
|
790
|
+
web_y_arr = np.array(web_y)
|
|
791
|
+
|
|
792
|
+
def update(frame: int) -> tuple:
|
|
793
|
+
end = min(len(web_x_arr), 2 * frame + 1)
|
|
794
|
+
web.set_data(web_x_arr[:end], web_y_arr[:end])
|
|
795
|
+
point.set_data([web_x_arr[end - 1]], [web_y_arr[end - 1]])
|
|
796
|
+
return web, point
|
|
797
|
+
|
|
798
|
+
anim = FuncAnimation(fig, update, frames=n_iter + 1, interval=interval, blit=False)
|
|
799
|
+
return anim
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def animate_trajectory_3d(
|
|
803
|
+
states: ArrayLike,
|
|
804
|
+
labels: tuple[str, str, str] = ("x", "y", "z"),
|
|
805
|
+
interval: int = 50,
|
|
806
|
+
skip: int = 1,
|
|
807
|
+
trail: int | None = None,
|
|
808
|
+
color: str = theme.PRIMARY,
|
|
809
|
+
title: str | None = None,
|
|
810
|
+
) -> FuncAnimation:
|
|
811
|
+
"""Animate a 3D trajectory being traced out over time.
|
|
812
|
+
|
|
813
|
+
Draws the path as a growing line with a bright marker at its leading
|
|
814
|
+
edge -- e.g. watching the Lorenz butterfly or a Rossler/Chua scroll being
|
|
815
|
+
drawn stroke by stroke, rather than seeing only the finished attractor.
|
|
816
|
+
|
|
817
|
+
Parameters
|
|
818
|
+
----------
|
|
819
|
+
states : array_like of float, shape (n, 3)
|
|
820
|
+
Trajectory to animate (e.g. from
|
|
821
|
+
:meth:`~physicskit.chaos.core.base_system.DynamicalSystem.trajectory`).
|
|
822
|
+
labels : tuple of str, default ("x", "y", "z")
|
|
823
|
+
Axis labels.
|
|
824
|
+
interval : int, default 50
|
|
825
|
+
Delay between animation frames, in milliseconds.
|
|
826
|
+
skip : int, default 1
|
|
827
|
+
Number of trajectory points advanced per animation frame; increase
|
|
828
|
+
for long trajectories so the animation covers them in a reasonable
|
|
829
|
+
number of frames.
|
|
830
|
+
trail : int, optional
|
|
831
|
+
Number of most-recent points to keep visible; the whole path-so-far
|
|
832
|
+
is kept if omitted.
|
|
833
|
+
color : str, default theme.PRIMARY
|
|
834
|
+
Line and marker color.
|
|
835
|
+
title : str, optional
|
|
836
|
+
Axes title.
|
|
837
|
+
|
|
838
|
+
Returns
|
|
839
|
+
-------
|
|
840
|
+
matplotlib.animation.FuncAnimation
|
|
841
|
+
"""
|
|
842
|
+
states = np.asarray(states, dtype=np.float64)
|
|
843
|
+
n = states.shape[0]
|
|
844
|
+
|
|
845
|
+
fig = plt.figure(figsize=(7, 6))
|
|
846
|
+
ax = fig.add_subplot(projection="3d")
|
|
847
|
+
ax.set_xlim(states[:, 0].min(), states[:, 0].max())
|
|
848
|
+
ax.set_ylim(states[:, 1].min(), states[:, 1].max())
|
|
849
|
+
ax.set_zlim(states[:, 2].min(), states[:, 2].max())
|
|
850
|
+
ax.set_xlabel(labels[0])
|
|
851
|
+
ax.set_ylabel(labels[1])
|
|
852
|
+
ax.set_zlabel(labels[2])
|
|
853
|
+
if title:
|
|
854
|
+
ax.set_title(title)
|
|
855
|
+
|
|
856
|
+
(line,) = ax.plot([], [], [], color=color, lw=0.8)
|
|
857
|
+
(head,) = ax.plot([], [], [], "o", color=theme.ACCENT, markersize=5)
|
|
858
|
+
|
|
859
|
+
frame_ends = np.arange(skip, n + skip, skip)
|
|
860
|
+
frame_ends[-1] = n
|
|
861
|
+
|
|
862
|
+
def update(frame: int) -> tuple:
|
|
863
|
+
end = int(frame_ends[frame])
|
|
864
|
+
lo = 0 if trail is None else max(0, end - trail)
|
|
865
|
+
line.set_data_3d(states[lo:end, 0], states[lo:end, 1], states[lo:end, 2])
|
|
866
|
+
head.set_data_3d([states[end - 1, 0]], [states[end - 1, 1]], [states[end - 1, 2]])
|
|
867
|
+
return line, head
|
|
868
|
+
|
|
869
|
+
anim = FuncAnimation(fig, update, frames=len(frame_ends), interval=interval, blit=False)
|
|
870
|
+
return anim
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def animate_phase_volume_contraction(
|
|
874
|
+
system: _TrajectorySystem,
|
|
875
|
+
center_state: ArrayLike | None = None,
|
|
876
|
+
ball_radius: float = 3.0,
|
|
877
|
+
n_points: int = 800,
|
|
878
|
+
t_max: float = 8.0,
|
|
879
|
+
dt: float = 0.01,
|
|
880
|
+
n_frames: int = 150,
|
|
881
|
+
interval: int = 50,
|
|
882
|
+
warmup_steps: int = 500,
|
|
883
|
+
seed: int | None = 0,
|
|
884
|
+
) -> FuncAnimation:
|
|
885
|
+
"""Animate a small ball of nearby states collapsing under a dissipative 3D flow.
|
|
886
|
+
|
|
887
|
+
Liouville's theorem (``d(log V)/dt = trace(Jacobian)``) is usually
|
|
888
|
+
illustrated by a single number -- the cumulative log-volume of an
|
|
889
|
+
infinitesimal ball, computed along one trajectory (see
|
|
890
|
+
:func:`~physicskit.chaos.utils.metrics.phase_volume_expansion`). This animates
|
|
891
|
+
what that number actually *means*: a literal ball of `n_points` nearby
|
|
892
|
+
initial conditions, each integrated forward independently, visibly
|
|
893
|
+
flattening from a sphere into a thin sheet draped over the attractor --
|
|
894
|
+
the geometric mechanism, for a dissipative system, behind both volume
|
|
895
|
+
contraction and the emergence of a lower-dimensional (often fractal)
|
|
896
|
+
attractor.
|
|
897
|
+
|
|
898
|
+
Parameters
|
|
899
|
+
----------
|
|
900
|
+
system : DynamicalSystem
|
|
901
|
+
System to integrate; must have ``dim == 3``.
|
|
902
|
+
center_state : array_like of float, shape (3,), optional
|
|
903
|
+
Center of the initial ball. If omitted, a short `warmup_steps`
|
|
904
|
+
integration from ``system.initial_state()`` is used to land near the
|
|
905
|
+
attractor first, so the ball starts already collapsing onto it
|
|
906
|
+
rather than spending time drifting there.
|
|
907
|
+
ball_radius : float, default 3.0
|
|
908
|
+
Radius of the initial ball of points.
|
|
909
|
+
n_points : int, default 800
|
|
910
|
+
Number of points sampled uniformly inside the ball.
|
|
911
|
+
t_max : float, default 8.0
|
|
912
|
+
Total integration time for every point.
|
|
913
|
+
dt : float, default 0.01
|
|
914
|
+
Integration step size.
|
|
915
|
+
n_frames : int, default 150
|
|
916
|
+
Number of animation frames, evenly spaced in time over ``[0, t_max]``.
|
|
917
|
+
interval : int, default 50
|
|
918
|
+
Delay between animation frames, in milliseconds.
|
|
919
|
+
warmup_steps : int, default 500
|
|
920
|
+
Number of integration steps used to find a default `center_state`
|
|
921
|
+
near the attractor; ignored if `center_state` is given.
|
|
922
|
+
seed : int, optional
|
|
923
|
+
Seed for the random point sampler, for reproducibility.
|
|
924
|
+
|
|
925
|
+
Returns
|
|
926
|
+
-------
|
|
927
|
+
matplotlib.animation.FuncAnimation
|
|
928
|
+
Assign it to a variable to keep it alive, and display it with
|
|
929
|
+
``plt.show()`` or save it with ``anim.save(...)``.
|
|
930
|
+
|
|
931
|
+
Raises
|
|
932
|
+
------
|
|
933
|
+
ValueError
|
|
934
|
+
If ``system.dim != 3``.
|
|
935
|
+
"""
|
|
936
|
+
if system.dim != 3:
|
|
937
|
+
raise ValueError("animate_phase_volume_contraction only supports 3D systems")
|
|
938
|
+
|
|
939
|
+
if center_state is None:
|
|
940
|
+
_, warmup = system.trajectory(n_steps=warmup_steps, dt=dt)
|
|
941
|
+
center = warmup[-1]
|
|
942
|
+
else:
|
|
943
|
+
center = np.asarray(center_state, dtype=np.float64)
|
|
944
|
+
|
|
945
|
+
rng = np.random.default_rng(seed)
|
|
946
|
+
directions = rng.normal(size=(n_points, 3))
|
|
947
|
+
directions /= np.linalg.norm(directions, axis=1, keepdims=True)
|
|
948
|
+
radii = ball_radius * rng.uniform(size=n_points) ** (1.0 / 3.0)
|
|
949
|
+
points0 = center + directions * radii[:, None]
|
|
950
|
+
|
|
951
|
+
n_steps = round(t_max / dt)
|
|
952
|
+
all_states = np.empty((n_points, n_steps + 1, 3))
|
|
953
|
+
for i in range(n_points):
|
|
954
|
+
_, states = system.trajectory(state0=points0[i], dt=dt, n_steps=n_steps)
|
|
955
|
+
all_states[i] = states
|
|
956
|
+
|
|
957
|
+
frame_indices = np.linspace(0, n_steps, n_frames).astype(int)
|
|
958
|
+
|
|
959
|
+
fig = plt.figure(figsize=(7, 6))
|
|
960
|
+
ax = fig.add_subplot(projection="3d")
|
|
961
|
+
flat = all_states.reshape(-1, 3)
|
|
962
|
+
ax.set_xlim(flat[:, 0].min(), flat[:, 0].max())
|
|
963
|
+
ax.set_ylim(flat[:, 1].min(), flat[:, 1].max())
|
|
964
|
+
ax.set_zlim(flat[:, 2].min(), flat[:, 2].max())
|
|
965
|
+
ax.set_xlabel("x")
|
|
966
|
+
ax.set_ylabel("y")
|
|
967
|
+
ax.set_zlabel("z")
|
|
968
|
+
|
|
969
|
+
scatter = ax.scatter(points0[:, 0], points0[:, 1], points0[:, 2], s=4, color=theme.ACCENT, alpha=0.6)
|
|
970
|
+
title = ax.set_title(f"{system.__class__.__name__}: phase-space volume contracting (t=0.00)")
|
|
971
|
+
|
|
972
|
+
def update(frame: int) -> tuple:
|
|
973
|
+
idx = int(frame_indices[frame])
|
|
974
|
+
pts = all_states[:, idx, :]
|
|
975
|
+
scatter._offsets3d = (pts[:, 0], pts[:, 1], pts[:, 2])
|
|
976
|
+
title.set_text(f"{system.__class__.__name__}: phase-space volume contracting (t={idx * dt:.2f})")
|
|
977
|
+
return scatter, title
|
|
978
|
+
|
|
979
|
+
anim = FuncAnimation(fig, update, frames=n_frames, interval=interval, blit=False)
|
|
980
|
+
return anim
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def animate_trajectory_2d(
|
|
984
|
+
states: ArrayLike,
|
|
985
|
+
labels: tuple[str, str] = ("x", "y"),
|
|
986
|
+
interval: int = 50,
|
|
987
|
+
skip: int = 1,
|
|
988
|
+
trail: int | None = None,
|
|
989
|
+
color: str = theme.PRIMARY,
|
|
990
|
+
markers: list[dict[str, Any]] | None = None,
|
|
991
|
+
equal_aspect: bool = False,
|
|
992
|
+
title: str | None = None,
|
|
993
|
+
) -> FuncAnimation:
|
|
994
|
+
"""Animate a 2D trajectory being traced out over time.
|
|
995
|
+
|
|
996
|
+
Draws the path as a growing line with a bright marker at its leading
|
|
997
|
+
edge, e.g. a Duffing oscillator's phase portrait ``(x, v)`` unspooling or
|
|
998
|
+
a restricted-three-body orbit looping around its primaries.
|
|
999
|
+
|
|
1000
|
+
Parameters
|
|
1001
|
+
----------
|
|
1002
|
+
states : array_like of float, shape (n, 2)
|
|
1003
|
+
Trajectory to animate; only the first two columns are used, so a
|
|
1004
|
+
higher-dimensional state (e.g. ``(x, y, vx, vy)``) can be passed
|
|
1005
|
+
directly to plot its first two components.
|
|
1006
|
+
labels : tuple of str, default ("x", "y")
|
|
1007
|
+
Axis labels.
|
|
1008
|
+
interval : int, default 50
|
|
1009
|
+
Delay between animation frames, in milliseconds.
|
|
1010
|
+
skip : int, default 1
|
|
1011
|
+
Number of trajectory points advanced per animation frame; increase
|
|
1012
|
+
for long trajectories so the animation covers them in a reasonable
|
|
1013
|
+
number of frames.
|
|
1014
|
+
trail : int, optional
|
|
1015
|
+
Number of most-recent points to keep visible; the whole path-so-far
|
|
1016
|
+
is kept if omitted.
|
|
1017
|
+
color : str, default theme.PRIMARY
|
|
1018
|
+
Line and marker color.
|
|
1019
|
+
markers : list of dict, optional
|
|
1020
|
+
Extra fixed reference points to scatter once (e.g. the two primaries
|
|
1021
|
+
of a restricted three-body orbit); each dict needs ``"x"`` and
|
|
1022
|
+
``"y"`` keys plus any ``ax.scatter`` keyword arguments.
|
|
1023
|
+
equal_aspect : bool, default False
|
|
1024
|
+
Whether to force an equal aspect ratio.
|
|
1025
|
+
title : str, optional
|
|
1026
|
+
Axes title.
|
|
1027
|
+
|
|
1028
|
+
Returns
|
|
1029
|
+
-------
|
|
1030
|
+
matplotlib.animation.FuncAnimation
|
|
1031
|
+
"""
|
|
1032
|
+
states = np.asarray(states, dtype=np.float64)
|
|
1033
|
+
n = states.shape[0]
|
|
1034
|
+
|
|
1035
|
+
fig, ax = plt.subplots(figsize=(6.5, 6))
|
|
1036
|
+
pad_x = 0.05 * (states[:, 0].max() - states[:, 0].min() + 1e-12)
|
|
1037
|
+
pad_y = 0.05 * (states[:, 1].max() - states[:, 1].min() + 1e-12)
|
|
1038
|
+
ax.set_xlim(states[:, 0].min() - pad_x, states[:, 0].max() + pad_x)
|
|
1039
|
+
ax.set_ylim(states[:, 1].min() - pad_y, states[:, 1].max() + pad_y)
|
|
1040
|
+
ax.set_xlabel(labels[0])
|
|
1041
|
+
ax.set_ylabel(labels[1])
|
|
1042
|
+
if equal_aspect:
|
|
1043
|
+
ax.set_aspect("equal")
|
|
1044
|
+
if title:
|
|
1045
|
+
ax.set_title(title)
|
|
1046
|
+
|
|
1047
|
+
for m in markers or []:
|
|
1048
|
+
ax.scatter([m["x"]], [m["y"]], **{k: v for k, v in m.items() if k not in ("x", "y")})
|
|
1049
|
+
if markers and any("label" in m for m in markers):
|
|
1050
|
+
ax.legend(loc="upper right", fontsize=8)
|
|
1051
|
+
|
|
1052
|
+
(line,) = ax.plot([], [], color=color, lw=0.8)
|
|
1053
|
+
(head,) = ax.plot([], [], "o", color=theme.ACCENT, markersize=6)
|
|
1054
|
+
|
|
1055
|
+
frame_ends = np.arange(skip, n + skip, skip)
|
|
1056
|
+
frame_ends[-1] = n
|
|
1057
|
+
|
|
1058
|
+
def update(frame: int) -> tuple:
|
|
1059
|
+
end = int(frame_ends[frame])
|
|
1060
|
+
lo = 0 if trail is None else max(0, end - trail)
|
|
1061
|
+
line.set_data(states[lo:end, 0], states[lo:end, 1])
|
|
1062
|
+
head.set_data([states[end - 1, 0]], [states[end - 1, 1]])
|
|
1063
|
+
return line, head
|
|
1064
|
+
|
|
1065
|
+
anim = FuncAnimation(fig, update, frames=len(frame_ends), interval=interval, blit=False)
|
|
1066
|
+
return anim
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def animate_double_pendulum(
|
|
1070
|
+
system: DoublePendulum,
|
|
1071
|
+
state0: ArrayLike | None = None,
|
|
1072
|
+
dt: float = 0.01,
|
|
1073
|
+
n_steps: int = 1000,
|
|
1074
|
+
interval: int = 40,
|
|
1075
|
+
trail: int = 300,
|
|
1076
|
+
skip: int = 1,
|
|
1077
|
+
) -> FuncAnimation:
|
|
1078
|
+
"""Animate the double pendulum swinging, arms and all.
|
|
1079
|
+
|
|
1080
|
+
Integrates the pendulum and renders it kinematically -- the two rigid
|
|
1081
|
+
rods and bobs swinging in real ``(x, y)`` space, with a fading trail
|
|
1082
|
+
behind the outer bob -- rather than only the abstract
|
|
1083
|
+
``(theta, omega)`` trajectory.
|
|
1084
|
+
|
|
1085
|
+
Parameters
|
|
1086
|
+
----------
|
|
1087
|
+
system : DoublePendulum
|
|
1088
|
+
Pendulum to animate.
|
|
1089
|
+
state0 : array_like of float, shape (4,), optional
|
|
1090
|
+
Initial state ``(theta1, theta2, omega1, omega2)``; defaults to
|
|
1091
|
+
:meth:`~physicskit.chaos.systems.continuous.DoublePendulum.initial_state`.
|
|
1092
|
+
dt : float, default 0.01
|
|
1093
|
+
Integration step size.
|
|
1094
|
+
n_steps : int, default 1000
|
|
1095
|
+
Number of integration steps to integrate.
|
|
1096
|
+
interval : int, default 40
|
|
1097
|
+
Delay between animation frames, in milliseconds.
|
|
1098
|
+
trail : int, default 300
|
|
1099
|
+
Number of most-recent integration steps to keep visible as a trail
|
|
1100
|
+
behind the outer bob.
|
|
1101
|
+
skip : int, default 1
|
|
1102
|
+
Number of integration steps advanced per animation frame; increase
|
|
1103
|
+
for long integrations so the animation covers them in a reasonable
|
|
1104
|
+
number of frames.
|
|
1105
|
+
|
|
1106
|
+
Returns
|
|
1107
|
+
-------
|
|
1108
|
+
matplotlib.animation.FuncAnimation
|
|
1109
|
+
"""
|
|
1110
|
+
state0 = system.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
|
|
1111
|
+
_, states = system.trajectory(state0, dt=dt, n_steps=n_steps)
|
|
1112
|
+
th1, th2 = states[:, 0], states[:, 1]
|
|
1113
|
+
l1, l2 = system.l1, system.l2
|
|
1114
|
+
|
|
1115
|
+
x1 = l1 * np.sin(th1)
|
|
1116
|
+
y1 = -l1 * np.cos(th1)
|
|
1117
|
+
x2 = x1 + l2 * np.sin(th2)
|
|
1118
|
+
y2 = y1 - l2 * np.cos(th2)
|
|
1119
|
+
|
|
1120
|
+
reach = l1 + l2
|
|
1121
|
+
fig, ax = plt.subplots(figsize=(6, 6))
|
|
1122
|
+
ax.set_xlim(-1.05 * reach, 1.05 * reach)
|
|
1123
|
+
ax.set_ylim(-1.05 * reach, 1.05 * reach)
|
|
1124
|
+
ax.set_aspect("equal")
|
|
1125
|
+
ax.set_title("Double pendulum")
|
|
1126
|
+
|
|
1127
|
+
(trail_line,) = ax.plot([], [], color=theme.ACCENT, lw=0.8, alpha=0.7)
|
|
1128
|
+
(rods,) = ax.plot([], [], "-", color=theme.STRUCTURE, lw=1.5)
|
|
1129
|
+
(bobs,) = ax.plot([], [], "o", color=theme.PRIMARY, markersize=9)
|
|
1130
|
+
|
|
1131
|
+
n = states.shape[0]
|
|
1132
|
+
frame_ends = np.arange(skip, n + skip, skip)
|
|
1133
|
+
frame_ends[-1] = n
|
|
1134
|
+
|
|
1135
|
+
def update(frame: int) -> tuple:
|
|
1136
|
+
end = int(frame_ends[frame]) - 1
|
|
1137
|
+
lo = max(0, end - trail)
|
|
1138
|
+
trail_line.set_data(x2[lo : end + 1], y2[lo : end + 1])
|
|
1139
|
+
rods.set_data([0.0, x1[end], x2[end]], [0.0, y1[end], y2[end]])
|
|
1140
|
+
bobs.set_data([x1[end], x2[end]], [y1[end], y2[end]])
|
|
1141
|
+
return trail_line, rods, bobs
|
|
1142
|
+
|
|
1143
|
+
anim = FuncAnimation(fig, update, frames=len(frame_ends), interval=interval, blit=False)
|
|
1144
|
+
return anim
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def animate_magnetic_pendulum(
|
|
1148
|
+
system: MagneticPendulum,
|
|
1149
|
+
state0: ArrayLike | None = None,
|
|
1150
|
+
dt: float = 0.05,
|
|
1151
|
+
n_steps: int = 600,
|
|
1152
|
+
interval: int = 40,
|
|
1153
|
+
trail: int = 150,
|
|
1154
|
+
skip: int = 1,
|
|
1155
|
+
) -> FuncAnimation:
|
|
1156
|
+
"""Animate a magnetic pendulum bob swinging in toward whichever magnet wins.
|
|
1157
|
+
|
|
1158
|
+
Parameters
|
|
1159
|
+
----------
|
|
1160
|
+
system : MagneticPendulum
|
|
1161
|
+
Pendulum to animate.
|
|
1162
|
+
state0 : array_like of float, shape (4,), optional
|
|
1163
|
+
Initial state ``(x, y, vx, vy)``; defaults to
|
|
1164
|
+
:meth:`~physicskit.chaos.systems.continuous.MagneticPendulum.initial_state`.
|
|
1165
|
+
dt : float, default 0.05
|
|
1166
|
+
Integration step size.
|
|
1167
|
+
n_steps : int, default 600
|
|
1168
|
+
Number of integration steps to integrate.
|
|
1169
|
+
interval : int, default 40
|
|
1170
|
+
Delay between animation frames, in milliseconds.
|
|
1171
|
+
trail : int, default 150
|
|
1172
|
+
Number of most-recent integration steps to keep visible as a trail.
|
|
1173
|
+
skip : int, default 1
|
|
1174
|
+
Number of integration steps advanced per animation frame; increase
|
|
1175
|
+
for long integrations so the animation covers them in a reasonable
|
|
1176
|
+
number of frames.
|
|
1177
|
+
|
|
1178
|
+
Returns
|
|
1179
|
+
-------
|
|
1180
|
+
matplotlib.animation.FuncAnimation
|
|
1181
|
+
"""
|
|
1182
|
+
state0 = system.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
|
|
1183
|
+
_, states = system.trajectory(state0, dt=dt, n_steps=n_steps)
|
|
1184
|
+
x, y = states[:, 0], states[:, 1]
|
|
1185
|
+
magnets = system.magnet_positions
|
|
1186
|
+
|
|
1187
|
+
extent = max(1.5, float(np.abs(magnets).max()) * 1.4, float(np.abs(states[:, :2]).max()) * 1.1)
|
|
1188
|
+
fig, ax = plt.subplots(figsize=(6, 6))
|
|
1189
|
+
ax.set_xlim(-extent, extent)
|
|
1190
|
+
ax.set_ylim(-extent, extent)
|
|
1191
|
+
ax.set_aspect("equal")
|
|
1192
|
+
ax.scatter(
|
|
1193
|
+
magnets[:, 0],
|
|
1194
|
+
magnets[:, 1],
|
|
1195
|
+
marker="X",
|
|
1196
|
+
s=120,
|
|
1197
|
+
color=theme.STRUCTURE,
|
|
1198
|
+
zorder=3,
|
|
1199
|
+
label="magnets",
|
|
1200
|
+
)
|
|
1201
|
+
ax.set_title("Magnetic pendulum")
|
|
1202
|
+
ax.legend(loc="upper right")
|
|
1203
|
+
|
|
1204
|
+
(trail_line,) = ax.plot([], [], color=theme.PRIMARY, lw=1.0, alpha=0.8)
|
|
1205
|
+
(bob,) = ax.plot([], [], "o", color=theme.ACCENT, markersize=8, zorder=4)
|
|
1206
|
+
|
|
1207
|
+
n = states.shape[0]
|
|
1208
|
+
frame_ends = np.arange(skip, n + skip, skip)
|
|
1209
|
+
frame_ends[-1] = n
|
|
1210
|
+
|
|
1211
|
+
def update(frame: int) -> tuple:
|
|
1212
|
+
end = int(frame_ends[frame]) - 1
|
|
1213
|
+
lo = max(0, end - trail)
|
|
1214
|
+
trail_line.set_data(x[lo : end + 1], y[lo : end + 1])
|
|
1215
|
+
bob.set_data([x[end]], [y[end]])
|
|
1216
|
+
return trail_line, bob
|
|
1217
|
+
|
|
1218
|
+
anim = FuncAnimation(fig, update, frames=len(frame_ends), interval=interval, blit=False)
|
|
1219
|
+
return anim
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
def animate_restricted_three_body(
|
|
1223
|
+
system: RestrictedThreeBody,
|
|
1224
|
+
state0: ArrayLike | None = None,
|
|
1225
|
+
dt: float = 0.0005,
|
|
1226
|
+
n_steps: int = 20000,
|
|
1227
|
+
interval: int = 30,
|
|
1228
|
+
trail: int = 1000,
|
|
1229
|
+
skip: int = 10,
|
|
1230
|
+
) -> FuncAnimation:
|
|
1231
|
+
"""Animate a CR3BP trajectory in the rotating frame, alongside both
|
|
1232
|
+
primaries and all five Lagrange points.
|
|
1233
|
+
|
|
1234
|
+
The two primaries sit fixed (by construction of the rotating frame)
|
|
1235
|
+
at ``(-mu, 0)`` and ``(1 - mu, 0)``; the five Lagrange points (see
|
|
1236
|
+
:func:`~physicskit.chaos.systems.continuous.lagrange_points`) are marked and
|
|
1237
|
+
labeled once, since they too are fixed equilibria of the rotating-frame
|
|
1238
|
+
dynamics -- watching the test particle's trajectory loop past them
|
|
1239
|
+
directly shows how close (or not) a given orbit passes to each one.
|
|
1240
|
+
|
|
1241
|
+
Parameters
|
|
1242
|
+
----------
|
|
1243
|
+
system : RestrictedThreeBody
|
|
1244
|
+
CR3BP system to animate.
|
|
1245
|
+
state0 : array_like of float, shape (4,), optional
|
|
1246
|
+
Initial state ``(x, y, vx, vy)``; defaults to
|
|
1247
|
+
:meth:`~physicskit.chaos.systems.continuous.RestrictedThreeBody.initial_state`.
|
|
1248
|
+
dt : float, default 0.0005
|
|
1249
|
+
Integration step size.
|
|
1250
|
+
n_steps : int, default 20000
|
|
1251
|
+
Number of integration steps to integrate.
|
|
1252
|
+
interval : int, default 30
|
|
1253
|
+
Delay between animation frames, in milliseconds.
|
|
1254
|
+
trail : int, default 1000
|
|
1255
|
+
Number of most-recent integration steps to keep visible as a trail.
|
|
1256
|
+
skip : int, default 10
|
|
1257
|
+
Number of integration steps advanced per animation frame; increase
|
|
1258
|
+
for long integrations so the animation covers them in a reasonable
|
|
1259
|
+
number of frames.
|
|
1260
|
+
|
|
1261
|
+
Returns
|
|
1262
|
+
-------
|
|
1263
|
+
matplotlib.animation.FuncAnimation
|
|
1264
|
+
"""
|
|
1265
|
+
state0 = system.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
|
|
1266
|
+
_, states = system.trajectory(state0, dt=dt, n_steps=n_steps)
|
|
1267
|
+
x, y = states[:, 0], states[:, 1]
|
|
1268
|
+
mu = system.mu
|
|
1269
|
+
lpoints = lagrange_points(mu)
|
|
1270
|
+
|
|
1271
|
+
fig, ax = plt.subplots(figsize=(7, 7))
|
|
1272
|
+
pad = 0.15 * max(1.0, float(np.abs(np.concatenate([x, y, lpoints.ravel()])).max()))
|
|
1273
|
+
ax.set_xlim(min(x.min(), lpoints[:, 0].min()) - pad, max(x.max(), lpoints[:, 0].max()) + pad)
|
|
1274
|
+
ax.set_ylim(min(y.min(), lpoints[:, 1].min()) - pad, max(y.max(), lpoints[:, 1].max()) + pad)
|
|
1275
|
+
ax.set_aspect("equal")
|
|
1276
|
+
ax.set_title(f"{system.__class__.__name__} (rotating frame)")
|
|
1277
|
+
|
|
1278
|
+
ax.scatter([-mu], [0.0], marker="o", s=100, color=theme.STRUCTURE, zorder=3, label="primary")
|
|
1279
|
+
ax.scatter([1.0 - mu], [0.0], marker="o", s=35, color=theme.MUTED, zorder=3, label="secondary")
|
|
1280
|
+
ax.scatter(lpoints[:, 0], lpoints[:, 1], marker="^", s=40, color=theme.ACCENT, zorder=3, label="Lagrange points")
|
|
1281
|
+
for i, (lx, ly) in enumerate(lpoints, start=1):
|
|
1282
|
+
ax.annotate(f"L{i}", (lx, ly), textcoords="offset points", xytext=(4, 4), fontsize=8)
|
|
1283
|
+
ax.legend(loc="upper right", fontsize=8)
|
|
1284
|
+
|
|
1285
|
+
(trail_line,) = ax.plot([], [], color=theme.PRIMARY, lw=0.8)
|
|
1286
|
+
(head,) = ax.plot([], [], "o", color=theme.PRIMARY, markersize=6, zorder=4)
|
|
1287
|
+
|
|
1288
|
+
n = states.shape[0]
|
|
1289
|
+
frame_ends = np.arange(skip, n + skip, skip)
|
|
1290
|
+
frame_ends[-1] = n
|
|
1291
|
+
|
|
1292
|
+
def update(frame: int) -> tuple:
|
|
1293
|
+
end = int(frame_ends[frame]) - 1
|
|
1294
|
+
lo = max(0, end - trail)
|
|
1295
|
+
trail_line.set_data(x[lo : end + 1], y[lo : end + 1])
|
|
1296
|
+
head.set_data([x[end]], [y[end]])
|
|
1297
|
+
return trail_line, head
|
|
1298
|
+
|
|
1299
|
+
anim = FuncAnimation(fig, update, frames=len(frame_ends), interval=interval, blit=False)
|
|
1300
|
+
return anim
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
def animate_driven_pendulum(
|
|
1304
|
+
system: DrivenPendulum,
|
|
1305
|
+
state0: ArrayLike | None = None,
|
|
1306
|
+
dt: float = 0.02,
|
|
1307
|
+
n_steps: int = 1500,
|
|
1308
|
+
interval: int = 40,
|
|
1309
|
+
trail: int = 200,
|
|
1310
|
+
skip: int = 1,
|
|
1311
|
+
length: float = 1.0,
|
|
1312
|
+
) -> FuncAnimation:
|
|
1313
|
+
"""Animate the driven, damped pendulum swinging in real space.
|
|
1314
|
+
|
|
1315
|
+
Parameters
|
|
1316
|
+
----------
|
|
1317
|
+
system : DrivenPendulum
|
|
1318
|
+
Pendulum to animate.
|
|
1319
|
+
state0 : array_like of float, shape (2,), optional
|
|
1320
|
+
Initial state ``(theta, omega)``; defaults to
|
|
1321
|
+
:meth:`~physicskit.chaos.systems.continuous.DrivenPendulum.initial_state`.
|
|
1322
|
+
dt : float, default 0.02
|
|
1323
|
+
Integration step size.
|
|
1324
|
+
n_steps : int, default 1500
|
|
1325
|
+
Number of integration steps to integrate.
|
|
1326
|
+
interval : int, default 40
|
|
1327
|
+
Delay between animation frames, in milliseconds.
|
|
1328
|
+
trail : int, default 200
|
|
1329
|
+
Number of most-recent integration steps to keep visible as a trail.
|
|
1330
|
+
skip : int, default 1
|
|
1331
|
+
Number of integration steps advanced per animation frame; increase
|
|
1332
|
+
for long integrations so the animation covers them in a reasonable
|
|
1333
|
+
number of frames.
|
|
1334
|
+
length : float, default 1.0
|
|
1335
|
+
Visual rod length; only ``g/l`` enters the dynamics, so the actual
|
|
1336
|
+
drawn length is an arbitrary visualization choice.
|
|
1337
|
+
|
|
1338
|
+
Returns
|
|
1339
|
+
-------
|
|
1340
|
+
matplotlib.animation.FuncAnimation
|
|
1341
|
+
"""
|
|
1342
|
+
state0 = system.initial_state() if state0 is None else np.asarray(state0, dtype=np.float64)
|
|
1343
|
+
_, states = system.trajectory(state0, dt=dt, n_steps=n_steps)
|
|
1344
|
+
theta = states[:, 0]
|
|
1345
|
+
x = length * np.sin(theta)
|
|
1346
|
+
y = -length * np.cos(theta)
|
|
1347
|
+
|
|
1348
|
+
reach = length * 1.2
|
|
1349
|
+
fig, ax = plt.subplots(figsize=(6, 6))
|
|
1350
|
+
ax.set_xlim(-reach, reach)
|
|
1351
|
+
ax.set_ylim(-reach, reach)
|
|
1352
|
+
ax.set_aspect("equal")
|
|
1353
|
+
ax.set_title("Driven, damped pendulum")
|
|
1354
|
+
|
|
1355
|
+
(trail_line,) = ax.plot([], [], color=theme.ACCENT, lw=0.8, alpha=0.7)
|
|
1356
|
+
(rod,) = ax.plot([], [], "-", color=theme.STRUCTURE, lw=1.5)
|
|
1357
|
+
(bob,) = ax.plot([], [], "o", color=theme.PRIMARY, markersize=10)
|
|
1358
|
+
|
|
1359
|
+
n = states.shape[0]
|
|
1360
|
+
frame_ends = np.arange(skip, n + skip, skip)
|
|
1361
|
+
frame_ends[-1] = n
|
|
1362
|
+
|
|
1363
|
+
def update(frame: int) -> tuple:
|
|
1364
|
+
end = int(frame_ends[frame]) - 1
|
|
1365
|
+
lo = max(0, end - trail)
|
|
1366
|
+
trail_line.set_data(x[lo : end + 1], y[lo : end + 1])
|
|
1367
|
+
rod.set_data([0.0, x[end]], [0.0, y[end]])
|
|
1368
|
+
bob.set_data([x[end]], [y[end]])
|
|
1369
|
+
return trail_line, rod, bob
|
|
1370
|
+
|
|
1371
|
+
anim = FuncAnimation(fig, update, frames=len(frame_ends), interval=interval, blit=False)
|
|
1372
|
+
return anim
|