import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.widgets import Button
from mpl_toolkits.mplot3d import Axes3D
# Set up the figure and axis
fig = plt.figure ( figsize= ( 8 , 8 ) )
ax = fig.add_subplot ( 111 , projection= '3d' )
# Initial settings
initial_radius = 5 # Initial radius of the sphere
num_galaxies = 20 # Number of other galaxies
# Generate random spherical coordinates for galaxies
phi = np.random .uniform ( 0 , 2 * np.pi , num_galaxies)
theta = np.random .uniform ( 0 , np.pi , num_galaxies)
radii = np.random .uniform ( 0 , initial_radius, num_galaxies)
# Convert spherical coordinates to Cartesian coordinates
x = radii * np.sin ( theta) * np.cos ( phi)
y = radii * np.sin ( theta) * np.sin ( phi)
z = radii * np.cos ( theta)
galaxy_positions = np.column_stack ( ( x, y, z) )
# Scatter the galaxies
galaxies_scatter = ax.scatter ( galaxy_positions[ :, 0 ] , galaxy_positions[ :, 1 ] , galaxy_positions[ :, 2 ] ,
color= 'red' , s= 50 , zorder= 3 )
ax.scatter ( 0 , 0 , 0 , color= 'black' , zorder= 4 ) # Home galaxy in the center
# Initialize traces (lines) for each galaxy
galaxy_traces = [ ]
for i in range ( num_galaxies) :
trace, = ax.plot ( [ ] , [ ] , [ ] , 'r--' , lw= 1 , alpha= 0.7 ) # Dotted red lines for traces
galaxy_traces.append ( trace)
# Set fixed plot limits and title
ax.set_xlim ( -initial_radius * 2 , initial_radius * 2 )
ax.set_ylim ( -initial_radius * 2 , initial_radius * 2 )
ax.set_zlim ( -initial_radius * 2 , initial_radius * 2 )
ax.set_title ( "Hubble's Law Simulation in 3D (Expanding Universe with Traces)" )
# Initialize sphere plot
sphere_patch = None
# Define the update function for the animation
def update( frame) :
global sphere_patch
# Stretch the sphere (radius grows)
new_radius = initial_radius + frame * ( initial_radius / 100 )
# Remove the previous sphere if it exists
if sphere_patch is not None :
sphere_patch.remove ( )
# Draw the new sphere
u = np.linspace ( 0 , 2 * np.pi , 100 )
v = np.linspace ( 0 , np.pi , 100 )
X = new_radius * np.outer ( np.cos ( u) , np.sin ( v) )
Y = new_radius * np.outer ( np.sin ( u) , np.sin ( v) )
Z = new_radius * np.outer ( np.ones ( np.size ( u) ) , np.cos ( v) )
sphere_patch = ax.plot_surface ( X, Y, Z, color= 'lightblue' , alpha= 0.2 , edgecolor= 'none' )
# Move the galaxies (except the home galaxy) with the expansion
new_positions = galaxy_positions * ( 1 + frame / 100 )
galaxies_scatter._offsets3d = ( new_positions[ :, 0 ] , new_positions[ :, 1 ] , new_positions[ :, 2 ] )
# Update the traces for each galaxy
for i, trace in enumerate ( galaxy_traces) :
trace.set_data ( [ 0 , new_positions[ i, 0 ] ] , [ 0 , new_positions[ i, 1 ] ] )
trace.set_3d_properties ( [ 0 , new_positions[ i, 2 ] ] )
# Create the animation
anim = FuncAnimation( fig, update, frames= 101 , interval= 50 )
# Pause/play button callback function
is_paused = False
def toggle_pause( event) :
global is_paused
if is_paused:
anim.event_source .start ( )
button.label .set_text ( 'Pause' )
else :
anim.event_source .stop ( )
button.label .set_text ( 'Play' )
is_paused = not is_paused
# Add the pause/play button
ax_button = plt.axes ( [ 0.4 , 0.05 , 0.2 , 0.075 ] ) # Position of the button
button = Button( ax_button, 'Pause' )
button.on_clicked ( toggle_pause)
plt.show ( )
aW1wb3J0IG51bXB5IGFzIG5wCmltcG9ydCBtYXRwbG90bGliLnB5cGxvdCBhcyBwbHQKZnJvbSBtYXRwbG90bGliLmFuaW1hdGlvbiBpbXBvcnQgRnVuY0FuaW1hdGlvbgpmcm9tIG1hdHBsb3RsaWIud2lkZ2V0cyBpbXBvcnQgQnV0dG9uCmZyb20gbXBsX3Rvb2xraXRzLm1wbG90M2QgaW1wb3J0IEF4ZXMzRAoKIyBTZXQgdXAgdGhlIGZpZ3VyZSBhbmQgYXhpcwpmaWcgPSBwbHQuZmlndXJlKGZpZ3NpemU9KDgsIDgpKQpheCA9IGZpZy5hZGRfc3VicGxvdCgxMTEsIHByb2plY3Rpb249JzNkJykKCiMgSW5pdGlhbCBzZXR0aW5ncwppbml0aWFsX3JhZGl1cyA9IDUgICMgSW5pdGlhbCByYWRpdXMgb2YgdGhlIHNwaGVyZQpudW1fZ2FsYXhpZXMgPSAyMCAgIyBOdW1iZXIgb2Ygb3RoZXIgZ2FsYXhpZXMKCiMgR2VuZXJhdGUgcmFuZG9tIHNwaGVyaWNhbCBjb29yZGluYXRlcyBmb3IgZ2FsYXhpZXMKcGhpID0gbnAucmFuZG9tLnVuaWZvcm0oMCwgMiAqIG5wLnBpLCBudW1fZ2FsYXhpZXMpCnRoZXRhID0gbnAucmFuZG9tLnVuaWZvcm0oMCwgbnAucGksIG51bV9nYWxheGllcykKcmFkaWkgPSBucC5yYW5kb20udW5pZm9ybSgwLCBpbml0aWFsX3JhZGl1cywgbnVtX2dhbGF4aWVzKQoKIyBDb252ZXJ0IHNwaGVyaWNhbCBjb29yZGluYXRlcyB0byBDYXJ0ZXNpYW4gY29vcmRpbmF0ZXMKeCA9IHJhZGlpICogbnAuc2luKHRoZXRhKSAqIG5wLmNvcyhwaGkpCnkgPSByYWRpaSAqIG5wLnNpbih0aGV0YSkgKiBucC5zaW4ocGhpKQp6ID0gcmFkaWkgKiBucC5jb3ModGhldGEpCmdhbGF4eV9wb3NpdGlvbnMgPSBucC5jb2x1bW5fc3RhY2soKHgsIHksIHopKQoKIyBTY2F0dGVyIHRoZSBnYWxheGllcwpnYWxheGllc19zY2F0dGVyID0gYXguc2NhdHRlcihnYWxheHlfcG9zaXRpb25zWzosIDBdLCBnYWxheHlfcG9zaXRpb25zWzosIDFdLCBnYWxheHlfcG9zaXRpb25zWzosIDJdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjb2xvcj0ncmVkJywgcz01MCwgem9yZGVyPTMpCmF4LnNjYXR0ZXIoMCwgMCwgMCwgY29sb3I9J2JsYWNrJywgem9yZGVyPTQpICAjIEhvbWUgZ2FsYXh5IGluIHRoZSBjZW50ZXIKCiMgSW5pdGlhbGl6ZSB0cmFjZXMgKGxpbmVzKSBmb3IgZWFjaCBnYWxheHkKZ2FsYXh5X3RyYWNlcyA9IFtdCmZvciBpIGluIHJhbmdlKG51bV9nYWxheGllcyk6CiAgICB0cmFjZSwgPSBheC5wbG90KFtdLCBbXSwgW10sICdyLS0nLCBsdz0xLCBhbHBoYT0wLjcpICAjIERvdHRlZCByZWQgbGluZXMgZm9yIHRyYWNlcwogICAgZ2FsYXh5X3RyYWNlcy5hcHBlbmQodHJhY2UpCgojIFNldCBmaXhlZCBwbG90IGxpbWl0cyBhbmQgdGl0bGUKYXguc2V0X3hsaW0oLWluaXRpYWxfcmFkaXVzICogMiwgaW5pdGlhbF9yYWRpdXMgKiAyKQpheC5zZXRfeWxpbSgtaW5pdGlhbF9yYWRpdXMgKiAyLCBpbml0aWFsX3JhZGl1cyAqIDIpCmF4LnNldF96bGltKC1pbml0aWFsX3JhZGl1cyAqIDIsIGluaXRpYWxfcmFkaXVzICogMikKYXguc2V0X3RpdGxlKCJIdWJibGUncyBMYXcgU2ltdWxhdGlvbiBpbiAzRCAoRXhwYW5kaW5nIFVuaXZlcnNlIHdpdGggVHJhY2VzKSIpCgojIEluaXRpYWxpemUgc3BoZXJlIHBsb3QKc3BoZXJlX3BhdGNoID0gTm9uZQoKIyBEZWZpbmUgdGhlIHVwZGF0ZSBmdW5jdGlvbiBmb3IgdGhlIGFuaW1hdGlvbgpkZWYgdXBkYXRlKGZyYW1lKToKICAgIGdsb2JhbCBzcGhlcmVfcGF0Y2gKCiAgICAjIFN0cmV0Y2ggdGhlIHNwaGVyZSAocmFkaXVzIGdyb3dzKQogICAgbmV3X3JhZGl1cyA9IGluaXRpYWxfcmFkaXVzICsgZnJhbWUgKiAoaW5pdGlhbF9yYWRpdXMgLyAxMDApCgogICAgIyBSZW1vdmUgdGhlIHByZXZpb3VzIHNwaGVyZSBpZiBpdCBleGlzdHMKICAgIGlmIHNwaGVyZV9wYXRjaCBpcyBub3QgTm9uZToKICAgICAgICBzcGhlcmVfcGF0Y2gucmVtb3ZlKCkKCiAgICAjIERyYXcgdGhlIG5ldyBzcGhlcmUKICAgIHUgPSBucC5saW5zcGFjZSgwLCAyICogbnAucGksIDEwMCkKICAgIHYgPSBucC5saW5zcGFjZSgwLCBucC5waSwgMTAwKQogICAgWCA9IG5ld19yYWRpdXMgKiBucC5vdXRlcihucC5jb3ModSksIG5wLnNpbih2KSkKICAgIFkgPSBuZXdfcmFkaXVzICogbnAub3V0ZXIobnAuc2luKHUpLCBucC5zaW4odikpCiAgICBaID0gbmV3X3JhZGl1cyAqIG5wLm91dGVyKG5wLm9uZXMobnAuc2l6ZSh1KSksIG5wLmNvcyh2KSkKICAgIHNwaGVyZV9wYXRjaCA9IGF4LnBsb3Rfc3VyZmFjZShYLCBZLCBaLCBjb2xvcj0nbGlnaHRibHVlJywgYWxwaGE9MC4yLCBlZGdlY29sb3I9J25vbmUnKQoKICAgICMgTW92ZSB0aGUgZ2FsYXhpZXMgKGV4Y2VwdCB0aGUgaG9tZSBnYWxheHkpIHdpdGggdGhlIGV4cGFuc2lvbgogICAgbmV3X3Bvc2l0aW9ucyA9IGdhbGF4eV9wb3NpdGlvbnMgKiAoMSArIGZyYW1lIC8gMTAwKQogICAgZ2FsYXhpZXNfc2NhdHRlci5fb2Zmc2V0czNkID0gKG5ld19wb3NpdGlvbnNbOiwgMF0sIG5ld19wb3NpdGlvbnNbOiwgMV0sIG5ld19wb3NpdGlvbnNbOiwgMl0pCiAgICAKICAgICMgVXBkYXRlIHRoZSB0cmFjZXMgZm9yIGVhY2ggZ2FsYXh5CiAgICBmb3IgaSwgdHJhY2UgaW4gZW51bWVyYXRlKGdhbGF4eV90cmFjZXMpOgogICAgICAgIHRyYWNlLnNldF9kYXRhKFswLCBuZXdfcG9zaXRpb25zW2ksIDBdXSwgWzAsIG5ld19wb3NpdGlvbnNbaSwgMV1dKQogICAgICAgIHRyYWNlLnNldF8zZF9wcm9wZXJ0aWVzKFswLCBuZXdfcG9zaXRpb25zW2ksIDJdXSkKCiMgQ3JlYXRlIHRoZSBhbmltYXRpb24KYW5pbSA9IEZ1bmNBbmltYXRpb24oZmlnLCB1cGRhdGUsIGZyYW1lcz0xMDEsIGludGVydmFsPTUwKQoKIyBQYXVzZS9wbGF5IGJ1dHRvbiBjYWxsYmFjayBmdW5jdGlvbgppc19wYXVzZWQgPSBGYWxzZQoKZGVmIHRvZ2dsZV9wYXVzZShldmVudCk6CiAgICBnbG9iYWwgaXNfcGF1c2VkCiAgICBpZiBpc19wYXVzZWQ6CiAgICAgICAgYW5pbS5ldmVudF9zb3VyY2Uuc3RhcnQoKQogICAgICAgIGJ1dHRvbi5sYWJlbC5zZXRfdGV4dCgnUGF1c2UnKQogICAgZWxzZToKICAgICAgICBhbmltLmV2ZW50X3NvdXJjZS5zdG9wKCkKICAgICAgICBidXR0b24ubGFiZWwuc2V0X3RleHQoJ1BsYXknKQogICAgaXNfcGF1c2VkID0gbm90IGlzX3BhdXNlZAoKIyBBZGQgdGhlIHBhdXNlL3BsYXkgYnV0dG9uCmF4X2J1dHRvbiA9IHBsdC5heGVzKFswLjQsIDAuMDUsIDAuMiwgMC4wNzVdKSAgIyBQb3NpdGlvbiBvZiB0aGUgYnV0dG9uCmJ1dHRvbiA9IEJ1dHRvbihheF9idXR0b24sICdQYXVzZScpCmJ1dHRvbi5vbl9jbGlja2VkKHRvZ2dsZV9wYXVzZSkKCnBsdC5zaG93KCkK