|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Test bouncing ball simulation to answer: How many bounces before stopping at 5mm? |
| 3 | +
|
| 4 | +Given: |
| 5 | +- Ball: 10cm diameter (0.1m), 1kg mass |
| 6 | +- Drop height: 10 meters |
| 7 | +- Stop threshold: 5mm (0.005m) |
| 8 | +- Coefficient of restitution: assume 0.6 (typical for a medicine ball) |
| 9 | +""" |
| 10 | + |
| 11 | +import asyncio |
| 12 | +import os |
| 13 | +from chuk_mcp_physics.server import ( |
| 14 | + create_simulation, |
| 15 | + add_rigid_body, |
| 16 | + record_trajectory, |
| 17 | + destroy_simulation, |
| 18 | +) |
| 19 | + |
| 20 | +# Use public Rapier service |
| 21 | +os.environ["RAPIER_SERVICE_URL"] = "https://rapier.chukai.io" |
| 22 | + |
| 23 | + |
| 24 | +async def test_bouncing_ball(): |
| 25 | + """Simulate a bouncing ball and count bounces.""" |
| 26 | + print("Creating simulation...") |
| 27 | + sim = await create_simulation(gravity_y=-9.81, dt=0.016) |
| 28 | + print(f"✓ Simulation created: {sim.sim_id}") |
| 29 | + |
| 30 | + try: |
| 31 | + # Add ground plane |
| 32 | + print("\nAdding ground plane...") |
| 33 | + await add_rigid_body( |
| 34 | + sim_id=sim.sim_id, |
| 35 | + body_id="ground", |
| 36 | + body_type="static", |
| 37 | + shape="plane", |
| 38 | + normal=[0.0, 1.0, 0.0], |
| 39 | + offset=0.0, |
| 40 | + ) |
| 41 | + print("✓ Ground plane added") |
| 42 | + |
| 43 | + # Add ball (10cm diameter = 0.05m radius, 1kg, dropped from 10m) |
| 44 | + print("\nAdding ball (0.1m diameter, 1kg, dropped from 10m)...") |
| 45 | + await add_rigid_body( |
| 46 | + sim_id=sim.sim_id, |
| 47 | + body_id="ball", |
| 48 | + body_type="dynamic", |
| 49 | + shape="sphere", |
| 50 | + radius=0.05, # 5cm radius = 10cm diameter |
| 51 | + mass=1.0, |
| 52 | + position=[0.0, 10.0, 0.0], |
| 53 | + velocity=[0.0, 0.0, 0.0], |
| 54 | + restitution=0.6, # Medicine ball-like bounciness |
| 55 | + friction=0.5, |
| 56 | + ) |
| 57 | + print("✓ Ball added") |
| 58 | + |
| 59 | + # Record trajectory for 15 seconds (should be plenty for all bounces) |
| 60 | + print("\nRecording trajectory for 15 seconds...") |
| 61 | + steps = int(15.0 / 0.016) # 15 seconds at 60 FPS |
| 62 | + trajectory = await record_trajectory( |
| 63 | + sim_id=sim.sim_id, body_id="ball", steps=steps |
| 64 | + ) |
| 65 | + print(f"✓ Recorded {trajectory.meta.num_frames} frames") |
| 66 | + |
| 67 | + # Analyze bounces |
| 68 | + print("\nAnalyzing bounces...") |
| 69 | + bounce_count = 0 |
| 70 | + max_heights = [] |
| 71 | + last_direction = 0 # -1 = down, 1 = up |
| 72 | + last_height = trajectory.frames[0].position[1] |
| 73 | + |
| 74 | + for i, frame in enumerate(trajectory.frames): |
| 75 | + height = frame.position[1] |
| 76 | + velocity_y = frame.velocity[1] if frame.velocity else 0.0 |
| 77 | + |
| 78 | + # Detect direction change (bounce) |
| 79 | + current_direction = 1 if velocity_y > 0 else -1 |
| 80 | + |
| 81 | + # Bounce detected: was going down, now going up, and close to ground |
| 82 | + if last_direction < 0 and current_direction > 0 and height < 0.1: |
| 83 | + bounce_count += 1 |
| 84 | + # Find peak after this bounce |
| 85 | + peak_height = height |
| 86 | + for j in range(i, min(i + 100, len(trajectory.frames))): |
| 87 | + if trajectory.frames[j].position[1] > peak_height: |
| 88 | + peak_height = trajectory.frames[j].position[1] |
| 89 | + max_heights.append(peak_height) |
| 90 | + |
| 91 | + print(f" Bounce #{bounce_count}: peak height = {peak_height:.4f}m") |
| 92 | + |
| 93 | + # Stop if peak height is below 5mm |
| 94 | + if peak_height < 0.005: |
| 95 | + print( |
| 96 | + f"\n✓ Ball stopped bouncing (peak < 5mm) after {bounce_count} bounces" |
| 97 | + ) |
| 98 | + break |
| 99 | + |
| 100 | + last_direction = current_direction |
| 101 | + last_height = height |
| 102 | + |
| 103 | + print(f"\n{'='*60}") |
| 104 | + print(f"RESULTS:") |
| 105 | + print(f"{'='*60}") |
| 106 | + print(f"Initial drop height: 10.0 m") |
| 107 | + print(f"Ball properties: 10cm diameter, 1kg, restitution=0.6") |
| 108 | + print(f"Stop threshold: 5mm (0.005m)") |
| 109 | + print(f"Total bounces before stopping: {bounce_count}") |
| 110 | + print(f"\nBounce heights:") |
| 111 | + for i, h in enumerate(max_heights[:10], 1): # Show first 10 |
| 112 | + print(f" Bounce {i}: {h*1000:.1f}mm") |
| 113 | + if len(max_heights) > 10: |
| 114 | + print(f" ... ({len(max_heights) - 10} more)") |
| 115 | + |
| 116 | + finally: |
| 117 | + # Cleanup |
| 118 | + print(f"\nCleaning up simulation {sim.sim_id}...") |
| 119 | + await destroy_simulation(sim.sim_id) |
| 120 | + print("✓ Simulation destroyed") |
| 121 | + |
| 122 | + |
| 123 | +if __name__ == "__main__": |
| 124 | + asyncio.run(test_bouncing_ball()) |
0 commit comments