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,283 @@
|
|
|
1
|
+
# backend/app/methods/dynamic_2d_conservative.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 Dynamic2DConservativeService:
|
|
15
|
+
@staticmethod
|
|
16
|
+
def clasificar_punto(traza, determinante) -> tuple:
|
|
17
|
+
if abs(determinante) < 1e-10:
|
|
18
|
+
return "Punto Degenerado", "Jacobiano singular. Requiere análisis superior."
|
|
19
|
+
elif determinante < 0:
|
|
20
|
+
return "Silla", "Punto hiperbólico inestable. Por aquí pasa la Separatriz."
|
|
21
|
+
# determinante > 0
|
|
22
|
+
if abs(traza) < 1e-10:
|
|
23
|
+
return (
|
|
24
|
+
"Centro",
|
|
25
|
+
"Punto estable (no asintótico). Las trayectorias forman órbitas cerradas (conservación de energía).",
|
|
26
|
+
)
|
|
27
|
+
discriminante = traza**2 - 4 * determinante
|
|
28
|
+
if discriminante < 0:
|
|
29
|
+
if traza < 0:
|
|
30
|
+
return "Foco Estable", "Espiral convergente (atractor). Las trayectorias se enrollan hacia el punto."
|
|
31
|
+
return "Foco Inestable", "Espiral divergente (repulsor). Las trayectorias se alejan del punto."
|
|
32
|
+
if traza < 0:
|
|
33
|
+
return "Nodo Estable", "Nodo asintóticamente estable. Las trayectorias convergen al punto."
|
|
34
|
+
return "Nodo Inestable", "Nodo repulsivo. Las trayectorias divergen del punto."
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def solve(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
38
|
+
eq_x_str = payload.get("eq_x", "y")
|
|
39
|
+
eq_y_str = payload.get("eq_y", "x - x**3")
|
|
40
|
+
mu_val = float(payload.get("mu", 0.0))
|
|
41
|
+
x0 = float(payload.get("x0", 0.1))
|
|
42
|
+
y0 = float(payload.get("y0", 0.0))
|
|
43
|
+
t0 = float(payload.get("t0", 0.0))
|
|
44
|
+
t_fin = float(payload.get("t_fin", 15.0))
|
|
45
|
+
h = float(payload.get("h", 0.02))
|
|
46
|
+
x_min = float(payload.get("x_min", -3.0))
|
|
47
|
+
x_max = float(payload.get("x_max", 3.0))
|
|
48
|
+
y_min = float(payload.get("y_min", -3.0))
|
|
49
|
+
y_max = float(payload.get("y_max", 3.0))
|
|
50
|
+
cantidad_trayectorias = int(payload.get("cantidad_trayectorias", 20))
|
|
51
|
+
|
|
52
|
+
x, y, mu_sym = sp.symbols("x y mu", real=True)
|
|
53
|
+
transformations = standard_transformations + (
|
|
54
|
+
implicit_multiplication_application,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
f_raw = safe_parse_expr(
|
|
59
|
+
eq_x_str,
|
|
60
|
+
local_dict={"x": x, "y": y, "mu": mu_sym},
|
|
61
|
+
allowed_symbols=["x", "y", "mu"],
|
|
62
|
+
transformations=transformations,
|
|
63
|
+
)
|
|
64
|
+
g_raw = safe_parse_expr(
|
|
65
|
+
eq_y_str,
|
|
66
|
+
local_dict={"x": x, "y": y, "mu": mu_sym},
|
|
67
|
+
allowed_symbols=["x", "y", "mu"],
|
|
68
|
+
transformations=transformations,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
f = f_raw.subs(mu_sym, mu_val)
|
|
72
|
+
g = g_raw.subs(mu_sym, mu_val)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
raise ValueError(f"Error de sintaxis: {str(e)}")
|
|
75
|
+
|
|
76
|
+
# 1. Calculo de Divergencia
|
|
77
|
+
divergencia = sp.diff(f, x) + sp.diff(g, y)
|
|
78
|
+
div_simplify = sp.simplify(divergencia)
|
|
79
|
+
es_conservativo = div_simplify == 0
|
|
80
|
+
|
|
81
|
+
# 2. Integracion del Hamiltoniano / Energia E(x,y) y Demostracion dH/dt
|
|
82
|
+
H_latex = ""
|
|
83
|
+
H_num = None
|
|
84
|
+
dh_dx_latex = ""
|
|
85
|
+
dh_dy_latex = ""
|
|
86
|
+
dh_dt_raw_latex = ""
|
|
87
|
+
dh_dt_simplified_latex = ""
|
|
88
|
+
|
|
89
|
+
if es_conservativo:
|
|
90
|
+
try:
|
|
91
|
+
# Ecuaciones de Hamilton: dH/dy = f y dH/dx = -g
|
|
92
|
+
H_y = sp.integrate(f, y)
|
|
93
|
+
rem = -g - sp.diff(H_y, x)
|
|
94
|
+
H_x = sp.integrate(rem, x)
|
|
95
|
+
H = sp.simplify(H_y + H_x)
|
|
96
|
+
H_latex = sp.latex(H)
|
|
97
|
+
H_num = sp.lambdify((x, y), H, "numpy")
|
|
98
|
+
|
|
99
|
+
# Demostracion dH/dt = (dH/dx)*x_dot + (dH/dy)*y_dot
|
|
100
|
+
dH_dx = sp.diff(H, x)
|
|
101
|
+
dH_dy = sp.diff(H, y)
|
|
102
|
+
dh_dt_raw = dH_dx * f + dH_dy * g
|
|
103
|
+
dh_dt_simplified = sp.simplify(dh_dt_raw)
|
|
104
|
+
|
|
105
|
+
dh_dx_latex = sp.latex(dH_dx)
|
|
106
|
+
dh_dy_latex = sp.latex(dH_dy)
|
|
107
|
+
dh_dt_raw_latex = sp.latex(dh_dt_raw)
|
|
108
|
+
dh_dt_simplified_latex = sp.latex(dh_dt_simplified)
|
|
109
|
+
except:
|
|
110
|
+
H_latex = "\\text{No integrable simbólicamente}"
|
|
111
|
+
|
|
112
|
+
# 3. Matriz Jacobiana Analitica
|
|
113
|
+
J = sp.Matrix([[sp.diff(f, x), sp.diff(f, y)], [sp.diff(g, x), sp.diff(g, y)]])
|
|
114
|
+
|
|
115
|
+
# 4. Puntos de Equilibrio
|
|
116
|
+
try:
|
|
117
|
+
sols = sp.solve([f, g], (x, y), dict=True)
|
|
118
|
+
puntos_equilibrio = []
|
|
119
|
+
for sol in sols:
|
|
120
|
+
if x in sol and y in sol:
|
|
121
|
+
x_val, y_val = complex(sol[x]), complex(sol[y])
|
|
122
|
+
if abs(x_val.imag) < 1e-9 and abs(y_val.imag) < 1e-9:
|
|
123
|
+
pto = (float(x_val.real), float(y_val.real))
|
|
124
|
+
if pto not in puntos_equilibrio:
|
|
125
|
+
puntos_equilibrio.append(pto)
|
|
126
|
+
except:
|
|
127
|
+
puntos_equilibrio = []
|
|
128
|
+
|
|
129
|
+
analisis_puntos = []
|
|
130
|
+
saddle_energies = []
|
|
131
|
+
|
|
132
|
+
for pto in puntos_equilibrio:
|
|
133
|
+
px, py = pto[0], pto[1]
|
|
134
|
+
J_eval = J.subs({x: px, y: py})
|
|
135
|
+
J_num_matrix = np.array(J_eval.tolist(), dtype=float)
|
|
136
|
+
|
|
137
|
+
traza = float(np.trace(J_num_matrix))
|
|
138
|
+
determinante = float(np.linalg.det(J_num_matrix))
|
|
139
|
+
discriminante = traza**2 - 4 * determinante
|
|
140
|
+
autovalores, _ = np.linalg.eig(J_num_matrix)
|
|
141
|
+
|
|
142
|
+
clasif, comp = Dynamic2DConservativeService.clasificar_punto(
|
|
143
|
+
traza, determinante
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if "Silla" in clasif and H_num is not None:
|
|
147
|
+
try:
|
|
148
|
+
saddle_energies.append(float(H_num(px, py)))
|
|
149
|
+
except:
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
j_local_formateado = [
|
|
153
|
+
[
|
|
154
|
+
round(float(J_num_matrix[0][0]), 4),
|
|
155
|
+
round(float(J_num_matrix[0][1]), 4),
|
|
156
|
+
],
|
|
157
|
+
[
|
|
158
|
+
round(float(J_num_matrix[1][0]), 4),
|
|
159
|
+
round(float(J_num_matrix[1][1]), 4),
|
|
160
|
+
],
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
analisis_puntos.append(
|
|
164
|
+
{
|
|
165
|
+
"x": px,
|
|
166
|
+
"y": py,
|
|
167
|
+
"traza": traza,
|
|
168
|
+
"determinante": determinante,
|
|
169
|
+
"discriminante": discriminante,
|
|
170
|
+
"clasificacion": clasif,
|
|
171
|
+
"comportamiento": comp,
|
|
172
|
+
"autovalores": [
|
|
173
|
+
{"real": float(np.real(av)), "imag": float(np.imag(av))}
|
|
174
|
+
for av in autovalores
|
|
175
|
+
],
|
|
176
|
+
"jacobiano_local": j_local_formateado,
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# 5. Simulacion Numerica (RK4)
|
|
181
|
+
f_num = sp.lambdify((x, y), f, "numpy")
|
|
182
|
+
g_num = sp.lambdify((x, y), g, "numpy")
|
|
183
|
+
|
|
184
|
+
def rk4_step(X_curr, step_h):
|
|
185
|
+
try:
|
|
186
|
+
v_x, v_y = float(X_curr[0]), float(X_curr[1])
|
|
187
|
+
k1_x = f_num(v_x, v_y)
|
|
188
|
+
k1_y = g_num(v_x, v_y)
|
|
189
|
+
k2_x = f_num(v_x + step_h * k1_x / 2, v_y + step_h * k1_y / 2)
|
|
190
|
+
k2_y = g_num(v_x + step_h * k1_x / 2, v_y + step_h * k1_y / 2)
|
|
191
|
+
k3_x = f_num(v_x + step_h * k2_x / 2, v_y + step_h * k2_y / 2)
|
|
192
|
+
k3_y = g_num(v_x + step_h * k2_x / 2, v_y + step_h * k2_y / 2)
|
|
193
|
+
k4_x = f_num(v_x + step_h * k3_x, v_y + step_h * k3_y)
|
|
194
|
+
k4_y = g_num(v_x + step_h * k3_x, v_y + step_h * k3_y)
|
|
195
|
+
|
|
196
|
+
new_x = v_x + (step_h / 6) * (k1_x + 2 * k2_x + 2 * k3_x + k4_x)
|
|
197
|
+
new_y = v_y + (step_h / 6) * (k1_y + 2 * k2_y + 2 * k3_y + k4_y)
|
|
198
|
+
|
|
199
|
+
if (
|
|
200
|
+
math.isnan(new_x)
|
|
201
|
+
or math.isnan(new_y)
|
|
202
|
+
or abs(new_x) > 1e6
|
|
203
|
+
or abs(new_y) > 1e6
|
|
204
|
+
):
|
|
205
|
+
return None
|
|
206
|
+
return np.array([new_x, new_y])
|
|
207
|
+
except:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
n_steps_t = max(1, int(round((t_fin - t0) / h)))
|
|
211
|
+
t_vals = np.linspace(t0, t_fin, n_steps_t + 1)
|
|
212
|
+
X_main = [np.array([x0, y0])]
|
|
213
|
+
for _ in range(len(t_vals) - 1):
|
|
214
|
+
curr = X_main[-1]
|
|
215
|
+
if curr is None:
|
|
216
|
+
break
|
|
217
|
+
X_main.append(rk4_step(curr, h))
|
|
218
|
+
X_main_final = np.array([p for p in X_main if p is not None])
|
|
219
|
+
|
|
220
|
+
automatic_trajectories = []
|
|
221
|
+
n_side = int(math.ceil(math.sqrt(cantidad_trayectorias)))
|
|
222
|
+
if n_side > 1:
|
|
223
|
+
x0_auto = np.linspace(x_min, x_max, n_side)
|
|
224
|
+
y0_auto = np.linspace(y_min, y_max, n_side)
|
|
225
|
+
for xi in x0_auto:
|
|
226
|
+
for yi in y0_auto:
|
|
227
|
+
X_temp = [np.array([xi, yi])]
|
|
228
|
+
for _ in range(int(len(t_vals) / 2) - 1):
|
|
229
|
+
curr = X_temp[-1]
|
|
230
|
+
if curr is None:
|
|
231
|
+
break
|
|
232
|
+
X_temp.append(rk4_step(curr, h * 2))
|
|
233
|
+
|
|
234
|
+
X_temp_final = np.array([p for p in X_temp if p is not None])
|
|
235
|
+
if len(X_temp_final) > 1:
|
|
236
|
+
automatic_trajectories.append(
|
|
237
|
+
{
|
|
238
|
+
"x": X_temp_final[:, 0].tolist(),
|
|
239
|
+
"y": X_temp_final[:, 1].tolist(),
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# 6. Grillas para Plotly
|
|
244
|
+
grid_res = 150
|
|
245
|
+
X_mesh, Y_mesh = np.meshgrid(
|
|
246
|
+
np.linspace(x_min, x_max, grid_res), np.linspace(y_min, y_max, grid_res)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
Z_H_grid = np.zeros_like(X_mesh)
|
|
250
|
+
if H_num is not None:
|
|
251
|
+
try:
|
|
252
|
+
Z_H_grid = H_num(X_mesh, Y_mesh)
|
|
253
|
+
if np.isscalar(Z_H_grid):
|
|
254
|
+
Z_H_grid = np.full(X_mesh.shape, Z_H_grid)
|
|
255
|
+
Z_H_grid = np.nan_to_num(Z_H_grid, nan=0.0, posinf=1e5, neginf=-1e5)
|
|
256
|
+
Z_H_grid = np.clip(Z_H_grid, -1e6, 1e6)
|
|
257
|
+
except:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
"ecuacion_latex_x": sp.latex(f_raw),
|
|
262
|
+
"ecuacion_latex_y": sp.latex(g_raw),
|
|
263
|
+
"divergencia_latex": sp.latex(div_simplify),
|
|
264
|
+
"es_conservativo": bool(es_conservativo),
|
|
265
|
+
"hamiltoniano_latex": H_latex,
|
|
266
|
+
"dh_dx_latex": dh_dx_latex,
|
|
267
|
+
"dh_dy_latex": dh_dy_latex,
|
|
268
|
+
"dh_dt_raw_latex": dh_dt_raw_latex,
|
|
269
|
+
"dh_dt_simplified_latex": dh_dt_simplified_latex,
|
|
270
|
+
"saddle_energies": list(set(saddle_energies)),
|
|
271
|
+
"puntos_analizados": analisis_puntos,
|
|
272
|
+
"principal_trajectory": {
|
|
273
|
+
"t": t_vals[: len(X_main_final)].tolist(),
|
|
274
|
+
"x": X_main_final[:, 0].tolist(),
|
|
275
|
+
"y": X_main_final[:, 1].tolist(),
|
|
276
|
+
},
|
|
277
|
+
"automatic_trajectories": automatic_trajectories,
|
|
278
|
+
"contour_data": {
|
|
279
|
+
"x_axis": X_mesh[0].tolist(),
|
|
280
|
+
"y_axis": Y_mesh[:, 0].tolist(),
|
|
281
|
+
"z_H": Z_H_grid.tolist(),
|
|
282
|
+
},
|
|
283
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# backend/app/methods/dynamic_2d_lanchester.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 Dynamic2DLanchesterService:
|
|
15
|
+
@staticmethod
|
|
16
|
+
def solve(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
17
|
+
eq_x_str = payload.get("eq_x", "-α * y")
|
|
18
|
+
eq_y_str = payload.get("eq_y", "-β * x")
|
|
19
|
+
a_val = float(payload.get("alpha", 1.0))
|
|
20
|
+
b_val = float(payload.get("beta", 2.0))
|
|
21
|
+
g_val = float(payload.get("gamma", 0.0))
|
|
22
|
+
e_val = float(payload.get("epsilon", 0.0))
|
|
23
|
+
m_val = float(payload.get("mu", 0.0))
|
|
24
|
+
d_val = float(payload.get("delta", 0.0))
|
|
25
|
+
|
|
26
|
+
x0 = float(payload.get("x0", 100.0))
|
|
27
|
+
y0 = float(payload.get("y0", 80.0))
|
|
28
|
+
t0 = float(payload.get("t0", 0.0))
|
|
29
|
+
t_fin = float(payload.get("t_fin", 5.0))
|
|
30
|
+
h = float(payload.get("h", 0.01))
|
|
31
|
+
|
|
32
|
+
x, y = sp.symbols("x y", real=True)
|
|
33
|
+
sym_alpha = sp.Symbol("α", real=True)
|
|
34
|
+
sym_beta = sp.Symbol("β", real=True)
|
|
35
|
+
sym_gamma = sp.Symbol("γ", real=True)
|
|
36
|
+
sym_eps = sp.Symbol("ε", real=True)
|
|
37
|
+
sym_mu = sp.Symbol("μ", real=True)
|
|
38
|
+
sym_delta = sp.Symbol("δ", real=True)
|
|
39
|
+
|
|
40
|
+
local_dict = {
|
|
41
|
+
"x": x,
|
|
42
|
+
"y": y,
|
|
43
|
+
"α": sym_alpha,
|
|
44
|
+
"β": sym_beta,
|
|
45
|
+
"γ": sym_gamma,
|
|
46
|
+
"ε": sym_eps,
|
|
47
|
+
"μ": sym_mu,
|
|
48
|
+
"δ": sym_delta,
|
|
49
|
+
}
|
|
50
|
+
subs_dict = {
|
|
51
|
+
sym_alpha: a_val,
|
|
52
|
+
sym_beta: b_val,
|
|
53
|
+
sym_gamma: g_val,
|
|
54
|
+
sym_eps: e_val,
|
|
55
|
+
sym_mu: m_val,
|
|
56
|
+
sym_delta: d_val,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
transformations = standard_transformations + (
|
|
60
|
+
implicit_multiplication_application,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
gate_allowed = list(dict.fromkeys(["x", "y", "α", "β", "γ", "ε", "μ", "δ"]))
|
|
65
|
+
f_raw = safe_parse_expr(
|
|
66
|
+
eq_x_str,
|
|
67
|
+
local_dict=local_dict,
|
|
68
|
+
allowed_symbols=gate_allowed,
|
|
69
|
+
transformations=transformations,
|
|
70
|
+
)
|
|
71
|
+
g_raw = safe_parse_expr(
|
|
72
|
+
eq_y_str,
|
|
73
|
+
local_dict=local_dict,
|
|
74
|
+
allowed_symbols=gate_allowed,
|
|
75
|
+
transformations=transformations,
|
|
76
|
+
)
|
|
77
|
+
f = f_raw.subs(subs_dict)
|
|
78
|
+
g = g_raw.subs(subs_dict)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Error de sintaxis matemática: revise los símbolos insertados. ({str(e)})"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
f_num = sp.lambdify((x, y), f, "numpy")
|
|
85
|
+
g_num = sp.lambdify((x, y), g, "numpy")
|
|
86
|
+
|
|
87
|
+
dx_dt_0 = float(f_num(x0, y0))
|
|
88
|
+
dy_dt_0 = float(g_num(x0, y0))
|
|
89
|
+
|
|
90
|
+
is_classic = False
|
|
91
|
+
C_val = 0.0
|
|
92
|
+
winner_analytic = "Indeterminado"
|
|
93
|
+
survivors_analytic = 0.0
|
|
94
|
+
state_eq_latex = ""
|
|
95
|
+
t_end_analytic = -1.0
|
|
96
|
+
|
|
97
|
+
# 1. Análisis Teórico de Lanchester
|
|
98
|
+
if (
|
|
99
|
+
sp.simplify(f - (-sym_alpha.subs(subs_dict) * y)) == 0
|
|
100
|
+
and sp.simplify(g - (-sym_beta.subs(subs_dict) * x)) == 0
|
|
101
|
+
):
|
|
102
|
+
is_classic = True
|
|
103
|
+
C_val = a_val * (y0**2) - b_val * (x0**2)
|
|
104
|
+
state_eq_latex = f"{a_val} y^2 - {b_val} x^2 = {C_val:.2f}"
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
if C_val > 0:
|
|
108
|
+
winner_analytic = "Ejército Y (Azul)"
|
|
109
|
+
survivors_analytic = math.sqrt(C_val / a_val) if a_val != 0 else 0
|
|
110
|
+
if a_val > 0 and b_val > 0:
|
|
111
|
+
arg = (x0 / y0) * math.sqrt(b_val / a_val)
|
|
112
|
+
t_end_analytic = (1.0 / math.sqrt(a_val * b_val)) * math.atanh(
|
|
113
|
+
arg
|
|
114
|
+
)
|
|
115
|
+
elif C_val < 0:
|
|
116
|
+
winner_analytic = "Ejército X (Rojo)"
|
|
117
|
+
survivors_analytic = math.sqrt(-C_val / b_val) if b_val != 0 else 0
|
|
118
|
+
if a_val > 0 and b_val > 0:
|
|
119
|
+
arg = (y0 / x0) * math.sqrt(a_val / b_val)
|
|
120
|
+
t_end_analytic = (1.0 / math.sqrt(a_val * b_val)) * math.atanh(
|
|
121
|
+
arg
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
winner_analytic = "Empate (Aniquilación Mutua)"
|
|
125
|
+
except:
|
|
126
|
+
t_end_analytic = -1.0
|
|
127
|
+
|
|
128
|
+
# 2. Simulación RK4 Principal (hasta t_fin)
|
|
129
|
+
def rk4_step(X_curr, step_h):
|
|
130
|
+
try:
|
|
131
|
+
v_x, v_y = float(X_curr[0]), float(X_curr[1])
|
|
132
|
+
k1_x = f_num(v_x, v_y)
|
|
133
|
+
k1_y = g_num(v_x, v_y)
|
|
134
|
+
k2_x = f_num(v_x + step_h * k1_x / 2, v_y + step_h * k1_y / 2)
|
|
135
|
+
k2_y = g_num(v_x + step_h * k1_x / 2, v_y + step_h * k1_y / 2)
|
|
136
|
+
k3_x = f_num(v_x + step_h * k2_x / 2, v_y + step_h * k2_y / 2)
|
|
137
|
+
k3_y = g_num(v_x + step_h * k2_x / 2, v_y + step_h * k2_y / 2)
|
|
138
|
+
k4_x = f_num(v_x + step_h * k3_x, v_y + step_h * k3_y)
|
|
139
|
+
k4_y = g_num(v_x + step_h * k3_x, v_y + step_h * k3_y)
|
|
140
|
+
|
|
141
|
+
new_x = v_x + (step_h / 6) * (k1_x + 2 * k2_x + 2 * k3_x + k4_x)
|
|
142
|
+
new_y = v_y + (step_h / 6) * (k1_y + 2 * k2_y + 2 * k3_y + k4_y)
|
|
143
|
+
return np.array([new_x, new_y])
|
|
144
|
+
except:
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
n_steps_t = max(1, int(round((t_fin - t0) / h)))
|
|
148
|
+
t_vals = np.linspace(t0, t_fin, n_steps_t + 1)
|
|
149
|
+
X_main = [np.array([x0, y0])]
|
|
150
|
+
t_actual = [t0]
|
|
151
|
+
|
|
152
|
+
winner_numeric = "Combate en curso"
|
|
153
|
+
for i in range(len(t_vals) - 1):
|
|
154
|
+
curr = X_main[-1]
|
|
155
|
+
if curr is None:
|
|
156
|
+
break
|
|
157
|
+
next_p = rk4_step(curr, h)
|
|
158
|
+
|
|
159
|
+
# NUEVO: Se considera aniquilación al tener menos de 1 soldado
|
|
160
|
+
if next_p[0] < 1.0 and next_p[1] < 1.0:
|
|
161
|
+
X_main.append(np.array([0.0, 0.0]))
|
|
162
|
+
t_actual.append(t_vals[i + 1])
|
|
163
|
+
winner_numeric = "Empate (Aniquilación Mutua)"
|
|
164
|
+
break
|
|
165
|
+
elif next_p[0] < 1.0:
|
|
166
|
+
X_main.append(np.array([0.0, next_p[1]]))
|
|
167
|
+
t_actual.append(t_vals[i + 1])
|
|
168
|
+
winner_numeric = "Ejército Y (Azul)"
|
|
169
|
+
break
|
|
170
|
+
elif next_p[1] < 1.0:
|
|
171
|
+
X_main.append(np.array([next_p[0], 0.0]))
|
|
172
|
+
t_actual.append(t_vals[i + 1])
|
|
173
|
+
winner_numeric = "Ejército X (Rojo)"
|
|
174
|
+
break
|
|
175
|
+
else:
|
|
176
|
+
X_main.append(next_p)
|
|
177
|
+
t_actual.append(t_vals[i + 1])
|
|
178
|
+
|
|
179
|
+
X_main_final = np.array(X_main)
|
|
180
|
+
|
|
181
|
+
# 3. Proyección Extendida en la Sombra
|
|
182
|
+
t_end_numeric_str = ""
|
|
183
|
+
if winner_numeric != "Combate en curso":
|
|
184
|
+
t_end_numeric_str = (
|
|
185
|
+
f"Finalizó dentro de la simulación en t = {t_actual[-1]:.2f}"
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
curr_sim = X_main[-1]
|
|
189
|
+
t_sim = t_actual[-1]
|
|
190
|
+
max_t_sim = t0 + 2000.0
|
|
191
|
+
|
|
192
|
+
while t_sim < max_t_sim:
|
|
193
|
+
next_p = rk4_step(curr_sim, h * 2)
|
|
194
|
+
if next_p is None:
|
|
195
|
+
t_end_numeric_str = "Error numérico proyectando al futuro."
|
|
196
|
+
break
|
|
197
|
+
t_sim += h * 2
|
|
198
|
+
|
|
199
|
+
# NUEVO: Menos de 1 soldado
|
|
200
|
+
if next_p[0] < 1.0 and next_p[1] < 1.0:
|
|
201
|
+
t_end_numeric_str = f"Empate proyectado en t ≈ {t_sim:.2f}"
|
|
202
|
+
break
|
|
203
|
+
elif next_p[0] < 1.0:
|
|
204
|
+
t_end_numeric_str = f"Y (Azul) ganará proyectado en t ≈ {t_sim:.2f}"
|
|
205
|
+
break
|
|
206
|
+
elif next_p[1] < 1.0:
|
|
207
|
+
t_end_numeric_str = f"X (Rojo) ganará proyectado en t ≈ {t_sim:.2f}"
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
curr_sim = next_p
|
|
211
|
+
|
|
212
|
+
if not t_end_numeric_str:
|
|
213
|
+
t_end_numeric_str = "El modelo decae asintóticamente (nunca llega a 0 tropas o excede el límite temporal)."
|
|
214
|
+
|
|
215
|
+
# Generar trayectorias de fase representativas
|
|
216
|
+
automatic_trajectories = []
|
|
217
|
+
x_max_plot = max(x0 * 1.5, 10)
|
|
218
|
+
y_max_plot = max(y0 * 1.5, 10)
|
|
219
|
+
x0_auto = np.linspace(1, x_max_plot, 4)
|
|
220
|
+
y0_auto = np.linspace(1, y_max_plot, 4)
|
|
221
|
+
for xi in x0_auto:
|
|
222
|
+
for yi in y0_auto:
|
|
223
|
+
X_temp = [np.array([xi, yi])]
|
|
224
|
+
for _ in range(int(len(t_vals) / 2)):
|
|
225
|
+
curr = X_temp[-1]
|
|
226
|
+
if curr[0] <= 0 or curr[1] <= 0:
|
|
227
|
+
break
|
|
228
|
+
next_p = rk4_step(curr, h * 3)
|
|
229
|
+
if next_p[0] < 0:
|
|
230
|
+
next_p[0] = 0
|
|
231
|
+
if next_p[1] < 0:
|
|
232
|
+
next_p[1] = 0
|
|
233
|
+
X_temp.append(next_p)
|
|
234
|
+
X_temp_final = np.array(X_temp)
|
|
235
|
+
if len(X_temp_final) > 1:
|
|
236
|
+
automatic_trajectories.append(
|
|
237
|
+
{
|
|
238
|
+
"x": X_temp_final[:, 0].tolist(),
|
|
239
|
+
"y": X_temp_final[:, 1].tolist(),
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
"ecuacion_latex_x": sp.latex(f_raw),
|
|
245
|
+
"ecuacion_latex_y": sp.latex(g_raw),
|
|
246
|
+
"dx_dt_0": dx_dt_0,
|
|
247
|
+
"dy_dt_0": dy_dt_0,
|
|
248
|
+
"is_classic": is_classic,
|
|
249
|
+
"C_val": C_val,
|
|
250
|
+
"state_eq_latex": state_eq_latex,
|
|
251
|
+
"winner_analytic": winner_analytic,
|
|
252
|
+
"survivors_analytic": survivors_analytic,
|
|
253
|
+
"t_end_analytic": t_end_analytic,
|
|
254
|
+
"t_end_numeric_str": t_end_numeric_str,
|
|
255
|
+
"winner_numeric": winner_numeric,
|
|
256
|
+
"final_x": X_main_final[-1][0],
|
|
257
|
+
"final_y": X_main_final[-1][1],
|
|
258
|
+
"principal_trajectory": {
|
|
259
|
+
"t": t_actual,
|
|
260
|
+
"x": X_main_final[:, 0].tolist(),
|
|
261
|
+
"y": X_main_final[:, 1].tolist(),
|
|
262
|
+
},
|
|
263
|
+
"automatic_trajectories": automatic_trajectories,
|
|
264
|
+
}
|