guohanghui commited on
Commit
e5d428b
·
verified ·
1 Parent(s): 074bb4c

Update rebound/mcp_output/mcp_plugin/mcp_service.py

Browse files
rebound/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -1,138 +1,1137 @@
 
 
 
 
 
 
 
 
1
  from fastmcp import FastMCP
 
 
 
 
 
 
 
 
 
 
2
 
3
  # Create the FastMCP service application
4
  mcp = FastMCP("rebound_service")
5
 
6
- @mcp.tool(name="initialize_simulation", description="Initialize a REBOUND simulation")
7
- def initialize_simulation(particles: list, integrator: str = "ias15") -> dict:
 
 
 
 
 
 
 
 
 
 
8
  """
9
- Initialize a REBOUND simulation with particles and an integrator.
10
 
11
  Parameters:
12
- - particles: A list of particle dictionaries with mass, position, and velocity.
13
- - integrator: The integrator to use (default: "ias15").
 
 
 
 
14
 
15
  Returns:
16
- - dict: Simulation object and status.
17
  """
 
 
 
18
  try:
19
- import rebound
20
-
21
  sim = rebound.Simulation()
22
  sim.integrator = integrator
23
- for p in particles:
24
- sim.add(m=p['mass'], x=p['x'], y=p['y'], z=p['z'], vx=p['vx'], vy=p['vy'], vz=p['vz'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  return {
27
  "success": True,
28
- "simulation": sim.serialize()
 
 
 
 
29
  }
30
  except Exception as e:
31
  return {"success": False, "error": str(e)}
32
 
33
- @mcp.tool(name="integrate_simulation", description="Integrate a REBOUND simulation")
34
- def integrate_simulation(simulation: dict, time: float) -> dict:
 
35
  """
36
- Integrate a REBOUND simulation to a specific time.
37
 
38
  Parameters:
39
- - simulation: Serialized REBOUND simulation object.
40
- - time: Time to integrate the simulation to.
 
41
 
42
  Returns:
43
- - dict: Updated simulation state.
44
  """
 
 
 
 
 
 
45
  try:
46
- import rebound
47
 
48
- sim = rebound.Simulation.deserialize(simulation)
49
- sim.integrate(time)
 
 
50
 
51
  return {
52
  "success": True,
53
- "simulation": sim.serialize()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  }
55
  except Exception as e:
56
  return {"success": False, "error": str(e)}
57
 
58
- @mcp.tool(name="add_particle", description="Add a particle to a REBOUND simulation")
59
- def add_particle(simulation: dict, particle: dict) -> dict:
 
60
  """
61
- Add a particle to a REBOUND simulation.
62
 
63
  Parameters:
64
- - simulation: Serialized REBOUND simulation object.
65
- - particle: A dictionary with mass, position, and velocity of the particle.
66
 
67
  Returns:
68
- - dict: Updated simulation state.
69
  """
 
 
 
 
 
 
70
  try:
71
- import rebound
 
72
 
73
- sim = rebound.Simulation.deserialize(simulation)
74
- sim.add(m=particle['mass'], x=particle['x'], y=particle['y'], z=particle['z'], vx=particle['vx'], vy=particle['vy'], vz=particle['vz'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
  return {
77
  "success": True,
78
- "simulation": sim.serialize()
 
 
 
 
 
 
 
79
  }
80
  except Exception as e:
81
  return {"success": False, "error": str(e)}
82
 
83
- @mcp.tool(name="analyze_orbital_elements", description="Analyze orbital elements of particles in a REBOUND simulation")
84
- def analyze_orbital_elements(simulation: dict) -> dict:
 
85
  """
86
- Analyze the orbital elements of particles in a REBOUND simulation.
87
 
88
  Parameters:
89
- - simulation: Serialized REBOUND simulation object.
90
 
91
  Returns:
92
- - dict: Orbital elements of particles.
93
  """
 
 
 
 
 
 
94
  try:
95
- import rebound
 
96
 
97
- sim = rebound.Simulation.deserialize(simulation)
98
- elements = [sim.particles[i].calculate_orbit() for i in range(len(sim.particles))]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
  return {
101
  "success": True,
102
- "orbital_elements": [e.__dict__ for e in elements]
 
103
  }
104
  except Exception as e:
105
  return {"success": False, "error": str(e)}
106
 
107
- @mcp.tool(name="visualize_simulation", description="Visualize a REBOUND simulation")
108
- def visualize_simulation(simulation: dict) -> dict:
 
109
  """
110
- Visualize a REBOUND simulation.
111
 
112
  Parameters:
113
- - simulation: Serialized REBOUND simulation object.
 
114
 
115
  Returns:
116
- - dict: Visualization status.
117
  """
 
 
 
 
 
 
118
  try:
119
- import rebound
120
 
121
- sim = rebound.Simulation.deserialize(simulation)
122
- sim.display()
 
 
123
 
124
  return {
125
  "success": True,
126
- "message": "Simulation visualization displayed successfully."
 
 
 
127
  }
128
  except Exception as e:
129
  return {"success": False, "error": str(e)}
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  def create_app() -> FastMCP:
132
  """
133
- Create and return the FastMCP application instance.
134
 
135
  Returns:
136
- - FastMCP: The FastMCP application instance.
137
  """
138
- return mcp
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # Add the local source directory to sys.path
5
+ source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
6
+ if source_path not in sys.path:
7
+ sys.path.insert(0, source_path)
8
+
9
  from fastmcp import FastMCP
10
+ import json
11
+ import math
12
+
13
+ # Check if rebound is available
14
+ try:
15
+ import rebound
16
+ from rebound import tools, data
17
+ REBOUND_AVAILABLE = True
18
+ except ImportError:
19
+ REBOUND_AVAILABLE = False
20
 
21
  # Create the FastMCP service application
22
  mcp = FastMCP("rebound_service")
23
 
24
+ # Store simulations in memory (keyed by simulation ID)
25
+ _simulations = {}
26
+ _sim_counter = 0
27
+
28
+ # ============================================================================
29
+ # Core Simulation Tools
30
+ # ============================================================================
31
+
32
+ @mcp.tool()
33
+ def create_simulation(integrator: str = "ias15", dt: float = 0.01,
34
+ gravity: str = "compensated", collision: str = "none",
35
+ boundary: str = "none", G: float = 1.0) -> dict:
36
  """
37
+ Create a new N-body simulation instance.
38
 
39
  Parameters:
40
+ - integrator: Integrator ('ias15', 'whfast', 'mercurius', 'leapfrog', 'sei', 'eos', 'bs', 'janus', 'trace')
41
+ - dt: Timestep for fixed timestep integrators
42
+ - gravity: Gravity solver ('compensated', 'basic', 'none', 'tree')
43
+ - collision: Collision detection ('none', 'direct', 'tree')
44
+ - boundary: Boundary conditions ('none', 'open', 'periodic', 'shear')
45
+ - G: Gravitational constant (default 1.0)
46
 
47
  Returns:
48
+ - dict: Simulation ID and configuration
49
  """
50
+ if not REBOUND_AVAILABLE:
51
+ return {"success": False, "error": "REBOUND not installed"}
52
+
53
  try:
54
+ global _sim_counter
 
55
  sim = rebound.Simulation()
56
  sim.integrator = integrator
57
+ sim.dt = dt
58
+ sim.gravity = gravity
59
+ sim.collision = collision
60
+ sim.boundary = boundary
61
+ sim.G = G
62
+
63
+ sim_id = f"sim_{_sim_counter}"
64
+ _simulations[sim_id] = sim
65
+ _sim_counter += 1
66
+
67
+ return {
68
+ "success": True,
69
+ "sim_id": sim_id,
70
+ "integrator": integrator,
71
+ "dt": dt,
72
+ "gravity": gravity,
73
+ "collision": collision,
74
+ "boundary": boundary,
75
+ "G": G,
76
+ "n_particles": sim.N
77
+ }
78
+ except Exception as e:
79
+ return {"success": False, "error": str(e)}
80
+
81
+
82
+ @mcp.tool()
83
+ def add_particle(sim_id: str, m: float = 0.0, x: float = 0.0, y: float = 0.0, z: float = 0.0,
84
+ vx: float = 0.0, vy: float = 0.0, vz: float = 0.0, r: float = 0.0,
85
+ hash_value: str = "") -> dict:
86
+ """
87
+ Add a particle using Cartesian coordinates.
88
+
89
+ Parameters:
90
+ - sim_id: Simulation ID
91
+ - m: Mass
92
+ - x, y, z: Position
93
+ - vx, vy, vz: Velocity
94
+ - r: Particle radius (for collision detection)
95
+ - hash_value: Optional hash identifier for particle
96
+
97
+ Returns:
98
+ - dict: Particle info
99
+ """
100
+ if not REBOUND_AVAILABLE:
101
+ return {"success": False, "error": "REBOUND not installed"}
102
+
103
+ if sim_id not in _simulations:
104
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
105
+
106
+ try:
107
+ sim = _simulations[sim_id]
108
+
109
+ if hash_value:
110
+ sim.add(m=m, x=x, y=y, z=z, vx=vx, vy=vy, vz=vz, r=r, hash=hash_value)
111
+ else:
112
+ sim.add(m=m, x=x, y=y, z=z, vx=vx, vy=vy, vz=vz, r=r)
113
+
114
+ particle_idx = sim.N - 1
115
+ p = sim.particles[particle_idx]
116
+
117
+ return {
118
+ "success": True,
119
+ "sim_id": sim_id,
120
+ "n_particles": sim.N,
121
+ "particle_index": particle_idx,
122
+ "particle": {
123
+ "m": p.m, "r": p.r,
124
+ "x": p.x, "y": p.y, "z": p.z,
125
+ "vx": p.vx, "vy": p.vy, "vz": p.vz,
126
+ "hash": str(p.hash)
127
+ }
128
+ }
129
+ except Exception as e:
130
+ return {"success": False, "error": str(e)}
131
+
132
+
133
+ @mcp.tool()
134
+ def add_particle_with_orbital_elements(sim_id: str, primary_index: int = 0,
135
+ m: float = 0.0, a: float = 1.0, e: float = 0.0,
136
+ inc: float = 0.0, Omega: float = 0.0,
137
+ omega: float = 0.0, f: float = 0.0, r: float = 0.0) -> dict:
138
+ """
139
+ Add a particle using orbital elements relative to a primary body.
140
+
141
+ Parameters:
142
+ - sim_id: Simulation ID
143
+ - primary_index: Index of primary body (usually 0 for central star)
144
+ - m: Mass
145
+ - a: Semi-major axis
146
+ - e: Eccentricity
147
+ - inc: Inclination (radians)
148
+ - Omega: Longitude of ascending node (radians)
149
+ - omega: Argument of periapsis (radians)
150
+ - f: True anomaly (radians)
151
+ - r: Particle radius
152
+
153
+ Returns:
154
+ - dict: Particle info
155
+ """
156
+ if not REBOUND_AVAILABLE:
157
+ return {"success": False, "error": "REBOUND not installed"}
158
+
159
+ if sim_id not in _simulations:
160
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
161
+
162
+ try:
163
+ sim = _simulations[sim_id]
164
+
165
+ if primary_index >= sim.N:
166
+ return {"success": False, "error": f"Primary index {primary_index} out of range"}
167
+
168
+ primary = sim.particles[primary_index]
169
+ sim.add(m=m, a=a, e=e, inc=inc, Omega=Omega, omega=omega, f=f, r=r, primary=primary)
170
+
171
+ particle_idx = sim.N - 1
172
+ p = sim.particles[particle_idx]
173
+
174
+ return {
175
+ "success": True,
176
+ "sim_id": sim_id,
177
+ "n_particles": sim.N,
178
+ "particle_index": particle_idx,
179
+ "particle": {
180
+ "m": p.m, "r": p.r,
181
+ "x": p.x, "y": p.y, "z": p.z,
182
+ "vx": p.vx, "vy": p.vy, "vz": p.vz
183
+ }
184
+ }
185
+ except Exception as e:
186
+ return {"success": False, "error": str(e)}
187
+
188
+
189
+ @mcp.tool()
190
+ def integrate(sim_id: str, tmax: float, exact_finish_time: bool = True) -> dict:
191
+ """
192
+ Integrate the simulation forward in time.
193
+
194
+ Parameters:
195
+ - sim_id: Simulation ID
196
+ - tmax: Time to integrate to
197
+ - exact_finish_time: Whether to finish exactly at tmax
198
+
199
+ Returns:
200
+ - dict: Simulation state after integration
201
+ """
202
+ if not REBOUND_AVAILABLE:
203
+ return {"success": False, "error": "REBOUND not installed"}
204
+
205
+ if sim_id not in _simulations:
206
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
207
+
208
+ try:
209
+ sim = _simulations[sim_id]
210
+ sim.integrate(tmax, exact_finish_time=1 if exact_finish_time else 0)
211
+
212
+ particles = []
213
+ for i, p in enumerate(sim.particles):
214
+ particles.append({
215
+ "index": i,
216
+ "m": p.m,
217
+ "x": p.x, "y": p.y, "z": p.z,
218
+ "vx": p.vx, "vy": p.vy, "vz": p.vz
219
+ })
220
+
221
+ return {
222
+ "success": True,
223
+ "sim_id": sim_id,
224
+ "t": sim.t,
225
+ "dt": sim.dt,
226
+ "n_particles": sim.N,
227
+ "particles": particles
228
+ }
229
+ except Exception as e:
230
+ return {"success": False, "error": str(e)}
231
+
232
+
233
+ @mcp.tool()
234
+ def step(sim_id: str) -> dict:
235
+ """
236
+ Perform a single integration step.
237
+
238
+ Parameters:
239
+ - sim_id: Simulation ID
240
+
241
+ Returns:
242
+ - dict: Simulation state after step
243
+ """
244
+ if not REBOUND_AVAILABLE:
245
+ return {"success": False, "error": "REBOUND not installed"}
246
+
247
+ if sim_id not in _simulations:
248
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
249
+
250
+ try:
251
+ sim = _simulations[sim_id]
252
+ sim.step()
253
+
254
+ return {
255
+ "success": True,
256
+ "sim_id": sim_id,
257
+ "t": sim.t,
258
+ "dt": sim.dt,
259
+ "n_particles": sim.N
260
+ }
261
+ except Exception as e:
262
+ return {"success": False, "error": str(e)}
263
+
264
+
265
+ @mcp.tool()
266
+ def remove_particle(sim_id: str, index: int = 0, keep_sorted: bool = True) -> dict:
267
+ """
268
+ Remove a particle from the simulation.
269
+
270
+ Parameters:
271
+ - sim_id: Simulation ID
272
+ - index: Particle index to remove
273
+ - keep_sorted: Whether to keep particle array sorted
274
+
275
+ Returns:
276
+ - dict: Updated simulation state
277
+ """
278
+ if not REBOUND_AVAILABLE:
279
+ return {"success": False, "error": "REBOUND not installed"}
280
+
281
+ if sim_id not in _simulations:
282
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
283
+
284
+ try:
285
+ sim = _simulations[sim_id]
286
+
287
+ if index >= sim.N:
288
+ return {"success": False, "error": f"Particle index {index} out of range"}
289
+
290
+ sim.remove(index=index, keep_sorted=keep_sorted)
291
+
292
+ return {
293
+ "success": True,
294
+ "sim_id": sim_id,
295
+ "n_particles": sim.N,
296
+ "message": f"Particle {index} removed"
297
+ }
298
+ except Exception as e:
299
+ return {"success": False, "error": str(e)}
300
+
301
+
302
+ @mcp.tool()
303
+ def get_particles(sim_id: str) -> dict:
304
+ """
305
+ Get all particles in the simulation.
306
+
307
+ Parameters:
308
+ - sim_id: Simulation ID
309
+
310
+ Returns:
311
+ - dict: List of all particles with full state information
312
+ """
313
+ if not REBOUND_AVAILABLE:
314
+ return {"success": False, "error": "REBOUND not installed"}
315
+
316
+ if sim_id not in _simulations:
317
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
318
+
319
+ try:
320
+ sim = _simulations[sim_id]
321
+
322
+ particles = []
323
+ for i, p in enumerate(sim.particles):
324
+ particles.append({
325
+ "index": i,
326
+ "hash": str(p.hash),
327
+ "m": p.m,
328
+ "r": p.r,
329
+ "x": p.x, "y": p.y, "z": p.z,
330
+ "vx": p.vx, "vy": p.vy, "vz": p.vz,
331
+ "ax": p.ax, "ay": p.ay, "az": p.az,
332
+ "last_collision": p.last_collision
333
+ })
334
 
335
  return {
336
  "success": True,
337
+ "sim_id": sim_id,
338
+ "t": sim.t,
339
+ "n_particles": sim.N,
340
+ "n_active": sim.N_active,
341
+ "particles": particles
342
  }
343
  except Exception as e:
344
  return {"success": False, "error": str(e)}
345
 
346
+
347
+ @mcp.tool()
348
+ def get_orbital_elements(sim_id: str, particle_index: int, primary_index: int = 0) -> dict:
349
  """
350
+ Calculate orbital elements for a particle.
351
 
352
  Parameters:
353
+ - sim_id: Simulation ID
354
+ - particle_index: Index of particle
355
+ - primary_index: Index of primary body (default 0)
356
 
357
  Returns:
358
+ - dict: Orbital elements
359
  """
360
+ if not REBOUND_AVAILABLE:
361
+ return {"success": False, "error": "REBOUND not installed"}
362
+
363
+ if sim_id not in _simulations:
364
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
365
+
366
  try:
367
+ sim = _simulations[sim_id]
368
 
369
+ if particle_index >= sim.N or primary_index >= sim.N:
370
+ return {"success": False, "error": "Particle index out of range"}
371
+
372
+ orbit = sim.particles[particle_index].calculate_orbit(primary=sim.particles[primary_index])
373
 
374
  return {
375
  "success": True,
376
+ "sim_id": sim_id,
377
+ "particle_index": particle_index,
378
+ "primary_index": primary_index,
379
+ "t": sim.t,
380
+ "orbital_elements": {
381
+ "a": orbit.a, # Semi-major axis
382
+ "e": orbit.e, # Eccentricity
383
+ "inc": orbit.inc, # Inclination
384
+ "Omega": orbit.Omega, # Longitude of ascending node
385
+ "omega": orbit.omega, # Argument of periapsis
386
+ "f": orbit.f, # True anomaly
387
+ "M": orbit.M, # Mean anomaly
388
+ "l": orbit.l, # Mean longitude
389
+ "theta": orbit.theta, # True longitude
390
+ "T": orbit.T, # Periapsis time
391
+ "n": orbit.n, # Mean motion
392
+ "P": orbit.P, # Orbital period
393
+ "rp": orbit.rp, # Periapsis distance
394
+ "ap": getattr(orbit, 'ap', None) # Apoapsis distance
395
+ }
396
  }
397
  except Exception as e:
398
  return {"success": False, "error": str(e)}
399
 
400
+
401
+ @mcp.tool()
402
+ def calculate_energy(sim_id: str) -> dict:
403
  """
404
+ Calculate total energy of the system.
405
 
406
  Parameters:
407
+ - sim_id: Simulation ID
 
408
 
409
  Returns:
410
+ - dict: Total energy
411
  """
412
+ if not REBOUND_AVAILABLE:
413
+ return {"success": False, "error": "REBOUND not installed"}
414
+
415
+ if sim_id not in _simulations:
416
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
417
+
418
  try:
419
+ sim = _simulations[sim_id]
420
+ energy = sim.calculate_energy()
421
 
422
+ return {
423
+ "success": True,
424
+ "sim_id": sim_id,
425
+ "total_energy": energy,
426
+ "t": sim.t
427
+ }
428
+ except Exception as e:
429
+ return {"success": False, "error": str(e)}
430
+
431
+
432
+ @mcp.tool()
433
+ def calculate_angular_momentum(sim_id: str) -> dict:
434
+ """
435
+ Calculate total angular momentum of the system.
436
+
437
+ Parameters:
438
+ - sim_id: Simulation ID
439
+
440
+ Returns:
441
+ - dict: Angular momentum vector
442
+ """
443
+ if not REBOUND_AVAILABLE:
444
+ return {"success": False, "error": "REBOUND not installed"}
445
+
446
+ if sim_id not in _simulations:
447
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
448
+
449
+ try:
450
+ sim = _simulations[sim_id]
451
+ L = sim.calculate_angular_momentum()
452
 
453
  return {
454
  "success": True,
455
+ "sim_id": sim_id,
456
+ "angular_momentum": {
457
+ "x": L[0],
458
+ "y": L[1],
459
+ "z": L[2]
460
+ },
461
+ "magnitude": math.sqrt(L[0]**2 + L[1]**2 + L[2]**2),
462
+ "t": sim.t
463
  }
464
  except Exception as e:
465
  return {"success": False, "error": str(e)}
466
 
467
+
468
+ @mcp.tool()
469
+ def calculate_com(sim_id: str) -> dict:
470
  """
471
+ Calculate center of mass position and velocity.
472
 
473
  Parameters:
474
+ - sim_id: Simulation ID
475
 
476
  Returns:
477
+ - dict: Center of mass coordinates
478
  """
479
+ if not REBOUND_AVAILABLE:
480
+ return {"success": False, "error": "REBOUND not installed"}
481
+
482
+ if sim_id not in _simulations:
483
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
484
+
485
  try:
486
+ sim = _simulations[sim_id]
487
+ com = sim.calculate_com()
488
 
489
+ return {
490
+ "success": True,
491
+ "sim_id": sim_id,
492
+ "com": {
493
+ "x": com.x, "y": com.y, "z": com.z,
494
+ "vx": com.vx, "vy": com.vy, "vz": com.vz
495
+ }
496
+ }
497
+ except Exception as e:
498
+ return {"success": False, "error": str(e)}
499
+
500
+
501
+ @mcp.tool()
502
+ def move_to_com(sim_id: str) -> dict:
503
+ """
504
+ Move simulation to center of mass frame.
505
+
506
+ Parameters:
507
+ - sim_id: Simulation ID
508
+
509
+ Returns:
510
+ - dict: Confirmation
511
+ """
512
+ if not REBOUND_AVAILABLE:
513
+ return {"success": False, "error": "REBOUND not installed"}
514
+
515
+ if sim_id not in _simulations:
516
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
517
+
518
+ try:
519
+ sim = _simulations[sim_id]
520
+ sim.move_to_com()
521
 
522
  return {
523
  "success": True,
524
+ "sim_id": sim_id,
525
+ "message": "Simulation moved to center of mass frame"
526
  }
527
  except Exception as e:
528
  return {"success": False, "error": str(e)}
529
 
530
+
531
+ @mcp.tool()
532
+ def calculate_megno(sim_id: str, tmax: float) -> dict:
533
  """
534
+ Calculate MEGNO (Mean Exponential Growth factor of Nearby Orbits) chaos indicator.
535
 
536
  Parameters:
537
+ - sim_id: Simulation ID
538
+ - tmax: Time to integrate
539
 
540
  Returns:
541
+ - dict: MEGNO value
542
  """
543
+ if not REBOUND_AVAILABLE:
544
+ return {"success": False, "error": "REBOUND not installed"}
545
+
546
+ if sim_id not in _simulations:
547
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
548
+
549
  try:
550
+ sim = _simulations[sim_id]
551
 
552
+ # Initialize variational particles
553
+ sim.init_megno()
554
+ sim.integrate(tmax)
555
+ megno = sim.calculate_megno()
556
 
557
  return {
558
  "success": True,
559
+ "sim_id": sim_id,
560
+ "megno": megno,
561
+ "t": sim.t,
562
+ "interpretation": "MEGNO < 2: Regular/Quasi-periodic, MEGNO > 2: Chaotic"
563
  }
564
  except Exception as e:
565
  return {"success": False, "error": str(e)}
566
 
567
+
568
+ @mcp.tool()
569
+ def calculate_lyapunov(sim_id: str, tmax: float) -> dict:
570
+ """
571
+ Calculate maximum Lyapunov characteristic exponent.
572
+
573
+ Parameters:
574
+ - sim_id: Simulation ID
575
+ - tmax: Time to integrate
576
+
577
+ Returns:
578
+ - dict: Lyapunov exponent
579
+ """
580
+ if not REBOUND_AVAILABLE:
581
+ return {"success": False, "error": "REBOUND not installed"}
582
+
583
+ if sim_id not in _simulations:
584
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
585
+
586
+ try:
587
+ sim = _simulations[sim_id]
588
+
589
+ # Initialize variational particles
590
+ sim.init_megno()
591
+ sim.integrate(tmax)
592
+ lyapunov = sim.calculate_lyapunov()
593
+
594
+ return {
595
+ "success": True,
596
+ "sim_id": sim_id,
597
+ "lyapunov_exponent": lyapunov,
598
+ "lyapunov_timescale": 1.0/lyapunov if lyapunov > 0 else float('inf'),
599
+ "t": sim.t
600
+ }
601
+ except Exception as e:
602
+ return {"success": False, "error": str(e)}
603
+
604
+
605
+ # ============================================================================
606
+ # Coordinate Transformation Tools
607
+ # ============================================================================
608
+
609
+ @mcp.tool()
610
+ def spherical_to_cartesian(magnitude: float = 1.0, theta: float = 0.0, phi: float = 0.0) -> dict:
611
+ """
612
+ Convert spherical coordinates to Cartesian.
613
+
614
+ Parameters:
615
+ - magnitude: Vector magnitude
616
+ - theta: Polar angle (radians, measured from z-axis)
617
+ - phi: Azimuthal angle (radians, measured from x-axis)
618
+
619
+ Returns:
620
+ - dict: Cartesian coordinates
621
+ """
622
+ if not REBOUND_AVAILABLE:
623
+ return {"success": False, "error": "REBOUND not installed"}
624
+
625
+ try:
626
+ vec = tools.spherical_to_xyz(magnitude=magnitude, theta=theta, phi=phi)
627
+
628
+ return {
629
+ "success": True,
630
+ "cartesian": {
631
+ "x": vec.x,
632
+ "y": vec.y,
633
+ "z": vec.z
634
+ },
635
+ "input": {
636
+ "magnitude": magnitude,
637
+ "theta": theta,
638
+ "phi": phi
639
+ }
640
+ }
641
+ except Exception as e:
642
+ return {"success": False, "error": str(e)}
643
+
644
+
645
+ @mcp.tool()
646
+ def cartesian_to_spherical(x: float, y: float, z: float) -> dict:
647
+ """
648
+ Convert Cartesian coordinates to spherical.
649
+
650
+ Parameters:
651
+ - x, y, z: Cartesian coordinates
652
+
653
+ Returns:
654
+ - dict: Spherical coordinates
655
+ """
656
+ if not REBOUND_AVAILABLE:
657
+ return {"success": False, "error": "REBOUND not installed"}
658
+
659
+ try:
660
+ magnitude, theta, phi = tools.xyz_to_spherical([x, y, z])
661
+
662
+ return {
663
+ "success": True,
664
+ "spherical": {
665
+ "magnitude": magnitude,
666
+ "theta": theta,
667
+ "phi": phi
668
+ },
669
+ "input": {
670
+ "x": x,
671
+ "y": y,
672
+ "z": z
673
+ }
674
+ }
675
+ except Exception as e:
676
+ return {"success": False, "error": str(e)}
677
+
678
+
679
+ @mcp.tool()
680
+ def mean_anomaly_to_true_anomaly(e: float, M: float) -> dict:
681
+ """
682
+ Convert mean anomaly to true anomaly.
683
+
684
+ Parameters:
685
+ - e: Eccentricity
686
+ - M: Mean anomaly (radians)
687
+
688
+ Returns:
689
+ - dict: True anomaly
690
+ """
691
+ if not REBOUND_AVAILABLE:
692
+ return {"success": False, "error": "REBOUND not installed"}
693
+
694
+ try:
695
+ f = tools.M_to_f(e, M)
696
+
697
+ return {
698
+ "success": True,
699
+ "true_anomaly": f,
700
+ "eccentricity": e,
701
+ "mean_anomaly": M
702
+ }
703
+ except Exception as e:
704
+ return {"success": False, "error": str(e)}
705
+
706
+
707
+ @mcp.tool()
708
+ def eccentric_anomaly_to_true_anomaly(e: float, E: float) -> dict:
709
+ """
710
+ Convert eccentric anomaly to true anomaly.
711
+
712
+ Parameters:
713
+ - e: Eccentricity
714
+ - E: Eccentric anomaly (radians)
715
+
716
+ Returns:
717
+ - dict: True anomaly
718
+ """
719
+ if not REBOUND_AVAILABLE:
720
+ return {"success": False, "error": "REBOUND not installed"}
721
+
722
+ try:
723
+ f = tools.E_to_f(e, E)
724
+
725
+ return {
726
+ "success": True,
727
+ "true_anomaly": f,
728
+ "eccentricity": e,
729
+ "eccentric_anomaly": E
730
+ }
731
+ except Exception as e:
732
+ return {"success": False, "error": str(e)}
733
+
734
+
735
+ @mcp.tool()
736
+ def mean_anomaly_to_eccentric_anomaly(e: float, M: float) -> dict:
737
+ """
738
+ Convert mean anomaly to eccentric anomaly.
739
+
740
+ Parameters:
741
+ - e: Eccentricity
742
+ - M: Mean anomaly (radians)
743
+
744
+ Returns:
745
+ - dict: Eccentric anomaly
746
+ """
747
+ if not REBOUND_AVAILABLE:
748
+ return {"success": False, "error": "REBOUND not installed"}
749
+
750
+ try:
751
+ E = tools.M_to_E(e, M)
752
+
753
+ return {
754
+ "success": True,
755
+ "eccentric_anomaly": E,
756
+ "eccentricity": e,
757
+ "mean_anomaly": M
758
+ }
759
+ except Exception as e:
760
+ return {"success": False, "error": str(e)}
761
+
762
+
763
+ # ============================================================================
764
+ # Preset Systems and Initial Conditions
765
+ # ============================================================================
766
+
767
+ @mcp.tool()
768
+ def add_outer_solar_system(sim_id: str) -> dict:
769
+ """
770
+ Add outer Solar System (Jupiter, Saturn, Uranus, Neptune, Pluto) from NASA Horizons data.
771
+
772
+ Parameters:
773
+ - sim_id: Simulation ID
774
+
775
+ Returns:
776
+ - dict: System information
777
+ """
778
+ if not REBOUND_AVAILABLE:
779
+ return {"success": False, "error": "REBOUND not installed"}
780
+
781
+ if sim_id not in _simulations:
782
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
783
+
784
+ try:
785
+ sim = _simulations[sim_id]
786
+ data.add_outer_solar_system(sim)
787
+
788
+ return {
789
+ "success": True,
790
+ "sim_id": sim_id,
791
+ "n_particles": sim.N,
792
+ "bodies": ["Sun", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"],
793
+ "message": "Outer Solar System added"
794
+ }
795
+ except Exception as e:
796
+ return {"success": False, "error": str(e)}
797
+
798
+
799
+ @mcp.tool()
800
+ def add_full_solar_system(sim_id: str) -> dict:
801
+ """
802
+ Add all planets of the Solar System from NASA Horizons data.
803
+
804
+ Parameters:
805
+ - sim_id: Simulation ID
806
+
807
+ Returns:
808
+ - dict: System information
809
+ """
810
+ if not REBOUND_AVAILABLE:
811
+ return {"success": False, "error": "REBOUND not installed"}
812
+
813
+ if sim_id not in _simulations:
814
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
815
+
816
+ try:
817
+ sim = _simulations[sim_id]
818
+ data.add_solar_system(sim)
819
+
820
+ return {
821
+ "success": True,
822
+ "sim_id": sim_id,
823
+ "n_particles": sim.N,
824
+ "bodies": ["Sun", "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"],
825
+ "message": "Full Solar System added"
826
+ }
827
+ except Exception as e:
828
+ return {"success": False, "error": str(e)}
829
+
830
+
831
+ @mcp.tool()
832
+ def add_horizons_body(sim_id: str, body_name: str, date: str = "2024-01-01") -> dict:
833
+ """
834
+ Add a celestial body from NASA Horizons database.
835
+
836
+ Parameters:
837
+ - sim_id: Simulation ID
838
+ - body_name: Name of body (e.g., 'Sun', 'Earth', 'Mars', 'Jupiter')
839
+ - date: Date for initial conditions (YYYY-MM-DD format)
840
+
841
+ Returns:
842
+ - dict: Body information
843
+ """
844
+ if not REBOUND_AVAILABLE:
845
+ return {"success": False, "error": "REBOUND not installed"}
846
+
847
+ if sim_id not in _simulations:
848
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
849
+
850
+ try:
851
+ sim = _simulations[sim_id]
852
+ sim.add(body_name, date=date)
853
+
854
+ particle_idx = sim.N - 1
855
+ p = sim.particles[particle_idx]
856
+
857
+ return {
858
+ "success": True,
859
+ "sim_id": sim_id,
860
+ "body_name": body_name,
861
+ "date": date,
862
+ "n_particles": sim.N,
863
+ "particle": {
864
+ "index": particle_idx,
865
+ "m": p.m,
866
+ "x": p.x, "y": p.y, "z": p.z,
867
+ "vx": p.vx, "vy": p.vy, "vz": p.vz
868
+ }
869
+ }
870
+ except Exception as e:
871
+ return {"success": False, "error": str(e)}
872
+
873
+
874
+ # ============================================================================
875
+ # File I/O Tools
876
+ # ============================================================================
877
+
878
+ @mcp.tool()
879
+ def save_simulation(sim_id: str, filename: str) -> dict:
880
+ """
881
+ Save simulation to a binary file.
882
+
883
+ Parameters:
884
+ - sim_id: Simulation ID
885
+ - filename: Output filename
886
+
887
+ Returns:
888
+ - dict: Confirmation
889
+ """
890
+ if not REBOUND_AVAILABLE:
891
+ return {"success": False, "error": "REBOUND not installed"}
892
+
893
+ if sim_id not in _simulations:
894
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
895
+
896
+ try:
897
+ sim = _simulations[sim_id]
898
+ sim.save_to_file(filename)
899
+
900
+ return {
901
+ "success": True,
902
+ "sim_id": sim_id,
903
+ "filename": filename,
904
+ "message": "Simulation saved"
905
+ }
906
+ except Exception as e:
907
+ return {"success": False, "error": str(e)}
908
+
909
+
910
+ @mcp.tool()
911
+ def load_simulation(filename: str) -> dict:
912
+ """
913
+ Load simulation from a binary file.
914
+
915
+ Parameters:
916
+ - filename: Input filename
917
+
918
+ Returns:
919
+ - dict: New simulation ID
920
+ """
921
+ if not REBOUND_AVAILABLE:
922
+ return {"success": False, "error": "REBOUND not installed"}
923
+
924
+ try:
925
+ global _sim_counter
926
+ sim = rebound.Simulation(filename)
927
+
928
+ sim_id = f"sim_{_sim_counter}"
929
+ _simulations[sim_id] = sim
930
+ _sim_counter += 1
931
+
932
+ return {
933
+ "success": True,
934
+ "sim_id": sim_id,
935
+ "filename": filename,
936
+ "t": sim.t,
937
+ "n_particles": sim.N,
938
+ "integrator": sim.integrator,
939
+ "message": "Simulation loaded"
940
+ }
941
+ except Exception as e:
942
+ return {"success": False, "error": str(e)}
943
+
944
+
945
+
946
+
947
+ # ============================================================================
948
+ # Configuration and Management Tools
949
+ # ============================================================================
950
+
951
+ @mcp.tool()
952
+ def set_integrator_options(sim_id: str, option_name: str, option_value: float) -> dict:
953
+ """
954
+ Set integrator-specific options.
955
+
956
+ Parameters:
957
+ - sim_id: Simulation ID
958
+ - option_name: Option name (e.g., 'epsilon' for IAS15, 'safe_mode' for WHFast)
959
+ - option_value: Option value
960
+
961
+ Returns:
962
+ - dict: Confirmation
963
+ """
964
+ if not REBOUND_AVAILABLE:
965
+ return {"success": False, "error": "REBOUND not installed"}
966
+
967
+ if sim_id not in _simulations:
968
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
969
+
970
+ try:
971
+ sim = _simulations[sim_id]
972
+ setattr(sim.ri, option_name, option_value)
973
+
974
+ return {
975
+ "success": True,
976
+ "sim_id": sim_id,
977
+ "option_name": option_name,
978
+ "option_value": option_value
979
+ }
980
+ except Exception as e:
981
+ return {"success": False, "error": str(e)}
982
+
983
+
984
+ @mcp.tool()
985
+ def get_simulation_status(sim_id: str) -> dict:
986
+ """
987
+ Get current simulation status and configuration.
988
+
989
+ Parameters:
990
+ - sim_id: Simulation ID
991
+
992
+ Returns:
993
+ - dict: Simulation configuration and status
994
+ """
995
+ if not REBOUND_AVAILABLE:
996
+ return {"success": False, "error": "REBOUND not installed"}
997
+
998
+ if sim_id not in _simulations:
999
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
1000
+
1001
+ try:
1002
+ sim = _simulations[sim_id]
1003
+
1004
+ return {
1005
+ "success": True,
1006
+ "sim_id": sim_id,
1007
+ "t": sim.t,
1008
+ "dt": sim.dt,
1009
+ "dt_last_done": sim.dt_last_done,
1010
+ "n_particles": sim.N,
1011
+ "n_active": sim.N_active,
1012
+ "integrator": sim.integrator,
1013
+ "gravity": sim.gravity,
1014
+ "collision": sim.collision,
1015
+ "boundary": sim.boundary,
1016
+ "G": sim.G,
1017
+ "softening": sim.softening,
1018
+ "testparticle_type": sim.testparticle_type,
1019
+ "walltime": sim.walltime
1020
+ }
1021
+ except Exception as e:
1022
+ return {"success": False, "error": str(e)}
1023
+
1024
+
1025
+ @mcp.tool()
1026
+ def list_active_simulations() -> dict:
1027
+ """
1028
+ List all active simulation IDs.
1029
+
1030
+ Returns:
1031
+ - dict: List of simulation IDs
1032
+ """
1033
+ if not REBOUND_AVAILABLE:
1034
+ return {"success": False, "error": "REBOUND not installed"}
1035
+
1036
+ try:
1037
+ sim_list = []
1038
+ for sim_id, sim in _simulations.items():
1039
+ sim_list.append({
1040
+ "sim_id": sim_id,
1041
+ "t": sim.t,
1042
+ "n_particles": sim.N,
1043
+ "integrator": sim.integrator
1044
+ })
1045
+
1046
+ return {
1047
+ "success": True,
1048
+ "n_simulations": len(sim_list),
1049
+ "simulations": sim_list
1050
+ }
1051
+ except Exception as e:
1052
+ return {"success": False, "error": str(e)}
1053
+
1054
+
1055
+ @mcp.tool()
1056
+ def delete_simulation(sim_id: str) -> dict:
1057
+ """
1058
+ Delete a simulation from memory.
1059
+
1060
+ Parameters:
1061
+ - sim_id: Simulation ID
1062
+
1063
+ Returns:
1064
+ - dict: Confirmation
1065
+ """
1066
+ if not REBOUND_AVAILABLE:
1067
+ return {"success": False, "error": "REBOUND not installed"}
1068
+
1069
+ if sim_id not in _simulations:
1070
+ return {"success": False, "error": f"Simulation {sim_id} not found"}
1071
+
1072
+ try:
1073
+ del _simulations[sim_id]
1074
+
1075
+ return {
1076
+ "success": True,
1077
+ "sim_id": sim_id,
1078
+ "message": "Simulation deleted"
1079
+ }
1080
+ except Exception as e:
1081
+ return {"success": False, "error": str(e)}
1082
+
1083
+
1084
+ @mcp.tool()
1085
+ def get_rebound_info() -> dict:
1086
+ """
1087
+ Get REBOUND version and available features.
1088
+
1089
+ Returns:
1090
+ - dict: Version and features
1091
+ """
1092
+ if not REBOUND_AVAILABLE:
1093
+ return {
1094
+ "success": False,
1095
+ "available": False,
1096
+ "message": "REBOUND not installed. Install with: pip install rebound"
1097
+ }
1098
+
1099
+ try:
1100
+ return {
1101
+ "success": True,
1102
+ "available": True,
1103
+ "version": rebound.__version__,
1104
+ "build": rebound.__build__,
1105
+ "githash": rebound.__githash__,
1106
+ "description": "Open-source multi-purpose N-body code for collisional dynamics",
1107
+ "integrators": ["IAS15", "WHFast", "Mercurius", "LeapFrog", "SEI", "EOS", "BS", "JANUS", "TRACE"],
1108
+ "gravity_solvers": ["compensated", "basic", "none", "tree"],
1109
+ "collision_detection": ["none", "direct", "tree"],
1110
+ "boundary_conditions": ["none", "open", "periodic", "shear"],
1111
+ "features": [
1112
+ "N-body gravity",
1113
+ "Collisional dynamics",
1114
+ "Orbital elements",
1115
+ "Variational equations",
1116
+ "MEGNO chaos indicator",
1117
+ "SimulationArchive",
1118
+ "NASA Horizons integration"
1119
+ ],
1120
+ "website": "https://rebound.readthedocs.io/",
1121
+ "github": "https://github.com/hannorein/rebound"
1122
+ }
1123
+ except Exception as e:
1124
+ return {"success": False, "error": str(e)}
1125
+
1126
+
1127
  def create_app() -> FastMCP:
1128
  """
1129
+ Creates and returns the FastMCP application instance.
1130
 
1131
  Returns:
1132
+ FastMCP: The FastMCP application instance.
1133
  """
1134
+ return mcp
1135
+
1136
+ if __name__ == "__main__":
1137
+ mcp.run(transport="http", host="0.0.0.0", port=8000)