guohanghui commited on
Commit
132681b
·
verified ·
1 Parent(s): fb1744f

Update galpy/mcp_output/mcp_plugin/mcp_service.py

Browse files
galpy/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -1,82 +1,415 @@
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
  from galpy.orbit import Orbit
11
- from galpy.potential import Potential
12
- from galpy.actionAngle import actionAngleIsochrone
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  # Create the FastMCP service application
15
  mcp = FastMCP("galpy_service")
16
 
17
- @mcp.tool(name="calculate_orbit", description="Calculate the orbit of a celestial object.")
18
- def calculate_orbit(R: float, vR: float, vT: float, z: float, vz: float, phi: float) -> dict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  """
20
  Calculate the orbit of a celestial object given initial conditions.
21
 
22
  Parameters:
23
- - R: float - Radial distance
24
- - vR: float - Radial velocity
25
- - vT: float - Tangential velocity
26
- - z: float - Vertical position
27
- - vz: float - Vertical velocity
28
- - phi: float - Azimuthal angle
 
 
 
29
 
30
  Returns:
31
- - dict: Contains success, result, or error fields
32
  """
33
  try:
 
 
 
 
 
 
 
 
 
 
 
34
  orbit = Orbit([R, vR, vT, z, vz, phi])
35
- result = orbit.integrate()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  return {"success": True, "result": result}
37
  except Exception as e:
38
  return {"success": False, "error": str(e)}
39
 
40
- @mcp.tool(name="evaluate_potential", description="Evaluate the gravitational potential at a point.")
41
- def evaluate_potential(R: float, z: float) -> dict:
 
42
  """
43
- Evaluate the gravitational potential at a given point.
44
 
45
  Parameters:
46
- - R: float - Radial distance
47
- - z: float - Vertical position
48
 
49
  Returns:
50
- - dict: Contains success, result, or error fields
51
  """
52
  try:
53
- potential = Potential()
54
- result = potential(R, z)
55
- return {"success": True, "result": result}
 
 
 
 
 
 
 
 
 
56
  except Exception as e:
57
  return {"success": False, "error": str(e)}
58
 
59
- @mcp.tool(name="compute_action_angle", description="Compute action-angle variables for a given orbit.")
60
- def compute_action_angle(R: float, vR: float, vT: float, z: float, vz: float, phi: float) -> dict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  """
62
  Compute action-angle variables for a given orbit.
63
 
64
  Parameters:
65
- - R: float - Radial distance
66
- - vR: float - Radial velocity
67
- - vT: float - Tangential velocity
68
- - z: float - Vertical position
69
- - vz: float - Vertical velocity
70
- - phi: float - Azimuthal angle
71
 
72
  Returns:
73
- - dict: Contains success, result, or error fields
74
  """
75
  try:
76
  orbit = Orbit([R, vR, vT, z, vz, phi])
77
- action_angle = actionAngleIsochrone()
78
- result = action_angle(orbit)
79
- return {"success": True, "result": result}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  except Exception as e:
81
  return {"success": False, "error": str(e)}
82
 
 
1
  import os
2
  import sys
3
 
4
+ # NOTE: galpy should be installed via pip, no need to add source path
5
+ # This allows using the compiled Cython extensions properly
 
 
6
 
7
  from fastmcp import FastMCP
8
+ import numpy as np
9
+
10
+ # Import core modules from galpy
11
  from galpy.orbit import Orbit
12
+ from galpy.potential import (
13
+ MiyamotoNagaiPotential,
14
+ NFWPotential,
15
+ LogarithmicHaloPotential,
16
+ HernquistPotential,
17
+ PlummerPotential,
18
+ IsochronePotential,
19
+ KeplerPotential,
20
+ evaluatePotentials,
21
+ vcirc,
22
+ epifreq,
23
+ verticalfreq,
24
+ omegac,
25
+ )
26
+ from galpy.actionAngle import (
27
+ actionAngleIsochrone,
28
+ actionAngleStaeckel,
29
+ actionAngleSpherical,
30
+ estimateDeltaStaeckel,
31
+ )
32
 
33
  # Create the FastMCP service application
34
  mcp = FastMCP("galpy_service")
35
 
