Skip to content

Commit 1c6e4af

Browse files
committed
updated rapier address
1 parent ed68e2d commit 1c6e4af

8 files changed

Lines changed: 151 additions & 9 deletions

File tree

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -892,11 +892,12 @@ python examples/05_rapier_simulation.py
892892
PHYSICS_PROVIDER=analytic # or "rapier"
893893

894894
# Rapier service (only if using Rapier provider)
895-
# Option 1: Public service (recommended for getting started)
896-
RAPIER_SERVICE_URL=https://rapier.chukai.io
897-
898-
# Option 2: Local development
899-
# RAPIER_SERVICE_URL=http://localhost:9000
895+
# The default is automatically determined:
896+
# - On Fly.io: uses https://rapier.chukai.io (public service)
897+
# - Locally: uses http://localhost:9000
898+
#
899+
# Override with:
900+
RAPIER_SERVICE_URL=https://rapier.chukai.io # or http://localhost:9000
900901

901902
# Optional configuration
902903
RAPIER_TIMEOUT=30.0

fly.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ primary_region = 'lhr'
1313
PHYSICS_PROVIDER = "rapier"
1414

1515
# Rapier service configuration (when using Rapier provider)
16-
RAPIER_SERVICE_URL = "https://chuk-rapier-physics.fly.dev"
16+
# Uses custom domain instead of fly.dev URL
17+
RAPIER_SERVICE_URL = "https://rapier.chukai.io"
1718

1819
[http_service]
1920
internal_port = 8000

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "chuk-mcp-physics"
3-
version = "0.2"
3+
version = "0.2.1"
44
description = "MCP server for physics simulations and calculations using Rapier physics engine"
55
authors = [
66
{name = "Chris Hay", email = "chris@example.com"}

src/chuk_mcp_physics/config.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,15 @@ class RapierConfig:
124124
_rapier_yaml = _yaml_config.get("rapier", {})
125125

126126
# Rapier service URL
127+
# Default depends on environment:
128+
# - Fly.io deployment: use public service
129+
# - Local development: use localhost
130+
_default_service_url = (
131+
"https://rapier.chukai.io" if os.getenv("FLY_APP_NAME") else "http://localhost:9000"
132+
)
127133
SERVICE_URL = os.getenv(
128134
"RAPIER_SERVICE_URL",
129-
_rapier_yaml.get("service_url", "http://localhost:9000"),
135+
_rapier_yaml.get("service_url", _default_service_url),
130136
)
131137

132138
# Request timeout in seconds

src/chuk_mcp_physics/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ class RigidBodyDefinition(BaseModel):
111111
)
112112
radius: Optional[float] = Field(None, description="Radius for sphere/capsule/cylinder")
113113
half_height: Optional[float] = Field(None, description="Half-height for capsule/cylinder")
114+
normal: Optional[list[float]] = Field(
115+
None, description="Normal vector [x, y, z] for plane (default [0, 1, 0])"
116+
)
117+
offset: Optional[float] = Field(None, description="Offset along normal for plane (default 0.0)")
114118

115119
# Physics properties
116120
mass: float = Field(default=1.0, description="Mass in kilograms", gt=0.0)

src/chuk_mcp_physics/server.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,8 @@ async def add_rigid_body(
404404
size: Optional[list[float]] = None,
405405
radius: Optional[float] = None,
406406
half_height: Optional[float] = None,
407+
normal: Optional[list[float]] = None,
408+
offset: Optional[float] = None,
407409
position: Optional[list[float]] = None,
408410
orientation: Optional[list[float]] = None,
409411
velocity: Optional[list[float]] = None,
@@ -429,6 +431,8 @@ async def add_rigid_body(
429431
size: For box: [width, height, depth]. For other shapes, use radius/half_height
430432
radius: Radius for sphere/capsule/cylinder
431433
half_height: Half-height for capsule/cylinder
434+
normal: Normal vector [x, y, z] for plane shape. Default [0, 1, 0] (upward)
435+
offset: Offset along normal for plane. Default 0.0
432436
position: Initial position [x, y, z]. Default [0, 0, 0]
433437
orientation: Initial orientation quaternion [x, y, z, w]. Default [0, 0, 0, 1] (identity)
434438
velocity: Initial linear velocity [x, y, z]. Default [0, 0, 0]
@@ -475,6 +479,8 @@ async def add_rigid_body(
475479
size=size,
476480
radius=radius,
477481
half_height=half_height,
482+
normal=normal,
483+
offset=offset,
478484
position=position or [0.0, 0.0, 0.0],
479485
orientation=orientation or [0.0, 0.0, 0.0, 1.0],
480486
velocity=velocity or [0.0, 0.0, 0.0],

test_bouncing_ball.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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())

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)