Spaces:
Sleeping
Sleeping
| # /// script | |
| # requires-python = ">=3.13" | |
| # dependencies = [ | |
| # "marimo", | |
| # "matplotlib==3.10.1", | |
| # "numpy==2.2.3", | |
| # ] | |
| # /// | |
| import marimo | |
| __generated_with = "0.11.20" | |
| app = marimo.App() | |
| def _(): | |
| import marimo as mo | |
| return (mo,) | |
| def _(mo): | |
| mo.md( | |
| r""" | |
| # Finding $\pi$ in colliding blocks | |
| One of the remarkable things about mathematical constants like $\pi$ is how frequently they arise in nature, in the most surprising of places. | |
| Inspired by 3Blue1Brown, this [marimo notebook](https://github.com/marimo-team/marimo) shows how the number of collisions incurred in a particular system involving two blocks converges to the digits in $\pi$. | |
| **Tip!**: Use the menu in the top right to reveal the notebook's code. | |
| """ | |
| ) | |
| return | |
| def _(mo): | |
| slider = mo.ui.slider(start=0, stop=3, value=3, show_value=True) | |
| return (slider,) | |
| def _(mo, slider): | |
| mo.md("## Simulate!") | |
| return | |
| def _(mo, slider): | |
| mo.md(f"Use this slider to control the weight of the heavier block: {slider}") | |
| return | |
| def _(mo, slider): | |
| mo.md(rf"The heavier block weighs **$100^{{ {slider.value} }}$** kg.") | |
| return | |
| def _(mo): | |
| run_button = mo.ui.run_button(label="Run simulation!") | |
| run_button.right() | |
| return (run_button,) | |
| def _(run_button, simulate_collisions, slider): | |
| if run_button.value: | |
| mass_ratio = 100**slider.value | |
| _, ani, collisions = simulate_collisions( | |
| mass_ratio, total_time=15, dt=0.001 | |
| ) | |
| return ani, collisions, mass_ratio | |
| def _(ani, mo, run_button): | |
| video = None | |
| if run_button.value: | |
| with mo.status.spinner(title="Rendering collision video ..."): | |
| video = mo.Html(ani.to_html5_video()) | |
| video | |
| return (video,) | |
| def _(mo): | |
| mo.md( | |
| r""" | |
| ## The 3Blue1Brown video | |
| If you haven't seen it, definitely check out the video that inspired this notebook: | |
| """ | |
| ) | |
| return | |
| def _(mo): | |
| mo.accordion( | |
| { | |
| "🎥 Watch the video": mo.Html( | |
| '<iframe width="700" height="400" src="https://www.youtube.com/embed/6dTyOl1fmDo?si=xl9v6Y8x2e3r3A9I" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>' | |
| ) | |
| }) | |
| return | |
| def _(): | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| import matplotlib.animation as animation | |
| from matplotlib.patches import Rectangle | |
| return Rectangle, animation, np, plt | |
| def _(): | |
| class Block: | |
| def __init__(self, mass, velocity, position, size=1.0): | |
| self.mass = mass | |
| self.velocity = velocity | |
| self.position = position | |
| self.size = size | |
| def update(self, dt): | |
| self.position += self.velocity * dt | |
| def collide(self, other): | |
| # Calculate velocities after elastic collision | |
| m1, m2 = self.mass, other.mass | |
| v1, v2 = self.velocity, other.velocity | |
| new_v1 = (m1 - m2) / (m1 + m2) * v1 + (2 * m2) / (m1 + m2) * v2 | |
| new_v2 = (2 * m1) / (m1 + m2) * v1 + (m2 - m1) / (m1 + m2) * v2 | |
| self.velocity = new_v1 | |
| other.velocity = new_v2 | |
| return 1 # Return 1 collision | |
| return (Block,) | |
| def check_collisions(): | |
| def check_collisions(small_block, big_block, wall_pos=0): | |
| collisions = 0 | |
| # Check for collision between blocks | |
| if small_block.position + small_block.size > big_block.position: | |
| small_block.position = big_block.position - small_block.size | |
| collisions += small_block.collide(big_block) | |
| # Check for collision with the wall | |
| if small_block.position < wall_pos: | |
| small_block.position = wall_pos | |
| small_block.velocity *= -1 | |
| collisions += 1 | |
| return collisions | |
| return (check_collisions,) | |
| def _(Block, check_collisions, create_animation): | |
| def simulate_collisions(mass_ratio, total_time=15, dt=0.001, animate=True): | |
| # Initialize blocks | |
| small_block = Block(mass=1, velocity=0, position=2) | |
| big_block = Block(mass=mass_ratio, velocity=-0.5, position=4) | |
| # Simulation variables | |
| time = 0 | |
| collision_count = 0 | |
| # For animation | |
| times = [] | |
| small_positions = [] | |
| big_positions = [] | |
| collision_counts = [] | |
| # Run simulation | |
| while time < total_time: | |
| # Update positions | |
| small_block.update(dt) | |
| big_block.update(dt) | |
| # Check for and handle collisions | |
| new_collisions = check_collisions(small_block, big_block) | |
| collision_count += new_collisions | |
| # Store data for animation | |
| times.append(time) | |
| small_positions.append(small_block.position) | |
| big_positions.append(big_block.position) | |
| collision_counts.append(collision_count) | |
| time += dt | |
| print(f"Mass ratio: {mass_ratio}, Total collisions: {collision_count}") | |
| if animate: | |
| axis, ani = create_animation( | |
| times, small_positions, big_positions, collision_counts, mass_ratio | |
| ) | |
| else: | |
| axis, ani = None | |
| return axis, ani, collision_count | |
| return (simulate_collisions,) | |
| def _(Rectangle, animation, plt): | |
| def create_animation( | |
| times, small_positions, big_positions, collision_counts, mass_ratio | |
| ): | |
| fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) | |
| # Setup for blocks visualization | |
| ax1.set_xlim(-1, 10) | |
| ax1.set_ylim(-1, 2) | |
| ax1.set_xlabel("Position") | |
| ax1.set_title(f"Block Collisions (Mass Ratio = {mass_ratio})") | |
| wall = plt.Line2D([0, 0], [-1, 2], color="black", linewidth=3) | |
| ax1.add_line(wall) | |
| small_block = Rectangle((small_positions[0], 0), 1, 1, color="blue") | |
| big_block = Rectangle((big_positions[0], 0), 1, 1, color="red") | |
| ax1.add_patch(small_block) | |
| ax1.add_patch(big_block) | |
| # Add weight labels for each block | |
| small_label = ax1.text( | |
| small_positions[0] + 0.5, | |
| 1.2, | |
| f"{1}kg", | |
| ha="center", | |
| va="center", | |
| color="blue", | |
| fontweight="bold", | |
| ) | |
| big_label = ax1.text( | |
| big_positions[0] + 0.5, | |
| 1.2, | |
| f"{mass_ratio}kg", | |
| ha="center", | |
| va="center", | |
| color="red", | |
| fontweight="bold", | |
| ) | |
| # Setup for collision count | |
| ax2.set_xlim(0, times[-1]) | |
| # ax2.set_ylim(0, collision_counts[-1] * 1.1) | |
| ax2.set_ylim(0, collision_counts[-1] * 1.1) | |
| ax2.set_xlabel("Time") | |
| ax2.set_ylabel("# Collisions:") | |
| ax2.set_yscale("symlog") | |
| (collision_line,) = ax2.plot([], [], "g-") | |
| # Add text for collision count | |
| collision_text = ax2.text( | |
| 0.02, 0.9, "", transform=ax2.transAxes, fontsize="x-large" | |
| ) | |
| def init(): | |
| small_block.set_xy((small_positions[0], 0)) | |
| big_block.set_xy((big_positions[0], 0)) | |
| small_label.set_position((small_positions[0] + 0.5, 1.2)) | |
| big_label.set_position((big_positions[0] + 0.5, 1.2)) | |
| collision_line.set_data([], []) | |
| collision_text.set_text("") | |
| return small_block, big_block, collision_line, collision_text | |
| frame_step = 300 | |
| def animate(i): | |
| # Speed up animation but ensure we reach the final frame | |
| frame_index = min(i * frame_step, len(times) - 1) | |
| small_block.set_xy((small_positions[frame_index], 0)) | |
| big_block.set_xy((big_positions[frame_index], 0)) | |
| # Update the weight labels to follow the blocks | |
| small_label.set_position((small_positions[frame_index] + 0.5, 1.2)) | |
| big_label.set_position((big_positions[frame_index] + 0.5, 1.2)) | |
| # Show data up to the current frame | |
| collision_line.set_data( | |
| times[: frame_index + 1], collision_counts[: frame_index + 1] | |
| ) | |
| # For the last frame, show the final collision count | |
| if frame_index >= len(times) - 1: | |
| collision_text.set_text( | |
| f"# Collisions: {collision_counts[-1]}" | |
| ) | |
| else: | |
| collision_text.set_text( | |
| f"# Collisions: {collision_counts[frame_index]}" | |
| ) | |
| return ( | |
| small_block, | |
| big_block, | |
| small_label, | |
| big_label, | |
| collision_line, | |
| collision_text, | |
| ) | |
| plt.tight_layout() | |
| frames = max(1, len(times) // frame_step) # Ensure at least 1 frame | |
| ani = animation.FuncAnimation( | |
| fig, | |
| animate, | |
| frames=frames + 1, # +1 to ensure we reach the end | |
| init_func=init, | |
| blit=True, | |
| interval=30, | |
| ) | |
| plt.tight_layout() | |
| return plt.gca(), ani | |
| # Uncomment to save animation | |
| # ani.save('pi_collisions.mp4', writer='ffmpeg', fps=30) | |
| return (create_animation,) | |
| if __name__ == "__main__": | |
| app.run() |