36
+ # ============= Potential Creation Tools =============
37
+
38
+ def _create_potential(potential_type: str, **params):
39
+ """Helper function to create potential objects."""
40
+ potential_map = {
41
+ "miyamoto_nagai": MiyamotoNagaiPotential,
42
+ "nfw": NFWPotential,
43
+ "logarithmic": LogarithmicHaloPotential,
44
+ "hernquist": HernquistPotential,
45
+ "plummer": PlummerPotential,
46
+ "isochrone": IsochronePotential,
47
+ "kepler": KeplerPotential,
48
+ }
49
+ if potential_type.lower() not in potential_map:
50
+ raise ValueError(f"Unknown potential type: {potential_type}. Available: {list(potential_map.keys())}")
51
+ return potential_map[potential_type.lower()](**params)
52
+
53
+ @mcp.tool(name="create_potential", description="Create a gravitational potential model. Supported types: miyamoto_nagai, nfw, logarithmic, hernquist, plummer, isochrone, kepler.")
54
+ def create_potential(potential_type: str, amp: float = 1.0, a: float = 1.0, b: float = 0.1) -> dict:
55
+ """
56
+ Create a gravitational potential model.
57
+
58
+ Parameters:
59
+ - potential_type: str - Type of potential (miyamoto_nagai, nfw, logarithmic, hernquist, plummer, isochrone, kepler)
60
+ - amp: float - Amplitude/mass parameter (default: 1.0)
61
+ - a: float - Scale length parameter (default: 1.0)
62
+ - b: float - Scale height parameter for disk potentials (default: 0.1)
63
+
64
+ Returns:
65
+ - dict: Contains success and potential info or error
66
+ """
67
+ try:
68
+ pot = _create_potential(potential_type, amp=amp, a=a, b=b) if potential_type.lower() == "miyamoto_nagai" else _create_potential(potential_type, amp=amp, a=a)
69
+ return {"success": True, "result": f"Created {potential_type} potential with amp={amp}, a={a}"}
70
+ except Exception as e:
71
+ return {"success": False, "error": str(e)}
72
+
73
+ @mcp.tool(name="evaluate_potential", description="Evaluate the gravitational potential at a given position (R, z).")
74
+ def evaluate_potential(R: float, z: float, potential_type: str = "miyamoto_nagai", amp: float = 1.0, a: float = 1.0, b: float = 0.1) -> dict:
75
+ """
76
+ Evaluate the gravitational potential at a given point.
77
+
78
+ Parameters:
79
+ - R: float - Cylindrical Galactocentric radius (in natural units, 8 kpc = 1)
80
+ - z: float - Vertical height above the plane (in natural units)
81
+ - potential_type: str - Type of potential to use
82
+ - amp: float - Amplitude/mass parameter
83
+ - a: float - Scale length
84
+ - b: float - Scale height (for disk potentials)
85
+
86
+ Returns:
87
+ - dict: Contains success and potential value or error
88
+ """
89
+ try:
90
+ if potential_type.lower() == "miyamoto_nagai":
91
+ pot = MiyamotoNagaiPotential(amp=amp, a=a, b=b)
92
+ elif potential_type.lower() == "nfw":
93
+ pot = NFWPotential(amp=amp, a=a)
94
+ elif potential_type.lower() == "logarithmic":
95
+ pot = LogarithmicHaloPotential(amp=amp)
96
+ else:
97
+ pot = MiyamotoNagaiPotential(amp=amp, a=a, b=b)
98
+
99
+ result = float(pot(R, z))
100
+ return {"success": True, "result": result, "R": R, "z": z, "potential_type": potential_type}
101
+ except Exception as e:
102
+ return {"success": False, "error": str(e)}
103
+
104
+ # ============= Orbit Tools =============
105
+
106
+ @mcp.tool(name="calculate_orbit", description="Calculate and integrate the orbit of a celestial object in a gravitational potential.")
107
+ def calculate_orbit(R: float, vR: float, vT: float, z: float, vz: float, phi: float,
108
+ t_end: float = 10.0, n_steps: int = 1000,
109
+ potential_type: str = "miyamoto_nagai") -> dict:
110
  """
111
  Calculate the orbit of a celestial object given initial conditions.
112
 
113
  Parameters:
114
+ - R: float - Initial radial distance (in natural units, 8 kpc = 1)
115
+ - vR: float - Initial radial velocity (in natural units, 220 km/s = 1)
116
+ - vT: float - Initial tangential velocity
117
+ - z: float - Initial vertical position
118
+ - vz: float - Initial vertical velocity
119
+ - phi: float - Initial azimuthal angle (radians)
120
+ - t_end: float - End time for integration (in natural units, ~4.7 Gyr = 1)
121
+ - n_steps: int - Number of time steps
122
+ - potential_type: str - Type of gravitational potential
123
 
124
  Returns:
125
+ - dict: Contains success, orbit data (R, z, phi arrays), or error
126
  """
