mathmethods-mcp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mathmethods/__init__.py +7 -0
- mathmethods/__main__.py +6 -0
- mathmethods/compiler.py +202 -0
- mathmethods/core/__init__.py +36 -0
- mathmethods/core/differentiation.py +122 -0
- mathmethods/core/dynamic_1d.py +567 -0
- mathmethods/core/dynamic_2d_conservative.py +283 -0
- mathmethods/core/dynamic_2d_lanchester.py +264 -0
- mathmethods/core/dynamic_2d_linear.py +269 -0
- mathmethods/core/dynamic_2d_non_homogeneous.py +710 -0
- mathmethods/core/dynamic_2d_nonlinear.py +298 -0
- mathmethods/core/integration.py +396 -0
- mathmethods/core/interpolation.py +126 -0
- mathmethods/core/monte_carlo.py +312 -0
- mathmethods/core/ode.py +154 -0
- mathmethods/core/root_finding.py +339 -0
- mathmethods/core/utils.py +226 -0
- mathmethods/server.py +1414 -0
- mathmethods_mcp-0.1.0.dist-info/METADATA +436 -0
- mathmethods_mcp-0.1.0.dist-info/RECORD +24 -0
- mathmethods_mcp-0.1.0.dist-info/WHEEL +5 -0
- mathmethods_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- mathmethods_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
- mathmethods_mcp-0.1.0.dist-info/top_level.txt +1 -0
mathmethods/server.py
ADDED
|
@@ -0,0 +1,1414 @@
|
|
|
1
|
+
"""FastMCP application and tool definitions for MathematicalMethods.
|
|
2
|
+
|
|
3
|
+
Kept inside the package so it can be imported by tests and by the thin
|
|
4
|
+
``server.py`` entry point at the repository root.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from mcp.server.fastmcp import FastMCP
|
|
16
|
+
|
|
17
|
+
from mathmethods import compiler
|
|
18
|
+
from mathmethods.core import (
|
|
19
|
+
differentiation,
|
|
20
|
+
dynamic_1d,
|
|
21
|
+
dynamic_2d_conservative,
|
|
22
|
+
dynamic_2d_lanchester,
|
|
23
|
+
dynamic_2d_linear,
|
|
24
|
+
dynamic_2d_non_homogeneous,
|
|
25
|
+
dynamic_2d_nonlinear,
|
|
26
|
+
integration,
|
|
27
|
+
interpolation,
|
|
28
|
+
monte_carlo,
|
|
29
|
+
ode,
|
|
30
|
+
root_finding,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# --------------------------------------------------------------------------
|
|
34
|
+
# Resource / input caps (denial-of-service mitigation, see README security)
|
|
35
|
+
# --------------------------------------------------------------------------
|
|
36
|
+
MAX_ITERATIONS = 500
|
|
37
|
+
MAX_SUBINTERVALS = 2000
|
|
38
|
+
MAX_ODE_STEPS = 2000
|
|
39
|
+
MAX_INTERPOLATION_POINTS = 30
|
|
40
|
+
MAX_PRECISION = 15
|
|
41
|
+
MAX_PLOT_POINTS = 400
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# --------------------------------------------------------------------------
|
|
45
|
+
# Helpers
|
|
46
|
+
# --------------------------------------------------------------------------
|
|
47
|
+
def _require(condition: bool, message: str) -> None:
|
|
48
|
+
if not condition:
|
|
49
|
+
raise ValueError(message)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _clean_float(value: float, name: str, *, positive: bool = False) -> float:
|
|
53
|
+
try:
|
|
54
|
+
value = float(value)
|
|
55
|
+
except (TypeError, ValueError) as exc:
|
|
56
|
+
raise ValueError(f"{name} must be a number.") from exc
|
|
57
|
+
if not math.isfinite(value):
|
|
58
|
+
raise ValueError(f"{name} must be finite.")
|
|
59
|
+
if positive and value <= 0:
|
|
60
|
+
raise ValueError(f"{name} must be positive.")
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _clean_int(value: int, name: str, *, minimum: int, maximum: int) -> int:
|
|
65
|
+
try:
|
|
66
|
+
value = int(value)
|
|
67
|
+
except (TypeError, ValueError) as exc:
|
|
68
|
+
raise ValueError(f"{name} must be an integer.") from exc
|
|
69
|
+
if not (minimum <= value <= maximum):
|
|
70
|
+
raise ValueError(f"{name} must be between {minimum} and {maximum}.")
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _json_safe(value, _depth: int = 0) -> object:
|
|
75
|
+
"""Recursively convert a result into plain JSON-serializable data.
|
|
76
|
+
|
|
77
|
+
NumPy scalars/arrays become Python values, NaN/Inf become ``None`` and
|
|
78
|
+
anything else unsupported falls back to its string representation.
|
|
79
|
+
"""
|
|
80
|
+
if _depth > 64:
|
|
81
|
+
return str(value)
|
|
82
|
+
if isinstance(value, dict):
|
|
83
|
+
return {str(k): _json_safe(v, _depth + 1) for k, v in value.items()}
|
|
84
|
+
if isinstance(value, np.ndarray):
|
|
85
|
+
return _json_safe(value.tolist(), _depth + 1)
|
|
86
|
+
if isinstance(value, (list, tuple)):
|
|
87
|
+
return [_json_safe(v, _depth + 1) for v in value]
|
|
88
|
+
if isinstance(value, np.generic):
|
|
89
|
+
value = value.item()
|
|
90
|
+
if isinstance(value, bool):
|
|
91
|
+
return value
|
|
92
|
+
if isinstance(value, (int, float)):
|
|
93
|
+
if isinstance(value, float) and (math.isnan(value) or math.isinf(value)):
|
|
94
|
+
return None
|
|
95
|
+
return value
|
|
96
|
+
if value is None or isinstance(value, str):
|
|
97
|
+
return value
|
|
98
|
+
return str(value)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _downsample(values, max_points: int = MAX_PLOT_POINTS) -> object:
|
|
102
|
+
if not isinstance(values, list) or len(values) <= max_points:
|
|
103
|
+
return values
|
|
104
|
+
step = len(values) / max_points
|
|
105
|
+
return [values[int(i * step)] for i in range(max_points)]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _finalize(result, *, downsample_keys: tuple[str, ...] = ()) -> dict:
|
|
109
|
+
clean = _json_safe(result)
|
|
110
|
+
for key in downsample_keys:
|
|
111
|
+
if isinstance(clean, dict) and key in clean:
|
|
112
|
+
clean[key] = _downsample(clean[key])
|
|
113
|
+
return clean
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _trim_large_arrays(value) -> object:
|
|
117
|
+
"""Recursively downsample long numeric lists to keep responses bounded."""
|
|
118
|
+
if isinstance(value, list):
|
|
119
|
+
if value and all(isinstance(v, (int, float)) for v in value):
|
|
120
|
+
return _downsample(value)
|
|
121
|
+
return [_trim_large_arrays(v) for v in value]
|
|
122
|
+
if isinstance(value, dict):
|
|
123
|
+
return {k: _trim_large_arrays(v) for k, v in value.items()}
|
|
124
|
+
return value
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _finalize_deep(result) -> dict:
|
|
128
|
+
return _trim_large_arrays(_json_safe(result))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# --------------------------------------------------------------------------
|
|
132
|
+
# MCP application
|
|
133
|
+
# --------------------------------------------------------------------------
|
|
134
|
+
_HTTP_HOST = os.environ.get("MCP_HTTP_HOST", "127.0.0.1")
|
|
135
|
+
_HTTP_PORT = int(os.environ.get("MCP_HTTP_PORT", "8000"))
|
|
136
|
+
|
|
137
|
+
mcp = FastMCP(
|
|
138
|
+
"MathematicalMethods",
|
|
139
|
+
host=_HTTP_HOST,
|
|
140
|
+
port=_HTTP_PORT,
|
|
141
|
+
instructions=(
|
|
142
|
+
"Numerical-methods server: root finding (bisection), numerical "
|
|
143
|
+
"integration (Simpson 1/3), ordinary differential equations "
|
|
144
|
+
"(Runge-Kutta 4) and Lagrange interpolation. Math expressions use "
|
|
145
|
+
"Python/SymPy syntax: x**2, sin(x), exp(x), sqrt(x), log(x). "
|
|
146
|
+
"Also accepted: e^x, sen(x) and the caret (^) for powers."
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@mcp.tool()
|
|
152
|
+
def root_bisection(
|
|
153
|
+
func_str: str,
|
|
154
|
+
a: float,
|
|
155
|
+
b: float,
|
|
156
|
+
tol: float = 1e-6,
|
|
157
|
+
max_iter: int = 100,
|
|
158
|
+
precision: int = 8,
|
|
159
|
+
) -> dict:
|
|
160
|
+
"""Find a root of f(x) = 0 in [a, b] using the bisection method.
|
|
161
|
+
|
|
162
|
+
Requires f(a) and f(b) to have opposite signs (Bolzano's theorem).
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
func_str: Math expression in x, e.g. "x**2 - 4".
|
|
166
|
+
a: Left endpoint of the interval.
|
|
167
|
+
b: Right endpoint of the interval (must be > a).
|
|
168
|
+
tol: Convergence tolerance on the residual/error.
|
|
169
|
+
max_iter: Maximum number of iterations.
|
|
170
|
+
precision: Rounding digits for the reported root.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Dict with the root, convergence flag, iteration table and errors.
|
|
174
|
+
"""
|
|
175
|
+
a = _clean_float(a, "a")
|
|
176
|
+
b = _clean_float(b, "b")
|
|
177
|
+
tol = _clean_float(tol, "tol", positive=True)
|
|
178
|
+
max_iter = _clean_int(max_iter, "max_iter", minimum=1, maximum=MAX_ITERATIONS)
|
|
179
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
180
|
+
_require(b > a, "b must be greater than a.")
|
|
181
|
+
|
|
182
|
+
normalized = compiler.validate(func_str, variables=("x",))
|
|
183
|
+
func = root_finding.RootFindingService.compilar_funcion(normalized)
|
|
184
|
+
result = root_finding.RootFindingService.biseccion(
|
|
185
|
+
func, a, b, tol=tol, max_iter=max_iter, precision=precision
|
|
186
|
+
)
|
|
187
|
+
return _finalize(result)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@mcp.tool()
|
|
191
|
+
def integral_simpson13(
|
|
192
|
+
func_str: str,
|
|
193
|
+
a: float,
|
|
194
|
+
b: float,
|
|
195
|
+
n: int,
|
|
196
|
+
epsilon: float | None = None,
|
|
197
|
+
precision: int = 8,
|
|
198
|
+
) -> dict:
|
|
199
|
+
"""Approximate the definite integral of f(x) on [a, b] with composite Simpson 1/3.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
func_str: Math expression in x, e.g. "sin(x)/x".
|
|
203
|
+
a: Lower integration limit.
|
|
204
|
+
b: Upper integration limit (must be > a).
|
|
205
|
+
n: Number of subintervals (must be even). Error is O(h^4).
|
|
206
|
+
epsilon: Point in [a, b] used to report the truncation error bound.
|
|
207
|
+
precision: Rounding digits.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Dict with the approximated integral, error estimates and table.
|
|
211
|
+
"""
|
|
212
|
+
a = _clean_float(a, "a")
|
|
213
|
+
b = _clean_float(b, "b")
|
|
214
|
+
n = _clean_int(n, "n", minimum=2, maximum=MAX_SUBINTERVALS)
|
|
215
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
216
|
+
_require(b > a, "b must be greater than a.")
|
|
217
|
+
_require(n % 2 == 0, "n must be even for Simpson 1/3.")
|
|
218
|
+
if epsilon is not None:
|
|
219
|
+
epsilon = _clean_float(epsilon, "epsilon")
|
|
220
|
+
|
|
221
|
+
normalized = compiler.validate(func_str, variables=("x",))
|
|
222
|
+
func = integration.IntegrationService.compilar_funcion(normalized)
|
|
223
|
+
result = integration.IntegrationService.simpson_13_compuesto(
|
|
224
|
+
func, a, b, n=n, precision=precision, epsilon=epsilon
|
|
225
|
+
)
|
|
226
|
+
return _finalize(result)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@mcp.tool()
|
|
230
|
+
def ode_rk4(
|
|
231
|
+
ecuacion_str: str,
|
|
232
|
+
x0: float,
|
|
233
|
+
y0: float,
|
|
234
|
+
xf: float,
|
|
235
|
+
h: float,
|
|
236
|
+
tol: float | None = None,
|
|
237
|
+
precision: int = 8,
|
|
238
|
+
) -> dict:
|
|
239
|
+
"""Solve an initial value problem y'(x) = f(x, y) with the Runge-Kutta 4 method.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
ecuacion_str: Right-hand side f(x, y), e.g. "y" or "-2*x*y".
|
|
243
|
+
x0: Initial x.
|
|
244
|
+
y0: Initial y(x0).
|
|
245
|
+
xf: Final x (must be > x0).
|
|
246
|
+
h: Step size (must be positive). The endpoint xf is always reached.
|
|
247
|
+
tol: Optional tolerance to flag whether the final error meets it.
|
|
248
|
+
precision: Rounding digits.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Dict with the numerical solution table, exact solution (when SymPy can
|
|
252
|
+
solve it) and per-step errors.
|
|
253
|
+
"""
|
|
254
|
+
return _ode_solve("rk4", ecuacion_str, x0, y0, xf, h, tol, precision)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@mcp.tool()
|
|
258
|
+
def interpolation_lagrange(
|
|
259
|
+
puntos_x: list[float],
|
|
260
|
+
x_eval: float | None = None,
|
|
261
|
+
func_str: str | None = None,
|
|
262
|
+
puntos_y: list[float] | None = None,
|
|
263
|
+
precision: int = 8,
|
|
264
|
+
) -> dict:
|
|
265
|
+
"""Build the Lagrange interpolating polynomial through a set of points.
|
|
266
|
+
|
|
267
|
+
Provide either the point values ``puntos_y`` or the generating function
|
|
268
|
+
``func_str`` evaluated at ``puntos_x``.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
puntos_x: Distinct abscissas of the interpolation points.
|
|
272
|
+
x_eval: Optional x where the polynomial is evaluated.
|
|
273
|
+
func_str: Optional generating function f(x) for the y values.
|
|
274
|
+
puntos_y: Optional y values matching ``puntos_x`` one-to-one.
|
|
275
|
+
precision: Rounding digits.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Dict with the polynomial, its degree, the points table and error
|
|
279
|
+
bounds when ``func_str`` is provided.
|
|
280
|
+
"""
|
|
281
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
282
|
+
_require(
|
|
283
|
+
isinstance(puntos_x, list) and len(puntos_x) >= 2,
|
|
284
|
+
"puntos_x must be a list with at least 2 points.",
|
|
285
|
+
)
|
|
286
|
+
n_points = len(puntos_x)
|
|
287
|
+
_require(
|
|
288
|
+
n_points <= MAX_INTERPOLATION_POINTS,
|
|
289
|
+
f"Too many points (max {MAX_INTERPOLATION_POINTS}); Lagrange expansion grows combinatorially.",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
cleaned_x = [_clean_float(xi, f"puntos_x[{i}]") for i, xi in enumerate(puntos_x)]
|
|
293
|
+
_require(len(set(cleaned_x)) == n_points, "puntos_x values must be distinct.")
|
|
294
|
+
puntos_x = cleaned_x
|
|
295
|
+
|
|
296
|
+
if x_eval is not None:
|
|
297
|
+
x_eval = _clean_float(x_eval, "x_eval")
|
|
298
|
+
|
|
299
|
+
normalized_func_str = None
|
|
300
|
+
if func_str is not None:
|
|
301
|
+
normalized_func_str = compiler.validate(func_str, variables=("x",))
|
|
302
|
+
elif puntos_y is None:
|
|
303
|
+
raise ValueError("Provide either func_str or puntos_y.")
|
|
304
|
+
|
|
305
|
+
if puntos_y is not None:
|
|
306
|
+
_require(
|
|
307
|
+
isinstance(puntos_y, list) and len(puntos_y) == n_points,
|
|
308
|
+
"puntos_y must be a list with the same length as puntos_x.",
|
|
309
|
+
)
|
|
310
|
+
puntos_y = [_clean_float(yi, f"puntos_y[{i}]") for i, yi in enumerate(puntos_y)]
|
|
311
|
+
|
|
312
|
+
result = interpolation.InterpolationService.lagrange(
|
|
313
|
+
puntos_x,
|
|
314
|
+
x_eval=x_eval,
|
|
315
|
+
func_str=normalized_func_str,
|
|
316
|
+
puntos_y=puntos_y,
|
|
317
|
+
precision=precision,
|
|
318
|
+
)
|
|
319
|
+
return _finalize(result)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# --------------------------------------------------------------------------
|
|
323
|
+
# Additional caps for the dynamic / Monte Carlo tools
|
|
324
|
+
# --------------------------------------------------------------------------
|
|
325
|
+
MAX_MC_N = 200_000
|
|
326
|
+
MAX_MC_M = 500
|
|
327
|
+
MAX_DYN_GRID = 2000
|
|
328
|
+
MAX_DYN_STEPS = 5000
|
|
329
|
+
MAX_TRAJECTORIES = 50
|
|
330
|
+
MAX_BIF_STEPS = 500
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# --------------------------------------------------------------------------
|
|
334
|
+
# Monte Carlo tools
|
|
335
|
+
# --------------------------------------------------------------------------
|
|
336
|
+
@mcp.tool()
|
|
337
|
+
def mc_hit_or_miss_1d(
|
|
338
|
+
func_str: str,
|
|
339
|
+
a: float,
|
|
340
|
+
b: float,
|
|
341
|
+
N: int = 10000,
|
|
342
|
+
seed: int | None = None,
|
|
343
|
+
precision: int = 8,
|
|
344
|
+
nivel_confianza: float = 0.95,
|
|
345
|
+
) -> dict:
|
|
346
|
+
"""Estimate ∫ₐᵇ f(x) dx with the hit-or-miss Monte Carlo method.
|
|
347
|
+
|
|
348
|
+
Handles sign-changing integrands correctly (returns the signed integral).
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
func_str: Math expression in x.
|
|
352
|
+
a, b: Integration limits (b > a).
|
|
353
|
+
N: Number of samples.
|
|
354
|
+
seed: Optional RNG seed for reproducibility.
|
|
355
|
+
precision: Rounding digits.
|
|
356
|
+
nivel_confianza: Confidence level in (0, 1) for the interval.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Dict with the estimate, interval, sample statistics and history.
|
|
360
|
+
"""
|
|
361
|
+
a = _clean_float(a, "a")
|
|
362
|
+
b = _clean_float(b, "b")
|
|
363
|
+
N = _clean_int(N, "N", minimum=1, maximum=MAX_MC_N)
|
|
364
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
365
|
+
nivel_confianza = _clean_float(nivel_confianza, "nivel_confianza")
|
|
366
|
+
_require(0 < nivel_confianza < 1, "nivel_confianza must be in (0, 1).")
|
|
367
|
+
_require(b > a, "b must be greater than a.")
|
|
368
|
+
if seed is not None:
|
|
369
|
+
seed = _clean_int(seed, "seed", minimum=0, maximum=2**31 - 1)
|
|
370
|
+
|
|
371
|
+
normalized = compiler.validate(func_str, variables=("x",))
|
|
372
|
+
func = monte_carlo.MonteCarloService.compilar_funcion(normalized, variables="x")
|
|
373
|
+
result = monte_carlo.MonteCarloService.hit_or_miss_1d(
|
|
374
|
+
func, a, b, N=N, seed=seed, precision=precision, nivel_confianza=nivel_confianza
|
|
375
|
+
)
|
|
376
|
+
result["historial"] = _downsample(result.get("historial") or [], 100)
|
|
377
|
+
return _finalize_deep(result)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@mcp.tool()
|
|
381
|
+
def mc_valor_promedio_1d(
|
|
382
|
+
func_str: str,
|
|
383
|
+
a: float,
|
|
384
|
+
b: float,
|
|
385
|
+
N: int = 10000,
|
|
386
|
+
seed: int | None = None,
|
|
387
|
+
precision: int = 8,
|
|
388
|
+
nivel_confianza: float = 0.95,
|
|
389
|
+
) -> dict:
|
|
390
|
+
"""Estimate ∫ₐᵇ f(x) dx with the mean-value Monte Carlo method.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
func_str: Math expression in x.
|
|
394
|
+
a, b: Integration limits (b > a).
|
|
395
|
+
N: Number of samples.
|
|
396
|
+
seed: Optional RNG seed for reproducibility.
|
|
397
|
+
precision: Rounding digits.
|
|
398
|
+
nivel_confianza: Confidence level in (0, 1).
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Dict with the estimate, confidence interval and sample statistics.
|
|
402
|
+
"""
|
|
403
|
+
a = _clean_float(a, "a")
|
|
404
|
+
b = _clean_float(b, "b")
|
|
405
|
+
N = _clean_int(N, "N", minimum=1, maximum=MAX_MC_N)
|
|
406
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
407
|
+
nivel_confianza = _clean_float(nivel_confianza, "nivel_confianza")
|
|
408
|
+
_require(0 < nivel_confianza < 1, "nivel_confianza must be in (0, 1).")
|
|
409
|
+
_require(b > a, "b must be greater than a.")
|
|
410
|
+
if seed is not None:
|
|
411
|
+
seed = _clean_int(seed, "seed", minimum=0, maximum=2**31 - 1)
|
|
412
|
+
|
|
413
|
+
normalized = compiler.validate(func_str, variables=("x",))
|
|
414
|
+
func = monte_carlo.MonteCarloService.compilar_funcion(normalized, variables="x")
|
|
415
|
+
result = monte_carlo.MonteCarloService.valor_promedio_1d(
|
|
416
|
+
func, a, b, N=N, seed=seed, precision=precision, nivel_confianza=nivel_confianza
|
|
417
|
+
)
|
|
418
|
+
return _finalize_deep(result)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@mcp.tool()
|
|
422
|
+
def mc_valor_promedio_2d(
|
|
423
|
+
func_str: str,
|
|
424
|
+
x_a: float,
|
|
425
|
+
x_b: float,
|
|
426
|
+
y_a: float,
|
|
427
|
+
y_b: float,
|
|
428
|
+
N: int = 10000,
|
|
429
|
+
seed: int | None = None,
|
|
430
|
+
precision: int = 8,
|
|
431
|
+
nivel_confianza: float = 0.95,
|
|
432
|
+
) -> dict:
|
|
433
|
+
"""Estimate the double integral of f(x, y) over [x_a,x_b]×[y_a,y_b] by mean value."""
|
|
434
|
+
x_a, x_b = _clean_float(x_a, "x_a"), _clean_float(x_b, "x_b")
|
|
435
|
+
y_a, y_b = _clean_float(y_a, "y_a"), _clean_float(y_b, "y_b")
|
|
436
|
+
N = _clean_int(N, "N", minimum=1, maximum=MAX_MC_N)
|
|
437
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
438
|
+
nivel_confianza = _clean_float(nivel_confianza, "nivel_confianza")
|
|
439
|
+
_require(0 < nivel_confianza < 1, "nivel_confianza must be in (0, 1).")
|
|
440
|
+
_require(x_b > x_a and y_b > y_a, "upper limits must be greater than lower limits.")
|
|
441
|
+
if seed is not None:
|
|
442
|
+
seed = _clean_int(seed, "seed", minimum=0, maximum=2**31 - 1)
|
|
443
|
+
|
|
444
|
+
normalized = compiler.validate(func_str, variables=("x", "y"))
|
|
445
|
+
func = monte_carlo.MonteCarloService.compilar_funcion(normalized, variables="x y")
|
|
446
|
+
result = monte_carlo.MonteCarloService.valor_promedio_2d(
|
|
447
|
+
func, (x_a, x_b), (y_a, y_b), N=N, seed=seed, precision=precision, nivel_confianza=nivel_confianza
|
|
448
|
+
)
|
|
449
|
+
return _finalize_deep(result)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@mcp.tool()
|
|
453
|
+
def mc_valor_promedio_3d(
|
|
454
|
+
func_str: str,
|
|
455
|
+
x_a: float,
|
|
456
|
+
x_b: float,
|
|
457
|
+
y_a: float,
|
|
458
|
+
y_b: float,
|
|
459
|
+
z_a: float,
|
|
460
|
+
z_b: float,
|
|
461
|
+
N: int = 10000,
|
|
462
|
+
seed: int | None = None,
|
|
463
|
+
precision: int = 8,
|
|
464
|
+
nivel_confianza: float = 0.95,
|
|
465
|
+
) -> dict:
|
|
466
|
+
"""Estimate the triple integral of f(x,y,z) over the box [x_a,x_b]×[y_a,y_b]×[z_a,z_b]."""
|
|
467
|
+
x_a, x_b = _clean_float(x_a, "x_a"), _clean_float(x_b, "x_b")
|
|
468
|
+
y_a, y_b = _clean_float(y_a, "y_a"), _clean_float(y_b, "y_b")
|
|
469
|
+
z_a, z_b = _clean_float(z_a, "z_a"), _clean_float(z_b, "z_b")
|
|
470
|
+
N = _clean_int(N, "N", minimum=1, maximum=MAX_MC_N)
|
|
471
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
472
|
+
nivel_confianza = _clean_float(nivel_confianza, "nivel_confianza")
|
|
473
|
+
_require(0 < nivel_confianza < 1, "nivel_confianza must be in (0, 1).")
|
|
474
|
+
_require(x_b > x_a and y_b > y_a and z_b > z_a, "upper limits must be greater than lower limits.")
|
|
475
|
+
if seed is not None:
|
|
476
|
+
seed = _clean_int(seed, "seed", minimum=0, maximum=2**31 - 1)
|
|
477
|
+
|
|
478
|
+
normalized = compiler.validate(func_str, variables=("x", "y", "z"))
|
|
479
|
+
func = monte_carlo.MonteCarloService.compilar_funcion(normalized, variables="x y z")
|
|
480
|
+
result = monte_carlo.MonteCarloService.valor_promedio_3d(
|
|
481
|
+
func,
|
|
482
|
+
(x_a, x_b),
|
|
483
|
+
(y_a, y_b),
|
|
484
|
+
(z_a, z_b),
|
|
485
|
+
N=N,
|
|
486
|
+
seed=seed,
|
|
487
|
+
precision=precision,
|
|
488
|
+
nivel_confianza=nivel_confianza,
|
|
489
|
+
)
|
|
490
|
+
return _finalize_deep(result)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@mcp.tool()
|
|
494
|
+
def mc_estadistico_1d(
|
|
495
|
+
func_str: str,
|
|
496
|
+
a: float,
|
|
497
|
+
b: float,
|
|
498
|
+
N: int = 1000,
|
|
499
|
+
M: int = 100,
|
|
500
|
+
nivel_confianza: float = 0.95,
|
|
501
|
+
seed: int | None = None,
|
|
502
|
+
precision: int = 8,
|
|
503
|
+
) -> dict:
|
|
504
|
+
"""Replicated Monte Carlo experiment (M repetitions of N samples) with statistical analysis.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
func_str: Math expression in x.
|
|
508
|
+
a, b: Integration limits (b > a).
|
|
509
|
+
N: Samples per replication.
|
|
510
|
+
M: Number of replications.
|
|
511
|
+
nivel_confianza: Confidence level in (0, 1).
|
|
512
|
+
seed: Optional RNG seed.
|
|
513
|
+
precision: Rounding digits.
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
Dict with the distribution of estimates and confidence intervals.
|
|
517
|
+
"""
|
|
518
|
+
a = _clean_float(a, "a")
|
|
519
|
+
b = _clean_float(b, "b")
|
|
520
|
+
N = _clean_int(N, "N", minimum=2, maximum=MAX_MC_N // 10)
|
|
521
|
+
M = _clean_int(M, "M", minimum=2, maximum=MAX_MC_M)
|
|
522
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
523
|
+
nivel_confianza = _clean_float(nivel_confianza, "nivel_confianza")
|
|
524
|
+
_require(0 < nivel_confianza < 1, "nivel_confianza must be in (0, 1).")
|
|
525
|
+
_require(b > a, "b must be greater than a.")
|
|
526
|
+
if seed is not None:
|
|
527
|
+
seed = _clean_int(seed, "seed", minimum=0, maximum=2**31 - 1)
|
|
528
|
+
|
|
529
|
+
normalized = compiler.validate(func_str, variables=("x",))
|
|
530
|
+
func = monte_carlo.MonteCarloService.compilar_funcion(normalized, variables="x")
|
|
531
|
+
result = monte_carlo.MonteCarloService.analisis_estadistico_1d(
|
|
532
|
+
func, a, b, N=N, M=M, nivel_confianza=nivel_confianza, seed=seed, precision=precision
|
|
533
|
+
)
|
|
534
|
+
return _finalize_deep(result)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
# --------------------------------------------------------------------------
|
|
538
|
+
# Dynamic 1D tools
|
|
539
|
+
# --------------------------------------------------------------------------
|
|
540
|
+
@mcp.tool()
|
|
541
|
+
def dynamic_1d_solve(
|
|
542
|
+
func_str: str = "x",
|
|
543
|
+
model: str = "custom",
|
|
544
|
+
params: dict[str, float] | None = None,
|
|
545
|
+
x_min: float = -1.0,
|
|
546
|
+
x_max: float = 3.0,
|
|
547
|
+
t_max: float = 10.0,
|
|
548
|
+
n_phase: int = 400,
|
|
549
|
+
n_time: int = 200,
|
|
550
|
+
initial_conditions: list[float] | None = None,
|
|
551
|
+
control_enabled: bool = False,
|
|
552
|
+
) -> dict:
|
|
553
|
+
"""Analyze the 1D autonomous system x' = f(x): equilibria, stability, phase portrait and time series.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
func_str: f(x) used when model is 'custom'.
|
|
557
|
+
model: 'custom' | 'malthus' (r*x) | 'verhulst' (mu*x*(1-x/K)) | 'newton' (-k*(x-Ta)).
|
|
558
|
+
params: parameter values, e.g. {'r': 1.5} or {'mu': 1.0, 'K': 2.0}.
|
|
559
|
+
x_min, x_max: state window.
|
|
560
|
+
t_max: final time of the time series.
|
|
561
|
+
n_phase: resolution of the phase analysis.
|
|
562
|
+
n_time: number of time steps.
|
|
563
|
+
initial_conditions: starting states, e.g. [0.1, 1.0].
|
|
564
|
+
control_enabled: add a constant control term -h (verhulst/custom only).
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
Dict with equilibria, their stability, the phase portrait data and time solutions.
|
|
568
|
+
"""
|
|
569
|
+
params = params or {}
|
|
570
|
+
x_min = _clean_float(x_min, "x_min")
|
|
571
|
+
x_max = _clean_float(x_max, "x_max")
|
|
572
|
+
t_max = _clean_float(t_max, "t_max", positive=True)
|
|
573
|
+
n_phase = _clean_int(n_phase, "n_phase", minimum=10, maximum=MAX_DYN_GRID)
|
|
574
|
+
n_time = _clean_int(n_time, "n_time", minimum=2, maximum=MAX_DYN_GRID)
|
|
575
|
+
_require(x_max > x_min, "x_max must be greater than x_min.")
|
|
576
|
+
if initial_conditions is None:
|
|
577
|
+
initial_conditions = [0.5]
|
|
578
|
+
|
|
579
|
+
if model == "custom":
|
|
580
|
+
compiler.validate(func_str, variables=("x", *params.keys()))
|
|
581
|
+
elif model not in ("malthus", "verhulst", "newton"):
|
|
582
|
+
raise ValueError("model must be one of: custom, malthus, verhulst, newton.")
|
|
583
|
+
|
|
584
|
+
payload = {
|
|
585
|
+
"model": model,
|
|
586
|
+
"func_str": func_str,
|
|
587
|
+
"params": params,
|
|
588
|
+
"control_enabled": control_enabled,
|
|
589
|
+
"x_min": x_min,
|
|
590
|
+
"x_max": x_max,
|
|
591
|
+
"t_max": t_max,
|
|
592
|
+
"n_phase": n_phase,
|
|
593
|
+
"n_time": n_time,
|
|
594
|
+
"initial_conditions": initial_conditions,
|
|
595
|
+
}
|
|
596
|
+
result = dynamic_1d.Dynamic1DService.solve(payload)
|
|
597
|
+
return _finalize_deep(result)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
@mcp.tool()
|
|
601
|
+
def dynamic_1d_equilibria(
|
|
602
|
+
func_str: str = "x",
|
|
603
|
+
model: str = "custom",
|
|
604
|
+
params: dict[str, float] | None = None,
|
|
605
|
+
x_min: float = -1.0,
|
|
606
|
+
x_max: float = 3.0,
|
|
607
|
+
n_phase: int = 400,
|
|
608
|
+
control_enabled: bool = False,
|
|
609
|
+
) -> dict:
|
|
610
|
+
"""Find the equilibria of x' = f(x) and classify their stability."""
|
|
611
|
+
params = params or {}
|
|
612
|
+
x_min = _clean_float(x_min, "x_min")
|
|
613
|
+
x_max = _clean_float(x_max, "x_max")
|
|
614
|
+
n_phase = _clean_int(n_phase, "n_phase", minimum=10, maximum=MAX_DYN_GRID)
|
|
615
|
+
_require(x_max > x_min, "x_max must be greater than x_min.")
|
|
616
|
+
|
|
617
|
+
if model == "custom":
|
|
618
|
+
compiler.validate(func_str, variables=("x", *params.keys()))
|
|
619
|
+
elif model not in ("malthus", "verhulst", "newton"):
|
|
620
|
+
raise ValueError("model must be one of: custom, malthus, verhulst, newton.")
|
|
621
|
+
|
|
622
|
+
payload = {
|
|
623
|
+
"model": model,
|
|
624
|
+
"func_str": func_str,
|
|
625
|
+
"params": params,
|
|
626
|
+
"control_enabled": control_enabled,
|
|
627
|
+
"x_min": x_min,
|
|
628
|
+
"x_max": x_max,
|
|
629
|
+
"n_phase": n_phase,
|
|
630
|
+
}
|
|
631
|
+
result = dynamic_1d.Dynamic1DService.solve(payload)
|
|
632
|
+
return _finalize_deep(result)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@mcp.tool()
|
|
636
|
+
def dynamic_1d_bifurcation(
|
|
637
|
+
func_str: str = "x",
|
|
638
|
+
model: str = "custom",
|
|
639
|
+
params: dict[str, float] | None = None,
|
|
640
|
+
bif_param: str = "r",
|
|
641
|
+
bif_min: float = -1.0,
|
|
642
|
+
bif_max: float = 1.0,
|
|
643
|
+
bif_steps: int = 60,
|
|
644
|
+
x_min: float = -1.0,
|
|
645
|
+
x_max: float = 3.0,
|
|
646
|
+
n_phase: int = 400,
|
|
647
|
+
phase_params: list[float] | None = None,
|
|
648
|
+
control_enabled: bool = False,
|
|
649
|
+
) -> dict:
|
|
650
|
+
"""Bifurcation analysis of x' = f(x; bif_param): equilibria vs parameter sweep.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
func_str: f(x) used when model is 'custom'.
|
|
654
|
+
model: 'custom' | 'malthus' | 'verhulst' | 'newton'.
|
|
655
|
+
params: values of the other model parameters, e.g. {'K': 2.0}.
|
|
656
|
+
bif_param: name of the bifurcation parameter swept between bif_min and bif_max.
|
|
657
|
+
bif_min, bif_max, bif_steps: parameter sweep range/resolution.
|
|
658
|
+
x_min, x_max, n_phase: state window for finding equilibria.
|
|
659
|
+
phase_params: optional explicit parameter values for phase slices.
|
|
660
|
+
control_enabled: add a constant control term -h (verhulst/custom only).
|
|
661
|
+
|
|
662
|
+
Returns:
|
|
663
|
+
Dict with the equilibria-vs-parameter table, exact symbolic analysis and phase slices.
|
|
664
|
+
"""
|
|
665
|
+
params = params or {}
|
|
666
|
+
bif_min = _clean_float(bif_min, "bif_min")
|
|
667
|
+
bif_max = _clean_float(bif_max, "bif_max")
|
|
668
|
+
bif_steps = _clean_int(bif_steps, "bif_steps", minimum=2, maximum=MAX_BIF_STEPS)
|
|
669
|
+
x_min = _clean_float(x_min, "x_min")
|
|
670
|
+
x_max = _clean_float(x_max, "x_max")
|
|
671
|
+
n_phase = _clean_int(n_phase, "n_phase", minimum=10, maximum=MAX_DYN_GRID)
|
|
672
|
+
_require(bif_max > bif_min, "bif_max must be greater than bif_min.")
|
|
673
|
+
_require(x_max > x_min, "x_max must be greater than x_min.")
|
|
674
|
+
|
|
675
|
+
if model == "custom":
|
|
676
|
+
compiler.validate(func_str, variables=("x", bif_param, *params.keys()))
|
|
677
|
+
elif model not in ("malthus", "verhulst", "newton"):
|
|
678
|
+
raise ValueError("model must be one of: custom, malthus, verhulst, newton.")
|
|
679
|
+
_require(
|
|
680
|
+
bool(bif_param.strip()) and bif_param.strip().isidentifier(),
|
|
681
|
+
"bif_param must be a valid symbol name.",
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
payload = {
|
|
685
|
+
"model": model,
|
|
686
|
+
"func_str": func_str,
|
|
687
|
+
"params": params,
|
|
688
|
+
"control_enabled": control_enabled,
|
|
689
|
+
"bif_param": bif_param,
|
|
690
|
+
"bif_min": bif_min,
|
|
691
|
+
"bif_max": bif_max,
|
|
692
|
+
"bif_steps": bif_steps,
|
|
693
|
+
"x_min": x_min,
|
|
694
|
+
"x_max": x_max,
|
|
695
|
+
"n_phase": n_phase,
|
|
696
|
+
"phase_params": phase_params,
|
|
697
|
+
}
|
|
698
|
+
result = dynamic_1d.Dynamic1DService.bifurcation(payload)
|
|
699
|
+
result = _finalize_deep(result)
|
|
700
|
+
# The equilibria-vs-parameter table can be huge; keep it digestible for the LLM.
|
|
701
|
+
bif = result.get("bifurcation")
|
|
702
|
+
if isinstance(bif, dict):
|
|
703
|
+
bif["equilibria"] = _downsample(bif.get("equilibria") or [], 300)
|
|
704
|
+
return result
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
# --------------------------------------------------------------------------
|
|
708
|
+
# Dynamic 2D tools
|
|
709
|
+
# --------------------------------------------------------------------------
|
|
710
|
+
def _clean_dynamic2d_inputs(x0, y0, t0, t_fin, h, x_min, x_max, y_min, y_max, traj):
|
|
711
|
+
x0 = _clean_float(x0, "x0")
|
|
712
|
+
y0 = _clean_float(y0, "y0")
|
|
713
|
+
t0 = _clean_float(t0, "t0")
|
|
714
|
+
t_fin = _clean_float(t_fin, "t_fin")
|
|
715
|
+
h = _clean_float(h, "h", positive=True)
|
|
716
|
+
x_min = _clean_float(x_min, "x_min")
|
|
717
|
+
x_max = _clean_float(x_max, "x_max")
|
|
718
|
+
y_min = _clean_float(y_min, "y_min")
|
|
719
|
+
y_max = _clean_float(y_max, "y_max")
|
|
720
|
+
traj = _clean_int(traj, "cantidad_trayectorias", minimum=1, maximum=MAX_TRAJECTORIES)
|
|
721
|
+
_require(t_fin > t0, "t_fin must be greater than t0.")
|
|
722
|
+
_require(x_max > x_min and y_max > y_min, "max limits must be greater than min limits.")
|
|
723
|
+
steps = round((t_fin - t0) / h)
|
|
724
|
+
_require(1 <= steps <= MAX_DYN_STEPS, "step count must be between 1 and 5000.")
|
|
725
|
+
return x0, y0, t0, t_fin, h, x_min, x_max, y_min, y_max, traj
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
@mcp.tool()
|
|
729
|
+
def dynamic_2d_linear_solve(
|
|
730
|
+
a: float = 3.0,
|
|
731
|
+
b: float = 1.0,
|
|
732
|
+
c: float = 1.0,
|
|
733
|
+
d: float = 3.0,
|
|
734
|
+
e: float = 0.0,
|
|
735
|
+
f: float = 0.0,
|
|
736
|
+
x0: float = 1.0,
|
|
737
|
+
y0: float = 1.0,
|
|
738
|
+
t0: float = 0.0,
|
|
739
|
+
t_fin: float = 10.0,
|
|
740
|
+
h: float = 0.01,
|
|
741
|
+
x_min: float = -5.0,
|
|
742
|
+
x_max: float = 5.0,
|
|
743
|
+
y_min: float = -5.0,
|
|
744
|
+
y_max: float = 5.0,
|
|
745
|
+
cantidad_trayectorias: int = 16,
|
|
746
|
+
) -> dict:
|
|
747
|
+
"""Solve and classify the linear 2D system X' = A·X + B with constant matrix A.
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
a, b, c, d: entries of A = [[a, b], [c, d]].
|
|
751
|
+
e, f: constant forcing vector B = [e, f].
|
|
752
|
+
x0, y0, t0, t_fin, h: initial condition and integration grid.
|
|
753
|
+
x_min..y_max: window for the phase portrait.
|
|
754
|
+
cantidad_trayectorias: number of sample trajectories.
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
Dict with the classification, eigenvalues, nullclines, analytic solution and trajectories.
|
|
758
|
+
"""
|
|
759
|
+
x0, y0, t0, t_fin, h, x_min, x_max, y_min, y_max, traj = _clean_dynamic2d_inputs(
|
|
760
|
+
x0, y0, t0, t_fin, h, x_min, x_max, y_min, y_max, cantidad_trayectorias
|
|
761
|
+
)
|
|
762
|
+
payload = {
|
|
763
|
+
"a": a, "b": b, "c": c, "d": d, "e": e, "f": f,
|
|
764
|
+
"x0": x0, "y0": y0, "t0": t0, "t_fin": t_fin, "h": h,
|
|
765
|
+
"x_min": x_min, "x_max": x_max, "y_min": y_min, "y_max": y_max,
|
|
766
|
+
"cantidad_trayectorias": traj,
|
|
767
|
+
}
|
|
768
|
+
result = dynamic_2d_linear.Dynamic2DLinearService.solve(payload)
|
|
769
|
+
return _finalize_deep(result)
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
@mcp.tool()
|
|
773
|
+
def dynamic_2d_nonlinear_solve(
|
|
774
|
+
eq_x: str = "y - x",
|
|
775
|
+
eq_y: str = "x**2 - 1",
|
|
776
|
+
params: dict[str, float] | None = None,
|
|
777
|
+
mu: float = 0.0,
|
|
778
|
+
x0: float = 0.5,
|
|
779
|
+
y0: float = 0.5,
|
|
780
|
+
t0: float = 0.0,
|
|
781
|
+
t_fin: float = 10.0,
|
|
782
|
+
h: float = 0.02,
|
|
783
|
+
x_min: float = -3.0,
|
|
784
|
+
x_max: float = 3.0,
|
|
785
|
+
y_min: float = -3.0,
|
|
786
|
+
y_max: float = 3.0,
|
|
787
|
+
cantidad_trayectorias: int = 25,
|
|
788
|
+
) -> dict:
|
|
789
|
+
"""Solve and analyze the nonlinear 2D system x' = f(x,y), y' = g(x,y).
|
|
790
|
+
|
|
791
|
+
Args:
|
|
792
|
+
eq_x, eq_y: Math expressions for f and g (variables x, y, mu and extra params).
|
|
793
|
+
params: extra parameter values (e.g. {'alpha': 0.5}).
|
|
794
|
+
mu: bifurcation parameter value.
|
|
795
|
+
x0..y_max, cantidad_trayectorias: integration and portrait settings.
|
|
796
|
+
|
|
797
|
+
Returns:
|
|
798
|
+
Dict with equilibria, Jacobian-based classification, nullclines and trajectories.
|
|
799
|
+
"""
|
|
800
|
+
params = params or {}
|
|
801
|
+
x0, y0, t0, t_fin, h, x_min, x_max, y_min, y_max, traj = _clean_dynamic2d_inputs(
|
|
802
|
+
x0, y0, t0, t_fin, h, x_min, x_max, y_min, y_max, cantidad_trayectorias
|
|
803
|
+
)
|
|
804
|
+
gate = ["x", "y", "mu", *list(params.keys())]
|
|
805
|
+
compiler.validate(eq_x, variables=tuple(dict.fromkeys(gate)))
|
|
806
|
+
compiler.validate(eq_y, variables=tuple(dict.fromkeys(gate)))
|
|
807
|
+
payload = {
|
|
808
|
+
"eq_x": eq_x, "eq_y": eq_y, "params": params, "mu": mu,
|
|
809
|
+
"x0": x0, "y0": y0, "t0": t0, "t_fin": t_fin, "h": h,
|
|
810
|
+
"x_min": x_min, "x_max": x_max, "y_min": y_min, "y_max": y_max,
|
|
811
|
+
"cantidad_trayectorias": traj,
|
|
812
|
+
}
|
|
813
|
+
result = dynamic_2d_nonlinear.Dynamic2DNonLinearService.solve(payload)
|
|
814
|
+
return _finalize_deep(result)
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
@mcp.tool()
|
|
818
|
+
def dynamic_2d_conservative_solve(
|
|
819
|
+
eq_x: str = "y",
|
|
820
|
+
eq_y: str = "x - x**3",
|
|
821
|
+
mu: float = 0.0,
|
|
822
|
+
x0: float = 0.1,
|
|
823
|
+
y0: float = 0.0,
|
|
824
|
+
t0: float = 0.0,
|
|
825
|
+
t_fin: float = 15.0,
|
|
826
|
+
h: float = 0.02,
|
|
827
|
+
x_min: float = -2.5,
|
|
828
|
+
x_max: float = 2.5,
|
|
829
|
+
y_min: float = -2.5,
|
|
830
|
+
y_max: float = 2.5,
|
|
831
|
+
cantidad_trayectorias: int = 25,
|
|
832
|
+
) -> dict:
|
|
833
|
+
"""Analyze a 2D system checking conservativeness (divergence-free), Hamiltonian/energy and closed orbits.
|
|
834
|
+
|
|
835
|
+
Args:
|
|
836
|
+
eq_x, eq_y: Math expressions for x' and y'.
|
|
837
|
+
mu: parameter value.
|
|
838
|
+
x0..y_max, cantidad_trayectorias: integration and portrait settings.
|
|
839
|
+
|
|
840
|
+
Returns:
|
|
841
|
+
Dict with the divergence check, the Hamiltonian reconstruction, the
|
|
842
|
+
equilibrium classification and the trajectories.
|
|
843
|
+
"""
|
|
844
|
+
x0, y0, t0, t_fin, h, x_min, x_max, y_min, y_max, traj = _clean_dynamic2d_inputs(
|
|
845
|
+
x0, y0, t0, t_fin, h, x_min, x_max, y_min, y_max, cantidad_trayectorias
|
|
846
|
+
)
|
|
847
|
+
compiler.validate(eq_x, variables=("x", "y", "mu"))
|
|
848
|
+
compiler.validate(eq_y, variables=("x", "y", "mu"))
|
|
849
|
+
payload = {
|
|
850
|
+
"eq_x": eq_x, "eq_y": eq_y, "mu": mu,
|
|
851
|
+
"x0": x0, "y0": y0, "t0": t0, "t_fin": t_fin, "h": h,
|
|
852
|
+
"x_min": x_min, "x_max": x_max, "y_min": y_min, "y_max": y_max,
|
|
853
|
+
"cantidad_trayectorias": traj,
|
|
854
|
+
}
|
|
855
|
+
result = dynamic_2d_conservative.Dynamic2DConservativeService.solve(payload)
|
|
856
|
+
return _finalize_deep(result)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
@mcp.tool()
|
|
860
|
+
def dynamic_2d_lanchester_solve(
|
|
861
|
+
eq_x: str = "-α * y",
|
|
862
|
+
eq_y: str = "-β * x",
|
|
863
|
+
alpha: float = 1.0,
|
|
864
|
+
beta: float = 2.0,
|
|
865
|
+
gamma: float = 0.0,
|
|
866
|
+
epsilon: float = 0.0,
|
|
867
|
+
mu: float = 0.0,
|
|
868
|
+
delta: float = 0.0,
|
|
869
|
+
x0: float = 100.0,
|
|
870
|
+
y0: float = 80.0,
|
|
871
|
+
t0: float = 0.0,
|
|
872
|
+
t_fin: float = 5.0,
|
|
873
|
+
h: float = 0.01,
|
|
874
|
+
) -> dict:
|
|
875
|
+
"""Simulate a Lanchester combat model x' = f(x,y), y' = g(x,y) with analytic time-to-annihilation.
|
|
876
|
+
|
|
877
|
+
Args:
|
|
878
|
+
eq_x, eq_y: Math expressions (classic default is x'=-αy, y'=-βx).
|
|
879
|
+
alpha, beta, gamma, epsilon, mu, delta: Greek parameter values.
|
|
880
|
+
x0, y0: initial force sizes.
|
|
881
|
+
t0, t_fin, h: simulation grid.
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
Dict with the analytic winner/survivors, state equation and the numerical trajectories.
|
|
885
|
+
"""
|
|
886
|
+
alpha = _clean_float(alpha, "alpha")
|
|
887
|
+
beta = _clean_float(beta, "beta")
|
|
888
|
+
gamma = _clean_float(gamma, "gamma")
|
|
889
|
+
epsilon = _clean_float(epsilon, "epsilon")
|
|
890
|
+
mu = _clean_float(mu, "mu")
|
|
891
|
+
delta = _clean_float(delta, "delta")
|
|
892
|
+
x0 = _clean_float(x0, "x0")
|
|
893
|
+
y0 = _clean_float(y0, "y0")
|
|
894
|
+
t0 = _clean_float(t0, "t0")
|
|
895
|
+
t_fin = _clean_float(t_fin, "t_fin")
|
|
896
|
+
h = _clean_float(h, "h", positive=True)
|
|
897
|
+
_require(t_fin > t0, "t_fin must be greater than t0.")
|
|
898
|
+
steps = round((t_fin - t0) / h)
|
|
899
|
+
_require(1 <= steps <= MAX_DYN_STEPS, "step count must be between 1 and 5000.")
|
|
900
|
+
|
|
901
|
+
# Aceptar tanto los símbolos griegos (α) como sus nombres latinos (alpha).
|
|
902
|
+
_GREEK_NAMES = {
|
|
903
|
+
"alpha": "α", "beta": "β", "gamma": "γ",
|
|
904
|
+
"epsilon": "ε", "mu": "μ", "delta": "δ",
|
|
905
|
+
}
|
|
906
|
+
for latin, greek in _GREEK_NAMES.items():
|
|
907
|
+
eq_x = re.sub(rf"\b{latin}\b", greek, eq_x)
|
|
908
|
+
eq_y = re.sub(rf"\b{latin}\b", greek, eq_y)
|
|
909
|
+
|
|
910
|
+
gate = ("x", "y", "α", "β", "γ", "ε", "μ", "δ")
|
|
911
|
+
compiler.validate(eq_x, variables=gate)
|
|
912
|
+
compiler.validate(eq_y, variables=gate)
|
|
913
|
+
payload = {
|
|
914
|
+
"eq_x": eq_x, "eq_y": eq_y,
|
|
915
|
+
"alpha": alpha, "beta": beta, "gamma": gamma, "epsilon": epsilon, "mu": mu, "delta": delta,
|
|
916
|
+
"x0": x0, "y0": y0, "t0": t0, "t_fin": t_fin, "h": h,
|
|
917
|
+
}
|
|
918
|
+
result = dynamic_2d_lanchester.Dynamic2DLanchesterService.solve(payload)
|
|
919
|
+
return _finalize_deep(result)
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
@mcp.tool()
|
|
923
|
+
def dynamic_2d_nonhomogeneous_solve(
|
|
924
|
+
a: float = 0.0,
|
|
925
|
+
b: float = -1.0,
|
|
926
|
+
c: float = -9.0,
|
|
927
|
+
d: float = 0.0,
|
|
928
|
+
e: str | float = 1.0,
|
|
929
|
+
f: str | float = 9.0,
|
|
930
|
+
x0: float = 2.0,
|
|
931
|
+
y0: float = 2.0,
|
|
932
|
+
t0: float = 0.0,
|
|
933
|
+
t_fin: float = 5.0,
|
|
934
|
+
h: float = 0.01,
|
|
935
|
+
x_min: float = -5.0,
|
|
936
|
+
x_max: float = 5.0,
|
|
937
|
+
y_min: float = -5.0,
|
|
938
|
+
y_max: float = 5.0,
|
|
939
|
+
cantidad_trayectorias: int = 16,
|
|
940
|
+
) -> dict:
|
|
941
|
+
"""Solve the non-homogeneous 2D system X' = A·X + B(t) with constant or time-varying forcing.
|
|
942
|
+
|
|
943
|
+
Args:
|
|
944
|
+
a, b, c, d: entries of A = [[a, b], [c, d]].
|
|
945
|
+
e, f: forcing components B(t); a number or an expression in t (e.g. "sin(2*t)").
|
|
946
|
+
x0, y0, t0, t_fin, h: integration settings.
|
|
947
|
+
x_min..y_max, cantidad_trayectorias: portrait settings.
|
|
948
|
+
|
|
949
|
+
Returns:
|
|
950
|
+
Dict with the classification, equilibrium/particular solution, analytic solution and trajectories.
|
|
951
|
+
"""
|
|
952
|
+
x0, y0, t0, t_fin, h, x_min, x_max, y_min, y_max, traj = _clean_dynamic2d_inputs(
|
|
953
|
+
x0, y0, t0, t_fin, h, x_min, x_max, y_min, y_max, cantidad_trayectorias
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
def _forcing(value, name):
|
|
957
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
958
|
+
return float(value)
|
|
959
|
+
text = str(value)
|
|
960
|
+
normalized = compiler.validate(text, variables=("t",))
|
|
961
|
+
return normalized
|
|
962
|
+
|
|
963
|
+
e_val = _forcing(e, "e")
|
|
964
|
+
f_val = _forcing(f, "f")
|
|
965
|
+
|
|
966
|
+
payload = {
|
|
967
|
+
"a": a, "b": b, "c": c, "d": d, "e": e_val, "f": f_val,
|
|
968
|
+
"x0": x0, "y0": y0, "t0": t0, "t_fin": t_fin, "h": h,
|
|
969
|
+
"x_min": x_min, "x_max": x_max, "y_min": y_min, "y_max": y_max,
|
|
970
|
+
"cantidad_trayectorias": traj,
|
|
971
|
+
}
|
|
972
|
+
result = dynamic_2d_non_homogeneous.Dynamic2DNonHomogeneousService.solve(payload)
|
|
973
|
+
return _finalize_deep(result)
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
# --------------------------------------------------------------------------
|
|
977
|
+
# Root finding (extended)
|
|
978
|
+
# --------------------------------------------------------------------------
|
|
979
|
+
def _compile_root(func_str: str) -> Callable:
|
|
980
|
+
normalized = compiler.validate(func_str, variables=("x",))
|
|
981
|
+
return root_finding.RootFindingService.compilar_funcion(normalized)
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
@mcp.tool()
|
|
985
|
+
def root_newton_raphson(
|
|
986
|
+
func_str: str,
|
|
987
|
+
x0: float,
|
|
988
|
+
tol: float = 1e-6,
|
|
989
|
+
max_iter: int = 100,
|
|
990
|
+
precision: int = 8,
|
|
991
|
+
) -> dict:
|
|
992
|
+
"""Find a root of f(x) = 0 with the Newton-Raphson method (numeric derivative).
|
|
993
|
+
|
|
994
|
+
Args:
|
|
995
|
+
func_str: Math expression in x.
|
|
996
|
+
x0: Initial guess.
|
|
997
|
+
tol: Convergence tolerance.
|
|
998
|
+
max_iter: Maximum iterations.
|
|
999
|
+
precision: Rounding digits.
|
|
1000
|
+
|
|
1001
|
+
Returns:
|
|
1002
|
+
Dict with the root, iteration table and convergence flag.
|
|
1003
|
+
"""
|
|
1004
|
+
x0 = _clean_float(x0, "x0")
|
|
1005
|
+
tol = _clean_float(tol, "tol", positive=True)
|
|
1006
|
+
max_iter = _clean_int(max_iter, "max_iter", minimum=1, maximum=MAX_ITERATIONS)
|
|
1007
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
1008
|
+
|
|
1009
|
+
func = _compile_root(func_str)
|
|
1010
|
+
result = root_finding.RootFindingService.newton_raphson(
|
|
1011
|
+
func, x0, tol=tol, max_iter=max_iter, precision=precision
|
|
1012
|
+
)
|
|
1013
|
+
return _finalize(result)
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
@mcp.tool()
|
|
1017
|
+
def root_punto_fijo(
|
|
1018
|
+
g_str: str,
|
|
1019
|
+
x0: float,
|
|
1020
|
+
tol: float = 1e-6,
|
|
1021
|
+
max_iter: int = 100,
|
|
1022
|
+
precision: int = 8,
|
|
1023
|
+
) -> dict:
|
|
1024
|
+
"""Find a fixed point of g(x) = x with the fixed-point iteration method.
|
|
1025
|
+
|
|
1026
|
+
Converges when |g'(x)| < 1 near the root (Lipschitz check is reported).
|
|
1027
|
+
|
|
1028
|
+
Args:
|
|
1029
|
+
g_str: Math expression for the iteration function g(x).
|
|
1030
|
+
x0: Initial guess.
|
|
1031
|
+
tol: Convergence tolerance.
|
|
1032
|
+
max_iter: Maximum iterations.
|
|
1033
|
+
precision: Rounding digits.
|
|
1034
|
+
|
|
1035
|
+
Returns:
|
|
1036
|
+
Dict with the fixed point, the Lipschitz check, iteration table and convergence flag.
|
|
1037
|
+
"""
|
|
1038
|
+
x0 = _clean_float(x0, "x0")
|
|
1039
|
+
tol = _clean_float(tol, "tol", positive=True)
|
|
1040
|
+
max_iter = _clean_int(max_iter, "max_iter", minimum=1, maximum=MAX_ITERATIONS)
|
|
1041
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
1042
|
+
|
|
1043
|
+
func = _compile_root(g_str)
|
|
1044
|
+
result = root_finding.RootFindingService.punto_fijo(
|
|
1045
|
+
func, x0, tol=tol, max_iter=max_iter, precision=precision
|
|
1046
|
+
)
|
|
1047
|
+
return _finalize(result)
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
@mcp.tool()
|
|
1051
|
+
def root_aitken(
|
|
1052
|
+
g_str: str,
|
|
1053
|
+
x0: float,
|
|
1054
|
+
tol: float = 1e-6,
|
|
1055
|
+
max_iter: int = 100,
|
|
1056
|
+
precision: int = 8,
|
|
1057
|
+
) -> dict:
|
|
1058
|
+
"""Accelerate fixed-point iteration with Aitken's delta-squared method.
|
|
1059
|
+
|
|
1060
|
+
Args:
|
|
1061
|
+
g_str: Math expression for the iteration function g(x).
|
|
1062
|
+
x0: Initial guess.
|
|
1063
|
+
tol: Convergence tolerance.
|
|
1064
|
+
max_iter: Maximum iterations.
|
|
1065
|
+
precision: Rounding digits.
|
|
1066
|
+
|
|
1067
|
+
Returns:
|
|
1068
|
+
Dict with the accelerated root, iteration table and convergence flag.
|
|
1069
|
+
"""
|
|
1070
|
+
x0 = _clean_float(x0, "x0")
|
|
1071
|
+
tol = _clean_float(tol, "tol", positive=True)
|
|
1072
|
+
max_iter = _clean_int(max_iter, "max_iter", minimum=1, maximum=MAX_ITERATIONS)
|
|
1073
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
1074
|
+
|
|
1075
|
+
func = _compile_root(g_str)
|
|
1076
|
+
result = root_finding.RootFindingService.aitken(
|
|
1077
|
+
func, x0, tol=tol, max_iter=max_iter, precision=precision
|
|
1078
|
+
)
|
|
1079
|
+
return _finalize(result)
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
@mcp.tool()
|
|
1083
|
+
def root_comparar(
|
|
1084
|
+
func_str: str,
|
|
1085
|
+
g_str: str,
|
|
1086
|
+
a: float,
|
|
1087
|
+
b: float,
|
|
1088
|
+
x0: float,
|
|
1089
|
+
tol: float = 1e-6,
|
|
1090
|
+
max_iter: int = 100,
|
|
1091
|
+
precision: int = 8,
|
|
1092
|
+
) -> dict:
|
|
1093
|
+
"""Compare bisection, fixed-point, Newton-Raphson and Aitken on the same problem.
|
|
1094
|
+
|
|
1095
|
+
Args:
|
|
1096
|
+
func_str: Math expression for f(x) (used by bisection/Newton).
|
|
1097
|
+
g_str: Math expression for g(x) (used by fixed-point/Aitken).
|
|
1098
|
+
a, b: Bracket for bisection (f(a) and f(b) must differ in sign).
|
|
1099
|
+
x0: Initial guess.
|
|
1100
|
+
tol, max_iter, precision: Shared tolerances.
|
|
1101
|
+
|
|
1102
|
+
Returns:
|
|
1103
|
+
Dict with per-method results.
|
|
1104
|
+
"""
|
|
1105
|
+
a = _clean_float(a, "a")
|
|
1106
|
+
b = _clean_float(b, "b")
|
|
1107
|
+
x0 = _clean_float(x0, "x0")
|
|
1108
|
+
tol = _clean_float(tol, "tol", positive=True)
|
|
1109
|
+
max_iter = _clean_int(max_iter, "max_iter", minimum=1, maximum=MAX_ITERATIONS)
|
|
1110
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
1111
|
+
_require(b > a, "b must be greater than a.")
|
|
1112
|
+
|
|
1113
|
+
compiler.validate(func_str, variables=("x",))
|
|
1114
|
+
compiler.validate(g_str, variables=("x",))
|
|
1115
|
+
result = root_finding.RootFindingService.comparar_metodos(
|
|
1116
|
+
func_str, g_str, a, b, x0, tol=tol, max_iter=max_iter, precision=precision
|
|
1117
|
+
)
|
|
1118
|
+
return _finalize(result)
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
# --------------------------------------------------------------------------
|
|
1122
|
+
# Integration (extended)
|
|
1123
|
+
# --------------------------------------------------------------------------
|
|
1124
|
+
def _compile_integrand(func_str: str) -> Callable:
|
|
1125
|
+
normalized = compiler.validate(func_str, variables=("x",))
|
|
1126
|
+
return integration.IntegrationService.compilar_funcion(normalized)
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def _integral_bounds(a: float, b: float, n: int, precision: int, epsilon):
|
|
1130
|
+
a = _clean_float(a, "a")
|
|
1131
|
+
b = _clean_float(b, "b")
|
|
1132
|
+
n = _clean_int(n, "n", minimum=1, maximum=MAX_SUBINTERVALS)
|
|
1133
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
1134
|
+
_require(b > a, "b must be greater than a.")
|
|
1135
|
+
if epsilon is not None:
|
|
1136
|
+
epsilon = _clean_float(epsilon, "epsilon")
|
|
1137
|
+
return a, b, n, precision, epsilon
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
@mcp.tool()
|
|
1141
|
+
def integral_rectangulo(
|
|
1142
|
+
func_str: str,
|
|
1143
|
+
a: float,
|
|
1144
|
+
b: float,
|
|
1145
|
+
n: int,
|
|
1146
|
+
epsilon: float | None = None,
|
|
1147
|
+
precision: int = 8,
|
|
1148
|
+
) -> dict:
|
|
1149
|
+
"""Approximate ∫ₐᵇ f(x) dx with the composite midpoint (rectangle) rule.
|
|
1150
|
+
|
|
1151
|
+
Args:
|
|
1152
|
+
func_str: Math expression in x.
|
|
1153
|
+
a, b: Integration limits (b > a).
|
|
1154
|
+
n: Number of subintervals.
|
|
1155
|
+
epsilon: Optional point in [a, b] for the truncation error bound.
|
|
1156
|
+
precision: Rounding digits.
|
|
1157
|
+
|
|
1158
|
+
Returns:
|
|
1159
|
+
Dict with the integral, error estimates and table.
|
|
1160
|
+
"""
|
|
1161
|
+
a, b, n, precision, epsilon = _integral_bounds(a, b, n, precision, epsilon)
|
|
1162
|
+
func = _compile_integrand(func_str)
|
|
1163
|
+
result = integration.IntegrationService.rectangulo_compuesto(
|
|
1164
|
+
func, a, b, n=n, precision=precision, epsilon=epsilon
|
|
1165
|
+
)
|
|
1166
|
+
return _finalize(result)
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
@mcp.tool()
|
|
1170
|
+
def integral_trapecio(
|
|
1171
|
+
func_str: str,
|
|
1172
|
+
a: float,
|
|
1173
|
+
b: float,
|
|
1174
|
+
n: int,
|
|
1175
|
+
epsilon: float | None = None,
|
|
1176
|
+
precision: int = 8,
|
|
1177
|
+
) -> dict:
|
|
1178
|
+
"""Approximate ∫ₐᵇ f(x) dx with the composite trapezoidal rule.
|
|
1179
|
+
|
|
1180
|
+
Args:
|
|
1181
|
+
func_str: Math expression in x.
|
|
1182
|
+
a, b: Integration limits (b > a).
|
|
1183
|
+
n: Number of subintervals.
|
|
1184
|
+
epsilon: Optional point in [a, b] for the truncation error bound.
|
|
1185
|
+
precision: Rounding digits.
|
|
1186
|
+
|
|
1187
|
+
Returns:
|
|
1188
|
+
Dict with the integral, error estimates and table.
|
|
1189
|
+
"""
|
|
1190
|
+
a, b, n, precision, epsilon = _integral_bounds(a, b, n, precision, epsilon)
|
|
1191
|
+
func = _compile_integrand(func_str)
|
|
1192
|
+
result = integration.IntegrationService.trapecio_compuesto(
|
|
1193
|
+
func, a, b, n=n, precision=precision, epsilon=epsilon
|
|
1194
|
+
)
|
|
1195
|
+
return _finalize(result)
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
@mcp.tool()
|
|
1199
|
+
def integral_simpson38(
|
|
1200
|
+
func_str: str,
|
|
1201
|
+
a: float,
|
|
1202
|
+
b: float,
|
|
1203
|
+
n: int,
|
|
1204
|
+
epsilon: float | None = None,
|
|
1205
|
+
precision: int = 8,
|
|
1206
|
+
) -> dict:
|
|
1207
|
+
"""Approximate ∫ₐᵇ f(x) dx with the composite Simpson 3/8 rule.
|
|
1208
|
+
|
|
1209
|
+
Args:
|
|
1210
|
+
func_str: Math expression in x.
|
|
1211
|
+
a, b: Integration limits (b > a).
|
|
1212
|
+
n: Number of subintervals (must be a multiple of 3).
|
|
1213
|
+
epsilon: Optional point in [a, b] for the truncation error bound.
|
|
1214
|
+
precision: Rounding digits.
|
|
1215
|
+
|
|
1216
|
+
Returns:
|
|
1217
|
+
Dict with the integral, error estimates and table.
|
|
1218
|
+
"""
|
|
1219
|
+
a, b, n, precision, epsilon = _integral_bounds(a, b, n, precision, epsilon)
|
|
1220
|
+
_require(n % 3 == 0, "n must be a multiple of 3 for Simpson 3/8.")
|
|
1221
|
+
func = _compile_integrand(func_str)
|
|
1222
|
+
result = integration.IntegrationService.simpson_38_compuesto(
|
|
1223
|
+
func, a, b, n=n, precision=precision, epsilon=epsilon
|
|
1224
|
+
)
|
|
1225
|
+
return _finalize(result)
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
@mcp.tool()
|
|
1229
|
+
def integral_comparar(
|
|
1230
|
+
func_str: str,
|
|
1231
|
+
a: float,
|
|
1232
|
+
b: float,
|
|
1233
|
+
n: int,
|
|
1234
|
+
epsilon: float | None = None,
|
|
1235
|
+
precision: int = 8,
|
|
1236
|
+
) -> dict:
|
|
1237
|
+
"""Compare rectangle, trapezoid, Simpson 1/3 and Simpson 3/8 on the same integral.
|
|
1238
|
+
|
|
1239
|
+
Args:
|
|
1240
|
+
func_str: Math expression in x.
|
|
1241
|
+
a, b: Integration limits (b > a).
|
|
1242
|
+
n: Number of subintervals (Simpson 1/3 needs even, 3/8 needs multiple of 3).
|
|
1243
|
+
epsilon, precision: Error-bound and rounding settings.
|
|
1244
|
+
|
|
1245
|
+
Returns:
|
|
1246
|
+
Dict with per-method results and success flags.
|
|
1247
|
+
"""
|
|
1248
|
+
a, b, n, precision, epsilon = _integral_bounds(a, b, n, precision, epsilon)
|
|
1249
|
+
compiler.validate(func_str, variables=("x",))
|
|
1250
|
+
result = integration.IntegrationService.comparar_metodos(
|
|
1251
|
+
func_str, a, b, n=n, precision=precision, epsilon=epsilon
|
|
1252
|
+
)
|
|
1253
|
+
return _finalize(result)
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
# --------------------------------------------------------------------------
|
|
1257
|
+
# ODE (Euler / Heun) and Monte Carlo convergence
|
|
1258
|
+
# --------------------------------------------------------------------------
|
|
1259
|
+
def _ode_solve(metodo: str, ecuacion_str, x0, y0, xf, h, tol, precision) -> dict:
|
|
1260
|
+
x0 = _clean_float(x0, "x0")
|
|
1261
|
+
y0 = _clean_float(y0, "y0")
|
|
1262
|
+
xf = _clean_float(xf, "xf")
|
|
1263
|
+
h = _clean_float(h, "h", positive=True)
|
|
1264
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
1265
|
+
_require(xf > x0, "xf must be greater than x0.")
|
|
1266
|
+
if tol is not None:
|
|
1267
|
+
tol = _clean_float(tol, "tol", positive=True)
|
|
1268
|
+
|
|
1269
|
+
n_steps = round((xf - x0) / h)
|
|
1270
|
+
_require(1 <= n_steps <= MAX_ODE_STEPS, "step count must be between 1 and 2000.")
|
|
1271
|
+
|
|
1272
|
+
normalized = compiler.validate(ecuacion_str, variables=("x", "y"))
|
|
1273
|
+
result = ode.ODEService.ejecutar_metodo(
|
|
1274
|
+
metodo, normalized, x0, y0, xf, h, precision=precision, tol=tol
|
|
1275
|
+
)
|
|
1276
|
+
return _finalize(result, downsample_keys=("x_plot", "y_plot", "y_exacta_plot"))
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
@mcp.tool()
|
|
1280
|
+
def ode_euler(
|
|
1281
|
+
ecuacion_str: str,
|
|
1282
|
+
x0: float,
|
|
1283
|
+
y0: float,
|
|
1284
|
+
xf: float,
|
|
1285
|
+
h: float,
|
|
1286
|
+
tol: float | None = None,
|
|
1287
|
+
precision: int = 8,
|
|
1288
|
+
) -> dict:
|
|
1289
|
+
"""Solve y'(x) = f(x, y), y(x0) = y0 with the explicit Euler method (1st order).
|
|
1290
|
+
|
|
1291
|
+
Args:
|
|
1292
|
+
ecuacion_str: Right-hand side f(x, y).
|
|
1293
|
+
x0, y0: Initial condition.
|
|
1294
|
+
xf: Final x (must be > x0).
|
|
1295
|
+
h: Step size (must be positive).
|
|
1296
|
+
tol: Optional tolerance flag for the final error.
|
|
1297
|
+
precision: Rounding digits.
|
|
1298
|
+
|
|
1299
|
+
Returns:
|
|
1300
|
+
Dict with the numerical solution, exact solution (when available) and errors.
|
|
1301
|
+
"""
|
|
1302
|
+
return _ode_solve("euler", ecuacion_str, x0, y0, xf, h, tol, precision)
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
@mcp.tool()
|
|
1306
|
+
def ode_heun(
|
|
1307
|
+
ecuacion_str: str,
|
|
1308
|
+
x0: float,
|
|
1309
|
+
y0: float,
|
|
1310
|
+
xf: float,
|
|
1311
|
+
h: float,
|
|
1312
|
+
tol: float | None = None,
|
|
1313
|
+
precision: int = 8,
|
|
1314
|
+
) -> dict:
|
|
1315
|
+
"""Solve y'(x) = f(x, y), y(x0) = y0 with Heun's predictor-corrector method (2nd order).
|
|
1316
|
+
|
|
1317
|
+
Args:
|
|
1318
|
+
ecuacion_str: Right-hand side f(x, y).
|
|
1319
|
+
x0, y0: Initial condition.
|
|
1320
|
+
xf: Final x (must be > x0).
|
|
1321
|
+
h: Step size (must be positive).
|
|
1322
|
+
tol: Optional tolerance flag for the final error.
|
|
1323
|
+
precision: Rounding digits.
|
|
1324
|
+
|
|
1325
|
+
Returns:
|
|
1326
|
+
Dict with the numerical solution, exact solution (when available) and errors.
|
|
1327
|
+
"""
|
|
1328
|
+
return _ode_solve("heun", ecuacion_str, x0, y0, xf, h, tol, precision)
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
@mcp.tool()
|
|
1332
|
+
def mc_convergencia_1d(
|
|
1333
|
+
func_str: str,
|
|
1334
|
+
a: float,
|
|
1335
|
+
b: float,
|
|
1336
|
+
N: int = 10000,
|
|
1337
|
+
seed: int | None = None,
|
|
1338
|
+
precision: int = 8,
|
|
1339
|
+
) -> dict:
|
|
1340
|
+
"""Show how the mean-value Monte Carlo estimate converges as samples accumulate.
|
|
1341
|
+
|
|
1342
|
+
Args:
|
|
1343
|
+
func_str: Math expression in x.
|
|
1344
|
+
a, b: Integration limits (b > a).
|
|
1345
|
+
N: Total number of samples.
|
|
1346
|
+
seed: Optional RNG seed.
|
|
1347
|
+
precision: Rounding digits.
|
|
1348
|
+
|
|
1349
|
+
Returns:
|
|
1350
|
+
Dict with the cumulative running average over the samples.
|
|
1351
|
+
"""
|
|
1352
|
+
a = _clean_float(a, "a")
|
|
1353
|
+
b = _clean_float(b, "b")
|
|
1354
|
+
N = _clean_int(N, "N", minimum=1, maximum=MAX_MC_N)
|
|
1355
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
1356
|
+
_require(b > a, "b must be greater than a.")
|
|
1357
|
+
if seed is not None:
|
|
1358
|
+
seed = _clean_int(seed, "seed", minimum=0, maximum=2**31 - 1)
|
|
1359
|
+
|
|
1360
|
+
normalized = compiler.validate(func_str, variables=("x",))
|
|
1361
|
+
func = monte_carlo.MonteCarloService.compilar_funcion(normalized, variables="x")
|
|
1362
|
+
result = monte_carlo.MonteCarloService.convergencia_1d(
|
|
1363
|
+
func, a, b, N=N, seed=seed, precision=precision
|
|
1364
|
+
)
|
|
1365
|
+
return _finalize_deep(result)
|
|
1366
|
+
|
|
1367
|
+
|
|
1368
|
+
# --------------------------------------------------------------------------
|
|
1369
|
+
# Differentiation
|
|
1370
|
+
# --------------------------------------------------------------------------
|
|
1371
|
+
@mcp.tool()
|
|
1372
|
+
def finite_differences(
|
|
1373
|
+
func_str: str,
|
|
1374
|
+
x_val: float,
|
|
1375
|
+
h: float = 1e-5,
|
|
1376
|
+
precision: int = 8,
|
|
1377
|
+
) -> dict:
|
|
1378
|
+
"""Approximate the first and second derivatives of f(x) at x with finite differences.
|
|
1379
|
+
|
|
1380
|
+
Computes forward, backward and central differences for the first derivative,
|
|
1381
|
+
plus the central second derivative, and compares each against the exact
|
|
1382
|
+
derivative from SymPy.
|
|
1383
|
+
|
|
1384
|
+
Args:
|
|
1385
|
+
func_str: Math expression in x.
|
|
1386
|
+
x_val: Point where the derivative is evaluated.
|
|
1387
|
+
h: Step size (small, positive).
|
|
1388
|
+
precision: Rounding digits.
|
|
1389
|
+
|
|
1390
|
+
Returns:
|
|
1391
|
+
Dict with the exact and numerical derivatives and their errors.
|
|
1392
|
+
"""
|
|
1393
|
+
x_val = _clean_float(x_val, "x_val")
|
|
1394
|
+
h = _clean_float(h, "h", positive=True)
|
|
1395
|
+
precision = _clean_int(precision, "precision", minimum=0, maximum=MAX_PRECISION)
|
|
1396
|
+
_require(h < 1.0, "h should be small (typically <= 1e-3).")
|
|
1397
|
+
|
|
1398
|
+
normalized = compiler.validate(func_str, variables=("x",))
|
|
1399
|
+
result = differentiation.DifferentiationService.calcular_diferencias_completas(
|
|
1400
|
+
normalized, x_val=x_val, h=h, precision=precision
|
|
1401
|
+
)
|
|
1402
|
+
return _finalize(result)
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
def run() -> None:
|
|
1406
|
+
"""Run the MCP server with the transport from ``MCP_TRANSPORT`` (default stdio)."""
|
|
1407
|
+
transport = os.environ.get("MCP_TRANSPORT", "stdio").lower()
|
|
1408
|
+
if transport not in ("stdio", "streamable-http", "sse"):
|
|
1409
|
+
transport = "stdio"
|
|
1410
|
+
mcp.run(transport=transport)
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
if __name__ == "__main__":
|
|
1414
|
+
run()
|