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
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# backend/app/methods/dynamic_2d_nonlinear.py
|
|
2
|
+
import math
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import sympy as sp
|
|
7
|
+
from sympy.parsing.sympy_parser import (
|
|
8
|
+
implicit_multiplication_application,
|
|
9
|
+
standard_transformations,
|
|
10
|
+
)
|
|
11
|
+
from .utils import safe_parse_expr
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Dynamic2DNonLinearService:
|
|
15
|
+
@staticmethod
|
|
16
|
+
def clasificar_punto(traza, determinante, discriminante, autovalores) -> tuple:
|
|
17
|
+
if abs(determinante) < 1e-10:
|
|
18
|
+
clasif = "Punto Degenerado (No Hiperbólico)"
|
|
19
|
+
comp = "El Jacobiano es singular (al menos un autovalor es cero). Falla la linealización. Esto suele indicar una variedad central donde ocurren bifurcaciones estáticas (Silla-Nodo, Transcrítica o Pitchfork)."
|
|
20
|
+
return clasif, comp
|
|
21
|
+
|
|
22
|
+
elif determinante < 0:
|
|
23
|
+
clasif = "Silla"
|
|
24
|
+
comp = "Trayectorias se acercan por una dirección (variedad estable) y se alejan por otra (variedad inestable). Punto hiperbólico."
|
|
25
|
+
|
|
26
|
+
elif discriminante > 1e-10:
|
|
27
|
+
if traza < 0:
|
|
28
|
+
clasif = "Nodo Estable"
|
|
29
|
+
comp = "Trayectorias tienden al equilibrio asintóticamente sin oscilar (Sumidero puro). Punto hiperbólico."
|
|
30
|
+
elif traza > 0:
|
|
31
|
+
clasif = "Nodo Inestable"
|
|
32
|
+
comp = "Trayectorias se alejan del equilibrio sin oscilar (Fuente pura). Punto hiperbólico."
|
|
33
|
+
else:
|
|
34
|
+
clasif = "Nodo con traza cero (Degenerado)"
|
|
35
|
+
comp = "Comportamiento nodal limítrofe."
|
|
36
|
+
|
|
37
|
+
elif abs(discriminante) <= 1e-10:
|
|
38
|
+
if traza < 0:
|
|
39
|
+
clasif = "Nodo Degenerado Estable"
|
|
40
|
+
comp = "Trayectorias convergen al punto, con un autovector dominante. Punto hiperbólico."
|
|
41
|
+
elif traza > 0:
|
|
42
|
+
clasif = "Nodo Degenerado Inestable"
|
|
43
|
+
comp = "Trayectorias divergen del punto, con un autovector dominante. Punto hiperbólico."
|
|
44
|
+
else:
|
|
45
|
+
clasif = "Caso No Hiperbólico"
|
|
46
|
+
comp = "Falla el teorema de Hartman-Grobman."
|
|
47
|
+
|
|
48
|
+
else:
|
|
49
|
+
parte_real = float(np.real(autovalores[0]))
|
|
50
|
+
if abs(parte_real) < 1e-10:
|
|
51
|
+
clasif = "Centro o Bifurcación de Hopf (No Hiperbólico)"
|
|
52
|
+
comp = "Los autovalores son puramente imaginarios. Hartman-Grobman falla. Físicamente, el punto puede ser un 'Centro' conservativo, o estar en el punto crítico de una Bifurcación de Hopf (nacimiento/muerte de un Ciclo Límite). Confirmar con Funciones de Lyapunov o cambio a Coordenadas Polares."
|
|
53
|
+
elif parte_real < 0:
|
|
54
|
+
clasif = "Foco Estable"
|
|
55
|
+
comp = "Trayectorias giran en espiral convergiendo hacia el equilibrio (Atractor). Punto hiperbólico."
|
|
56
|
+
else:
|
|
57
|
+
clasif = "Foco Inestable"
|
|
58
|
+
comp = "Trayectorias giran en espiral alejándose del equilibrio (Repulsor). Punto hiperbólico."
|
|
59
|
+
|
|
60
|
+
return clasif, comp
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def solve(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
64
|
+
eq_x_str = payload.get("eq_x", "y - x")
|
|
65
|
+
eq_y_str = payload.get("eq_y", "x**2 - 1")
|
|
66
|
+
params = payload.get("params", {}) or {}
|
|
67
|
+
params = {str(k): float(v) for k, v in params.items()}
|
|
68
|
+
mu_val = float(params.pop("mu", payload.get("mu", 0.0)))
|
|
69
|
+
x0 = float(payload.get("x0", 0.5))
|
|
70
|
+
y0 = float(payload.get("y0", 0.5))
|
|
71
|
+
t0 = float(payload.get("t0", 0.0))
|
|
72
|
+
t_fin = float(payload.get("t_fin", 10.0))
|
|
73
|
+
h = float(payload.get("h", 0.02))
|
|
74
|
+
x_min = float(payload.get("x_min", -4.0))
|
|
75
|
+
x_max = float(payload.get("x_max", 4.0))
|
|
76
|
+
y_min = float(payload.get("y_min", -4.0))
|
|
77
|
+
y_max = float(payload.get("y_max", 4.0))
|
|
78
|
+
cantidad_trayectorias = int(payload.get("cantidad_trayectorias", 25))
|
|
79
|
+
|
|
80
|
+
x, y, mu_sym = sp.symbols("x y mu", real=True)
|
|
81
|
+
param_symbols = {name: sp.Symbol(name, real=True) for name in params.keys()}
|
|
82
|
+
transformations = standard_transformations + (
|
|
83
|
+
implicit_multiplication_application,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
local_dict = {"x": x, "y": y, "mu": mu_sym, **param_symbols}
|
|
88
|
+
gate_allowed = list(dict.fromkeys(["x", "y", "mu"] + list(params.keys())))
|
|
89
|
+
f_raw = safe_parse_expr(
|
|
90
|
+
eq_x_str,
|
|
91
|
+
local_dict=local_dict,
|
|
92
|
+
allowed_symbols=gate_allowed,
|
|
93
|
+
transformations=transformations,
|
|
94
|
+
)
|
|
95
|
+
g_raw = safe_parse_expr(
|
|
96
|
+
eq_y_str,
|
|
97
|
+
local_dict=local_dict,
|
|
98
|
+
allowed_symbols=gate_allowed,
|
|
99
|
+
transformations=transformations,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
subs_map = {mu_sym: mu_val, **{param_symbols[k]: v for k, v in params.items()}}
|
|
103
|
+
f = f_raw.subs(subs_map)
|
|
104
|
+
g = g_raw.subs(subs_map)
|
|
105
|
+
|
|
106
|
+
free_symbols = {str(sym) for sym in f.free_symbols.union(g.free_symbols)}
|
|
107
|
+
missing = sorted(sym for sym in free_symbols if sym not in {"x", "y"})
|
|
108
|
+
if missing:
|
|
109
|
+
raise ValueError(f"Faltan parametros: {', '.join(missing)}")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
raise ValueError(f"Error de sintaxis en las ecuaciones: {str(e)}")
|
|
112
|
+
|
|
113
|
+
J = sp.Matrix([[sp.diff(f, x), sp.diff(f, y)], [sp.diff(g, x), sp.diff(g, y)]])
|
|
114
|
+
|
|
115
|
+
sols = sp.nonlinsolve([f, g], [x, y])
|
|
116
|
+
puntos_equilibrio = []
|
|
117
|
+
|
|
118
|
+
if isinstance(sols, sp.FiniteSet):
|
|
119
|
+
for sol in sols:
|
|
120
|
+
try:
|
|
121
|
+
x_val = complex(sol[0])
|
|
122
|
+
y_val = complex(sol[1])
|
|
123
|
+
if abs(x_val.imag) < 1e-9 and abs(y_val.imag) < 1e-9:
|
|
124
|
+
puntos_equilibrio.append((x_val.real, y_val.real))
|
|
125
|
+
except:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
analisis_puntos = []
|
|
129
|
+
for pto in puntos_equilibrio:
|
|
130
|
+
px, py = pto[0], pto[1]
|
|
131
|
+
J_eval = J.subs({x: px, y: py})
|
|
132
|
+
J_num = np.array(J_eval.tolist(), dtype=float)
|
|
133
|
+
|
|
134
|
+
traza = float(np.trace(J_num))
|
|
135
|
+
determinante = float(np.linalg.det(J_num))
|
|
136
|
+
discriminante = traza**2 - 4 * determinante
|
|
137
|
+
autovalores, autovectores = np.linalg.eig(J_num)
|
|
138
|
+
|
|
139
|
+
clasif, comp = Dynamic2DNonLinearService.clasificar_punto(
|
|
140
|
+
traza, determinante, discriminante, autovalores
|
|
141
|
+
)
|
|
142
|
+
autos_formateados = [
|
|
143
|
+
{"real": float(np.real(av)), "imag": float(np.imag(av))}
|
|
144
|
+
for av in autovalores
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
j_local_formateado = [
|
|
148
|
+
[round(float(J_num[0][0]), 4), round(float(J_num[0][1]), 4)],
|
|
149
|
+
[round(float(J_num[1][0]), 4), round(float(J_num[1][1]), 4)],
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
analisis_puntos.append(
|
|
153
|
+
{
|
|
154
|
+
"x": px,
|
|
155
|
+
"y": py,
|
|
156
|
+
"traza": traza,
|
|
157
|
+
"determinante": determinante,
|
|
158
|
+
"discriminante": discriminante,
|
|
159
|
+
"clasificacion": clasif,
|
|
160
|
+
"comportamiento": comp,
|
|
161
|
+
"autovalores": autos_formateados,
|
|
162
|
+
"jacobiano_local": j_local_formateado,
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
f_num = sp.lambdify((x, y), f, "numpy")
|
|
167
|
+
g_num = sp.lambdify((x, y), g, "numpy")
|
|
168
|
+
|
|
169
|
+
def rk4_step(X_curr, step_h):
|
|
170
|
+
try:
|
|
171
|
+
v_x, v_y = float(X_curr[0]), float(X_curr[1])
|
|
172
|
+
k1_x = f_num(v_x, v_y)
|
|
173
|
+
k1_y = g_num(v_x, v_y)
|
|
174
|
+
|
|
175
|
+
k2_x = f_num(v_x + step_h * k1_x / 2, v_y + step_h * k1_y / 2)
|
|
176
|
+
k2_y = g_num(v_x + step_h * k1_x / 2, v_y + step_h * k1_y / 2)
|
|
177
|
+
|
|
178
|
+
k3_x = f_num(v_x + step_h * k2_x / 2, v_y + step_h * k2_y / 2)
|
|
179
|
+
k3_y = g_num(v_x + step_h * k2_x / 2, v_y + step_h * k2_y / 2)
|
|
180
|
+
|
|
181
|
+
k4_x = f_num(v_x + step_h * k3_x, v_y + step_h * k3_y)
|
|
182
|
+
k4_y = g_num(v_x + step_h * k3_x, v_y + step_h * k3_y)
|
|
183
|
+
|
|
184
|
+
new_x = v_x + (step_h / 6) * (k1_x + 2 * k2_x + 2 * k3_x + k4_x)
|
|
185
|
+
new_y = v_y + (step_h / 6) * (k1_y + 2 * k2_y + 2 * k3_y + k4_y)
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
math.isnan(new_x)
|
|
189
|
+
or math.isnan(new_y)
|
|
190
|
+
or abs(new_x) > 1e6
|
|
191
|
+
or abs(new_y) > 1e6
|
|
192
|
+
):
|
|
193
|
+
return None
|
|
194
|
+
return np.array([new_x, new_y])
|
|
195
|
+
except:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
n_steps_t = max(1, int(round((t_fin - t0) / h)))
|
|
199
|
+
t_vals = np.linspace(t0, t_fin, n_steps_t + 1)
|
|
200
|
+
X_main = [np.array([x0, y0])]
|
|
201
|
+
for _ in range(len(t_vals) - 1):
|
|
202
|
+
curr = X_main[-1]
|
|
203
|
+
if curr is None:
|
|
204
|
+
break
|
|
205
|
+
next_p = rk4_step(curr, h)
|
|
206
|
+
X_main.append(next_p)
|
|
207
|
+
X_main_final = np.array([p for p in X_main if p is not None])
|
|
208
|
+
|
|
209
|
+
automatic_trajectories = []
|
|
210
|
+
n_side = int(math.ceil(math.sqrt(cantidad_trayectorias)))
|
|
211
|
+
if n_side > 1:
|
|
212
|
+
x0_auto = np.linspace(x_min, x_max, n_side)
|
|
213
|
+
y0_auto = np.linspace(y_min, y_max, n_side)
|
|
214
|
+
for xi in x0_auto:
|
|
215
|
+
for yi in y0_auto:
|
|
216
|
+
t_len = int(len(t_vals) / 2)
|
|
217
|
+
X_temp = [np.array([xi, yi])]
|
|
218
|
+
for _ in range(t_len - 1):
|
|
219
|
+
curr = X_temp[-1]
|
|
220
|
+
if curr is None:
|
|
221
|
+
break
|
|
222
|
+
next_p = rk4_step(curr, h * 2)
|
|
223
|
+
X_temp.append(next_p)
|
|
224
|
+
|
|
225
|
+
X_temp_final = np.array([p for p in X_temp if p is not None])
|
|
226
|
+
if len(X_temp_final) > 1:
|
|
227
|
+
automatic_trajectories.append(
|
|
228
|
+
{
|
|
229
|
+
"x": X_temp_final[:, 0].tolist(),
|
|
230
|
+
"y": X_temp_final[:, 1].tolist(),
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# --- ARREGLO DE NULCLINAS ROTA ---
|
|
235
|
+
# Aumentamos la resolucion de la grilla a 150 para curvas ultra suaves
|
|
236
|
+
grid_res = 150
|
|
237
|
+
X_mesh, Y_mesh = np.meshgrid(
|
|
238
|
+
np.linspace(x_min, x_max, grid_res), np.linspace(y_min, y_max, grid_res)
|
|
239
|
+
)
|
|
240
|
+
try:
|
|
241
|
+
Z_x = f_num(X_mesh, Y_mesh)
|
|
242
|
+
Z_y = g_num(X_mesh, Y_mesh)
|
|
243
|
+
if np.isscalar(Z_x):
|
|
244
|
+
Z_x = np.full(X_mesh.shape, Z_x)
|
|
245
|
+
if np.isscalar(Z_y):
|
|
246
|
+
Z_y = np.full(Y_mesh.shape, Z_y)
|
|
247
|
+
|
|
248
|
+
# Recortamos los valores explosivos y limpiamos NaNs
|
|
249
|
+
Z_x = np.nan_to_num(Z_x, nan=0.0, posinf=1e5, neginf=-1e5)
|
|
250
|
+
Z_y = np.nan_to_num(Z_y, nan=0.0, posinf=1e5, neginf=-1e5)
|
|
251
|
+
Z_x = np.clip(Z_x, -1e6, 1e6)
|
|
252
|
+
Z_y = np.clip(Z_y, -1e6, 1e6)
|
|
253
|
+
|
|
254
|
+
except Exception:
|
|
255
|
+
Z_x = np.zeros_like(X_mesh)
|
|
256
|
+
Z_y = np.zeros_like(Y_mesh)
|
|
257
|
+
|
|
258
|
+
x_sym, y_sym_var = sp.symbols("x y", real=True)
|
|
259
|
+
try:
|
|
260
|
+
sol_x = sp.solve(sp.Eq(f, 0), y_sym_var)
|
|
261
|
+
nul_x_despejada = (
|
|
262
|
+
f"y = {str(sol_x[0]).replace('**', '^').replace('*', ' ')}"
|
|
263
|
+
if sol_x
|
|
264
|
+
else "No despejable como y(x)"
|
|
265
|
+
)
|
|
266
|
+
except:
|
|
267
|
+
nul_x_despejada = "No despejable como y(x)"
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
sol_y = sp.solve(sp.Eq(g, 0), y_sym_var)
|
|
271
|
+
nul_y_despejada = (
|
|
272
|
+
f"y = {str(sol_y[0]).replace('**', '^').replace('*', ' ')}"
|
|
273
|
+
if sol_y
|
|
274
|
+
else "No despejable como y(x)"
|
|
275
|
+
)
|
|
276
|
+
except:
|
|
277
|
+
nul_y_despejada = "No despejable como y(x)"
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
"ecuacion_latex_x": sp.latex(f_raw),
|
|
281
|
+
"ecuacion_latex_y": sp.latex(g_raw),
|
|
282
|
+
"jacobiano_latex": sp.latex(J),
|
|
283
|
+
"puntos_analizados": analisis_puntos,
|
|
284
|
+
"principal_trajectory": {
|
|
285
|
+
"t": t_vals[: len(X_main_final)].tolist(),
|
|
286
|
+
"x": X_main_final[:, 0].tolist(),
|
|
287
|
+
"y": X_main_final[:, 1].tolist(),
|
|
288
|
+
},
|
|
289
|
+
"automatic_trajectories": automatic_trajectories,
|
|
290
|
+
"contour_data": {
|
|
291
|
+
"x_axis": X_mesh[0].tolist(),
|
|
292
|
+
"y_axis": Y_mesh[:, 0].tolist(),
|
|
293
|
+
"z_x_nul": Z_x.tolist(),
|
|
294
|
+
"z_y_nul": Z_y.tolist(),
|
|
295
|
+
},
|
|
296
|
+
"nulclina_x_desp": nul_x_despejada,
|
|
297
|
+
"nulclina_y_desp": nul_y_despejada,
|
|
298
|
+
}
|