127
  try:
128
+ # Create potential
129
+ if potential_type.lower() == "miyamoto_nagai":
130
+ pot = MiyamotoNagaiPotential(amp=1.0, a=1.0, b=0.1)
131
+ elif potential_type.lower() == "nfw":
132
+ pot = NFWPotential(amp=1.0, a=1.0)
133
+ elif potential_type.lower() == "logarithmic":
134
+ pot = LogarithmicHaloPotential(amp=1.0)
135
+ else:
136
+ pot = MiyamotoNagaiPotential(amp=1.0, a=1.0, b=0.1)
137
+
138
+ # Create orbit with initial conditions
139
  orbit = Orbit([R, vR, vT, z, vz, phi])
140
+
141
+ # Time array
142
+ ts = np.linspace(0, t_end, n_steps)
143
+
144
+ # Integrate
145
+ orbit.integrate(ts, pot)
146
+
147
+ # Extract orbit data (sample every 10th point to reduce output size)
148
+ sample_rate = max(1, n_steps // 100)
149
+ result = {
150
+ "R": orbit.R(ts[::sample_rate]).tolist(),
151
+ "z": orbit.z(ts[::sample_rate]).tolist(),
152
+ "phi": orbit.phi(ts[::sample_rate]).tolist(),
153
+ "t": ts[::sample_rate].tolist(),
154
+ }
155
  return {"success": True, "result": result}
156
  except Exception as e:
157
  return {"success": False, "error": str(e)}
158
 
159
+ @mcp.tool(name="calculate_orbit_energy", description="Calculate the energy of an orbit at given initial conditions.")
160
+ def calculate_orbit_energy(R: float, vR: float, vT: float, z: float, vz: float, phi: float,
161
+ potential_type: str = "miyamoto_nagai") -> dict:
162
  """
163
+ Calculate the total energy of an orbit.
164
 
165
  Parameters:
166
+ - R, vR, vT, z, vz, phi: Initial conditions for the orbit
167
+ - potential_type: str - Type of gravitational potential
168
 
169
  Returns:
170
+ - dict: Contains success and energy value or error
171
  """
172
  try:
173
+ if potential_type.lower() == "miyamoto_nagai":
174
+ pot = MiyamotoNagaiPotential(amp=1.0, a=1.0, b=0.1)
175
+ elif potential_type.lower() == "nfw":
176
+ pot = NFWPotential(amp=1.0, a=1.0)
177
+ else:
178
+ pot = MiyamotoNagaiPotential(amp=1.0, a=1.0, b=0.1)
179
+
180
+ orbit = Orbit([R, vR, vT, z, vz, phi])
181
+ orbit.integrate([0, 0.01], pot) # Need to integrate briefly to access energy
182
+ energy = float(orbit.E(pot=pot))
183
+
184
+ return {"success": True, "result": energy, "unit": "natural units (220 km/s)^2"}
185
  except Exception as e:
186
  return {"success": False, "error": str(e)}
187
 
188
+ @mcp.tool(name="calculate_angular_momentum", description="Calculate the angular momentum of an orbit.")
189
+ def calculate_angular_momentum(R: float, vR: float, vT: float, z: float, vz: float, phi: float) -> dict:
190
+ """
191
+ Calculate the angular momentum vector of an orbit.
192
+
193
+ Parameters:
194
+ - R, vR, vT, z, vz, phi: Initial conditions for the orbit
195
+
196
+ Returns:
197
+ - dict: Contains success and angular momentum (Lx, Ly, Lz) or error
198
+ """
199
+ try:
200
+ orbit = Orbit([R, vR, vT, z, vz, phi])
201
+ L = orbit.L()
202
+
203
+ return {
204
+ "success": True,
205
+ "result": {
206
+ "Lx": float(L[0]),
207
+ "Ly": float(L[1]),
208
+ "Lz": float(L[2])
209
+ },
210
+ "unit": "natural units (8 kpc * 220 km/s)"
211
+ }
212
+ except Exception as e:
213
+ return {"success": False, "error": str(e)}
214
+
215
+ @mcp.tool(name="calculate_orbital_parameters", description="Calculate orbital parameters: eccentricity, apocenter, pericenter.")
216
+ def calculate_orbital_parameters(R: float, vR: float, vT: float, z: float, vz: float, phi: float,
217
+ potential_type: str = "miyamoto_nagai") -> dict:
218
+ """
219
+ Calculate orbital parameters from initial conditions.
220
+
221
+ Parameters:
222
+ - R, vR, vT, z, vz, phi: Initial conditions for the orbit
223
+ - potential_type: str - Type of gravitational potential
224
+
225
+ Returns:
226
+ - dict: Contains eccentricity, apocenter radius, pericenter radius, or error
227
+ """
228
+ try:
229
+ if potential_type.lower() == "miyamoto_nagai":
230
+ pot = MiyamotoNagaiPotential(amp=1.0, a=1.0, b=0.1)
231
+ else:
232
+ pot = MiyamotoNagaiPotential(amp=1.0, a=1.0, b=0.1)
233
+
234
+ orbit = Orbit([R, vR, vT, z, vz, phi])
235
+ ts = np.linspace(0, 20, 2000) # Integrate for longer to get good orbital params
236
+ orbit.integrate(ts, pot)
237
+
238
+ ecc = float(orbit.e())
239
+ rap = float(orbit.rap())
240
+ rperi = float(orbit.rperi())
241
+
242
+ return {
243
+ "success": True,
244
+ "result": {
245
+ "eccentricity": ecc,
246
+ "apocenter": rap,
247
+ "pericenter": rperi
248
+ },
249
+ "unit": "natural units (8 kpc for distances)"
250
+ }
251
+ except Exception as e:
252
+ return {"success": False, "error": str(e)}
253
+
254
+ # ============= Action-Angle Tools =============
255
+
256
+ @mcp.tool(name="compute_action_angle", description="Compute action-angle variables for a given orbit using the isochrone approximation.")
257
+ def compute_action_angle(R: float, vR: float, vT: float, z: float, vz: float, phi: float,
258
+ method: str = "isochrone") -> dict:
259
  """
260
  Compute action-angle variables for a given orbit.
261
 
262
  Parameters:
263
+ - R, vR, vT, z, vz, phi: Orbital phase-space coordinates
264
+ - method: str - Method to use: 'isochrone', 'staeckel', or 'spherical'
 
 
 
 
265
 
266
  Returns:
267
+ - dict: Contains actions (JR, Lz, Jz) and angles or error
268
  """
269
  try:
270
  orbit = Orbit([R, vR, vT, z, vz, phi])
271
+
272
+ if method.lower() == "isochrone":
273
+ aA = actionAngleIsochrone(ip=IsochronePotential(amp=1.0, b=0.9))
274
+ elif method.lower() == "staeckel":
275
+ pot = MiyamotoNagaiPotential(amp=1.0, a=1.0, b=0.1)
276
+ delta = estimateDeltaStaeckel(pot, R, z)
277
+ aA = actionAngleStaeckel(pot=pot, delta=delta)
278
+ elif method.lower() == "spherical":
279
+ pot = LogarithmicHaloPotential(amp=1.0)
280
+ aA = actionAngleSpherical(pot=pot)
281
+ else:
282
+ aA = actionAngleIsochrone(ip=IsochronePotential(amp=1.0, b=0.9))
283
+
284
+ # Compute actions
285
+ jr, lz, jz = aA(R, vR, vT, z, vz)
286
+
287
+ return {
288
+ "success": True,
289
+ "result": {
290
+ "JR": float(jr),
291
+ "Lz": float(lz),
292
+ "Jz": float(jz)
293
+ },
294
+ "method": method,
295
+ "unit": "natural units (8 kpc * 220 km/s)"
296
+ }
297
+ except Exception as e:
298
+ return {"success": False, "error": str(e)}
299
+
300
+ # ============= Galaxy Dynamics Tools =============
301
+
302
+ @mcp.tool(name="calculate_circular_velocity", description="Calculate the circular velocity at a given radius in a potential.")
303
+ def calculate_circular_velocity(R: float, potential_type: str = "miyamoto_nagai",
304
+ amp: float = 1.0, a: float = 1.0, b: float = 0.1) -> dict:
305
+ """
306
+ Calculate circular velocity at radius R.
307
+
308
+ Parameters:
309
+ - R: float - Galactocentric radius
310
+ - potential_type: str - Type of potential
311
+ - amp, a, b: Potential parameters
312
+
313
+ Returns:
314
+ - dict: Contains circular velocity or error
315
+ """
316
+ try:
317
+ if potential_type.lower() == "miyamoto_nagai":
318
+ pot = MiyamotoNagaiPotential(amp=amp, a=a, b=b)
319
+ elif potential_type.lower() == "nfw":
320
+ pot = NFWPotential(amp=amp, a=a)
321
+ elif potential_type.lower() == "logarithmic":
322
+ pot = LogarithmicHaloPotential(amp=amp)
323
+ else:
324
+ pot = MiyamotoNagaiPotential(amp=amp, a=a, b=b)
325
+
326
+ vc = float(vcirc(pot, R))
327
+
328
+ return {
329
+ "success": True,
330
+ "result": vc,
331
+ "R": R,
332
+ "unit": "natural units (220 km/s = 1)"
333
+ }
334
+ except Exception as e:
335
+ return {"success": False, "error": str(e)}
336
+
337
+ @mcp.tool(name="calculate_frequencies", description="Calculate orbital frequencies (epicyclic, vertical, circular) at a given radius.")
338
+ def calculate_frequencies(R: float, potential_type: str = "miyamoto_nagai",
339
+ amp: float = 1.0, a: float = 1.0, b: float = 0.1) -> dict:
340
+ """
341
+ Calculate orbital frequencies at radius R.
342
+
343
+ Parameters:
344
+ - R: float - Galactocentric radius
345
+ - potential_type: str - Type of potential
346
+ - amp, a, b: Potential parameters
347
+
348
+ Returns:
349
+ - dict: Contains epicyclic, vertical, and circular frequencies or error
350
+ """
351
+ try:
352
+ if potential_type.lower() == "miyamoto_nagai":
353
+ pot = MiyamotoNagaiPotential(amp=amp, a=a, b=b)
354
+ elif potential_type.lower() == "nfw":
355
+ pot = NFWPotential(amp=amp, a=a)
356
+ else:
357
+ pot = MiyamotoNagaiPotential(amp=amp, a=a, b=b)
358
+
359
+ kappa = float(epifreq(pot, R)) # Epicyclic frequency
360
+ nu = float(verticalfreq(pot, R)) # Vertical frequency
361
+ omega = float(omegac(pot, R)) # Circular frequency
362
+
363
+ return {
364
+ "success": True,
365
+ "result": {
366
+ "epicyclic_frequency": kappa,
367
+ "vertical_frequency": nu,
368
+ "circular_frequency": omega
369
+ },
370
+ "R": R,
371
+ "unit": "natural units (1/time, where time unit ~ 4.7 Gyr)"
372
+ }
373
+ except Exception as e:
374
+ return {"success": False, "error": str(e)}
375
+
376
+ @mcp.tool(name="calculate_rotation_curve", description="Calculate the rotation curve of a galaxy model.")
377
+ def calculate_rotation_curve(R_min: float = 0.1, R_max: float = 3.0, n_points: int = 50,
378
+ potential_type: str = "miyamoto_nagai",
379
+ amp: float = 1.0, a: float = 1.0, b: float = 0.1) -> dict:
380
+ """
381
+ Calculate the rotation curve (circular velocity vs radius).
382
+
383
+ Parameters:
384
+ - R_min, R_max: float - Range of radii to calculate
385
+ - n_points: int - Number of points
386
+ - potential_type: str - Type of potential
387
+ - amp, a, b: Potential parameters
388
+
389
+ Returns:
390
+ - dict: Contains arrays of R and v_circ or error
391
+ """
392
+ try:
393
+ if potential_type.lower() == "miyamoto_nagai":
394
+ pot = MiyamotoNagaiPotential(amp=amp, a=a, b=b)
395
+ elif potential_type.lower() == "nfw":
396
+ pot = NFWPotential(amp=amp, a=a)
397
+ elif potential_type.lower() == "logarithmic":
398
+ pot = LogarithmicHaloPotential(amp=amp)
399
+ else:
400
+ pot = MiyamotoNagaiPotential(amp=amp, a=a, b=b)
401
+
402
+ Rs = np.linspace(R_min, R_max, n_points)
403
+ vcs = [float(vcirc(pot, r)) for r in Rs]
404
+
405
+ return {
406
+ "success": True,
407
+ "result": {
408
+ "R": Rs.tolist(),
409
+ "v_circular": vcs
410
+ },
411
+ "unit": "R in 8 kpc, v in 220 km/s"
412
+ }
413
  except Exception as e:
414
  return {"success": False, "error": str(e)}
415