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,710 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import sympy as sp
|
|
3
|
+
import math
|
|
4
|
+
from fractions import Fraction
|
|
5
|
+
from typing import Dict, Any, List
|
|
6
|
+
from .utils import safe_sympify
|
|
7
|
+
|
|
8
|
+
class Dynamic2DNonHomogeneousService:
|
|
9
|
+
@staticmethod
|
|
10
|
+
def clasificar_sistema(A) -> str:
|
|
11
|
+
traza = float(np.trace(A))
|
|
12
|
+
determinante = float(np.linalg.det(A))
|
|
13
|
+
discriminante = traza**2 - 4 * determinante
|
|
14
|
+
autovalores = np.linalg.eigvals(A)
|
|
15
|
+
|
|
16
|
+
if abs(determinante) < 1e-10:
|
|
17
|
+
return "Caso degenerado / determinante cero"
|
|
18
|
+
if determinante < 0:
|
|
19
|
+
return "Silla"
|
|
20
|
+
if discriminante > 1e-10:
|
|
21
|
+
if traza < 0:
|
|
22
|
+
return "Nodo estable"
|
|
23
|
+
elif traza > 0:
|
|
24
|
+
return "Nodo inestable"
|
|
25
|
+
else:
|
|
26
|
+
return "Nodo con traza cero / caso especial"
|
|
27
|
+
elif abs(discriminante) <= 1e-10:
|
|
28
|
+
if traza < 0:
|
|
29
|
+
return "Nodo degenerado estable"
|
|
30
|
+
elif traza > 0:
|
|
31
|
+
return "Nodo degenerado inestable"
|
|
32
|
+
else:
|
|
33
|
+
return "Caso no hiperbolico"
|
|
34
|
+
else:
|
|
35
|
+
parte_real = float(np.real(autovalores[0]))
|
|
36
|
+
if abs(parte_real) < 1e-10:
|
|
37
|
+
return "Centro"
|
|
38
|
+
elif parte_real < 0:
|
|
39
|
+
return "Foco estable"
|
|
40
|
+
else:
|
|
41
|
+
return "Foco inestable"
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def descripcion_comportamiento(clasificacion: str) -> str:
|
|
45
|
+
if "Silla" in clasificacion:
|
|
46
|
+
return "Las trayectorias se acercan en una direccion y se alejan en otra."
|
|
47
|
+
if "Nodo estable" in clasificacion:
|
|
48
|
+
return "Todas las trayectorias cercanas tienden al equilibrio sin oscilar."
|
|
49
|
+
if "Nodo inestable" in clasificacion:
|
|
50
|
+
return "Las trayectorias se alejan del equilibrio sin oscilar."
|
|
51
|
+
if "Foco estable" in clasificacion:
|
|
52
|
+
return "Las trayectorias giran en espiral hacia el equilibrio."
|
|
53
|
+
if "Foco inestable" in clasificacion:
|
|
54
|
+
return "Las trayectorias giran en espiral alejandose del equilibrio."
|
|
55
|
+
if "Centro" in clasificacion:
|
|
56
|
+
return "Las trayectorias cerradas rodean el equilibrio."
|
|
57
|
+
if "degenerado" in clasificacion:
|
|
58
|
+
return "El sistema tiene autovalores repetidos; puede requerir analizar autovectores."
|
|
59
|
+
return "Caso especial; requiere analisis adicional."
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def rk4_sistema(A, B, X0, t0, t_fin, h):
|
|
63
|
+
n_steps_t = max(1, int(round((t_fin - t0) / h)))
|
|
64
|
+
t_vals = np.linspace(t0, t_fin, n_steps_t + 1)
|
|
65
|
+
X_vals = np.zeros((len(t_vals), 2))
|
|
66
|
+
X_vals[0] = X0
|
|
67
|
+
|
|
68
|
+
if callable(B):
|
|
69
|
+
def eval_B(ti):
|
|
70
|
+
return np.array(B(ti), dtype=float)
|
|
71
|
+
else:
|
|
72
|
+
B_vec = np.array(B, dtype=float)
|
|
73
|
+
|
|
74
|
+
def eval_B(ti):
|
|
75
|
+
return B_vec
|
|
76
|
+
|
|
77
|
+
for i in range(len(t_vals) - 1):
|
|
78
|
+
ti = t_vals[i]
|
|
79
|
+
Xi = X_vals[i]
|
|
80
|
+
h_loc = t_vals[i + 1] - ti
|
|
81
|
+
|
|
82
|
+
k1 = A @ Xi + eval_B(ti)
|
|
83
|
+
k2 = A @ (Xi + h_loc * k1 / 2) + eval_B(ti + h_loc / 2)
|
|
84
|
+
k3 = A @ (Xi + h_loc * k2 / 2) + eval_B(ti + h_loc / 2)
|
|
85
|
+
k4 = A @ (Xi + h_loc * k3) + eval_B(ti + h_loc)
|
|
86
|
+
|
|
87
|
+
X_vals[i + 1] = Xi + (h_loc / 6) * (k1 + 2 * k2 + 2 * k3 + k4)
|
|
88
|
+
|
|
89
|
+
return t_vals, X_vals
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def solve(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
93
|
+
a = float(payload.get('a', 0.0))
|
|
94
|
+
b = float(payload.get('b', -1.0))
|
|
95
|
+
c = float(payload.get('c', -9.0))
|
|
96
|
+
d = float(payload.get('d', 0.0))
|
|
97
|
+
e_raw = payload.get('e', 1.0)
|
|
98
|
+
f_raw = payload.get('f', 9.0)
|
|
99
|
+
x0 = float(payload.get('x0', 1.0))
|
|
100
|
+
y0 = float(payload.get('y0', 1.0))
|
|
101
|
+
t0 = float(payload.get('t0', 0.0))
|
|
102
|
+
t_fin = float(payload.get('t_fin', 5.0))
|
|
103
|
+
h = float(payload.get('h', 0.01))
|
|
104
|
+
x_min = float(payload.get('x_min', -5.0))
|
|
105
|
+
x_max = float(payload.get('x_max', 5.0))
|
|
106
|
+
y_min = float(payload.get('y_min', -5.0))
|
|
107
|
+
y_max = float(payload.get('y_max', 5.0))
|
|
108
|
+
cantidad_trayectorias = int(payload.get('cantidad_trayectorias', 16))
|
|
109
|
+
|
|
110
|
+
t_sym = sp.symbols('t', real=True)
|
|
111
|
+
|
|
112
|
+
def parse_forcing_component(value):
|
|
113
|
+
if isinstance(value, (int, float, np.integer, np.floating)):
|
|
114
|
+
return sp.Float(value)
|
|
115
|
+
text = str(value).strip()
|
|
116
|
+
try:
|
|
117
|
+
return safe_sympify(text, local_dict={'t': t_sym, 'sin': sp.sin, 'cos': sp.cos, 'tan': sp.tan, 'exp': sp.exp, 'pi': sp.pi}, allowed_symbols=['t'])
|
|
118
|
+
except Exception:
|
|
119
|
+
return sp.Float(text)
|
|
120
|
+
|
|
121
|
+
e_expr = parse_forcing_component(e_raw)
|
|
122
|
+
f_expr = parse_forcing_component(f_raw)
|
|
123
|
+
forcing_is_time_varying = bool((getattr(e_expr, 'free_symbols', set()) and t_sym in e_expr.free_symbols) or (getattr(f_expr, 'free_symbols', set()) and t_sym in f_expr.free_symbols))
|
|
124
|
+
|
|
125
|
+
A = np.array([[a, b], [c, d]], dtype=float)
|
|
126
|
+
if forcing_is_time_varying:
|
|
127
|
+
e_func = sp.lambdify(t_sym, e_expr, 'numpy')
|
|
128
|
+
f_func = sp.lambdify(t_sym, f_expr, 'numpy')
|
|
129
|
+
|
|
130
|
+
def B(ti):
|
|
131
|
+
return np.array([float(e_func(ti)), float(f_func(ti))], dtype=float)
|
|
132
|
+
else:
|
|
133
|
+
B = np.array([float(sp.N(e_expr)), float(sp.N(f_expr))], dtype=float)
|
|
134
|
+
|
|
135
|
+
det_A = float(np.linalg.det(A))
|
|
136
|
+
equilibrio_unico = abs(det_A) > 1e-12 and not forcing_is_time_varying
|
|
137
|
+
equilibrio_pnto = None
|
|
138
|
+
if equilibrio_unico:
|
|
139
|
+
eq = -np.linalg.solve(A, B)
|
|
140
|
+
equilibrio_pnto = {"x": float(eq[0]), "y": float(eq[1])}
|
|
141
|
+
|
|
142
|
+
autovalores, autovectores = np.linalg.eig(A)
|
|
143
|
+
# Fijar un orden estable: primero el autovalor con parte real mayor.
|
|
144
|
+
# Esto hace que v1/v2 no cambien de lugar entre ejecuciones.
|
|
145
|
+
order = np.argsort([-float(np.real(av)) for av in autovalores])
|
|
146
|
+
autovalores = autovalores[order]
|
|
147
|
+
autovectores = autovectores[:, order]
|
|
148
|
+
traza = float(np.trace(A))
|
|
149
|
+
discriminante = traza**2 - 4 * det_A
|
|
150
|
+
clasificacion = Dynamic2DNonHomogeneousService.clasificar_sistema(A)
|
|
151
|
+
comportamiento = Dynamic2DNonHomogeneousService.descripcion_comportamiento(clasificacion)
|
|
152
|
+
|
|
153
|
+
list_autovalores = []
|
|
154
|
+
for av in autovalores:
|
|
155
|
+
list_autovalores.append({
|
|
156
|
+
"real": float(np.real(av)),
|
|
157
|
+
"imag": float(np.imag(av))
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
list_autovectores = []
|
|
161
|
+
for i in range(len(autovalores)):
|
|
162
|
+
list_autovectores.append({
|
|
163
|
+
"vx": float(np.real(autovectores[0, i])),
|
|
164
|
+
"vy": float(np.real(autovectores[1, i]))
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
X0 = np.array([x0, y0], dtype=float)
|
|
168
|
+
t_vals, X_vals = Dynamic2DNonHomogeneousService.rk4_sistema(A, B, X0, t0, t_fin, h)
|
|
169
|
+
|
|
170
|
+
principal_trajectory = {
|
|
171
|
+
"t": t_vals.tolist(),
|
|
172
|
+
"x": X_vals[:, 0].tolist(),
|
|
173
|
+
"y": X_vals[:, 1].tolist(),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
automatic_trajectories = []
|
|
177
|
+
n_side = int(math.ceil(math.sqrt(cantidad_trayectorias)))
|
|
178
|
+
if n_side > 1:
|
|
179
|
+
x0_auto = np.linspace(x_min, x_max, n_side)
|
|
180
|
+
y0_auto = np.linspace(y_min, y_max, n_side)
|
|
181
|
+
for xi in x0_auto:
|
|
182
|
+
for yi in y0_auto:
|
|
183
|
+
_, X_auto = Dynamic2DNonHomogeneousService.rk4_sistema(A, B, np.array([xi, yi]), t0, t_fin, h)
|
|
184
|
+
automatic_trajectories.append({
|
|
185
|
+
"x": X_auto[::5, 0].tolist(),
|
|
186
|
+
"y": X_auto[::5, 1].tolist()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
C1, C2 = sp.symbols('C1 C2')
|
|
190
|
+
# Definiciones de funciones simbólicas para generar ecuaciones en LaTeX
|
|
191
|
+
xs, ys = sp.Function('x'), sp.Function('y')
|
|
192
|
+
|
|
193
|
+
# 1. Solución Homogénea (forma general) con C1, C2 simbólicos
|
|
194
|
+
# Usamos SymPy para obtener autovalores/autovectores simbólicos más simples
|
|
195
|
+
A_sym = sp.Matrix([[a, b], [c, d]]).applyfunc(sp.nsimplify)
|
|
196
|
+
evects = A_sym.eigenvects()
|
|
197
|
+
|
|
198
|
+
# Manejo sencillo: si aparecen autovalores complejos, devolvemos placeholder
|
|
199
|
+
complex_eigen = any([ev[0].has(sp.I) for ev in evects])
|
|
200
|
+
if complex_eigen:
|
|
201
|
+
lam = None
|
|
202
|
+
v = None
|
|
203
|
+
for ev_sym, mult, vecs in evects:
|
|
204
|
+
if sp.im(ev_sym) > 0:
|
|
205
|
+
lam = ev_sym
|
|
206
|
+
v = vecs[0]
|
|
207
|
+
break
|
|
208
|
+
if lam is None:
|
|
209
|
+
lam = evects[0][0]
|
|
210
|
+
v = evects[0][2][0]
|
|
211
|
+
|
|
212
|
+
alpha = sp.re(lam)
|
|
213
|
+
beta = sp.im(lam)
|
|
214
|
+
|
|
215
|
+
vec = sp.Matrix(v)
|
|
216
|
+
dens = [el.as_numer_denom()[1] for el in vec]
|
|
217
|
+
l = 1
|
|
218
|
+
for den_val in dens:
|
|
219
|
+
l = sp.ilcm(l, den_val)
|
|
220
|
+
vec_int = (vec * l).applyfunc(sp.simplify)
|
|
221
|
+
|
|
222
|
+
a_vec = vec_int.applyfunc(sp.re)
|
|
223
|
+
b_vec = vec_int.applyfunc(sp.im)
|
|
224
|
+
|
|
225
|
+
nums = []
|
|
226
|
+
for val in list(a_vec) + list(b_vec):
|
|
227
|
+
if val != 0:
|
|
228
|
+
nums.append(abs(int(val)))
|
|
229
|
+
if nums:
|
|
230
|
+
g = nums[0]
|
|
231
|
+
for n in nums[1:]:
|
|
232
|
+
g = math.gcd(g, n)
|
|
233
|
+
if g > 1:
|
|
234
|
+
vec_int = (vec_int / g).applyfunc(sp.simplify)
|
|
235
|
+
a_vec = vec_int.applyfunc(sp.re)
|
|
236
|
+
b_vec = vec_int.applyfunc(sp.im)
|
|
237
|
+
|
|
238
|
+
autovectores_normalizados = []
|
|
239
|
+
autovectores_relaciones_latex = []
|
|
240
|
+
autovectores_parametricos_latex = []
|
|
241
|
+
|
|
242
|
+
exp_at = sp.exp(alpha * t_sym)
|
|
243
|
+
cos_bt = sp.cos(beta * t_sym)
|
|
244
|
+
sin_bt = sp.sin(beta * t_sym)
|
|
245
|
+
|
|
246
|
+
term1 = a_vec * cos_bt - b_vec * sin_bt
|
|
247
|
+
term2 = a_vec * sin_bt + b_vec * cos_bt
|
|
248
|
+
|
|
249
|
+
X1 = sp.simplify(exp_at * term1)
|
|
250
|
+
X2 = sp.simplify(exp_at * term2)
|
|
251
|
+
|
|
252
|
+
sol_homogenea_vectorial = sp.simplify(C1 * X1 + C2 * X2)
|
|
253
|
+
|
|
254
|
+
term1_latex = sp.latex(term1)
|
|
255
|
+
term2_latex = sp.latex(term2)
|
|
256
|
+
exp_latex = sp.latex(exp_at)
|
|
257
|
+
|
|
258
|
+
sol_homogenea_vectorial_unmultiplied_latex = f"X_h(t) = C_1 {exp_latex} \\left( {term1_latex} \\right) + C_2 {exp_latex} \\left( {term2_latex} \\right)"
|
|
259
|
+
sol_homogenea_vectorial_latex = f"X_h(t) = {sp.latex(sol_homogenea_vectorial)}"
|
|
260
|
+
|
|
261
|
+
xh_t = sp.simplify(sol_homogenea_vectorial[0])
|
|
262
|
+
yh_t = sp.simplify(sol_homogenea_vectorial[1])
|
|
263
|
+
sol_homogenea_componentes_latex = [sp.latex(sp.Eq(xs(t_sym), xh_t)), sp.latex(sp.Eq(ys(t_sym), yh_t))]
|
|
264
|
+
|
|
265
|
+
if forcing_is_time_varying:
|
|
266
|
+
sol_particular_latex = ["Solución particular simbólica no soportada para forcings variantes con autovalores complejos."]
|
|
267
|
+
Xp_sym = sp.Matrix([0, 0])
|
|
268
|
+
else:
|
|
269
|
+
Xp = np.zeros(2)
|
|
270
|
+
if equilibrio_pnto:
|
|
271
|
+
Xp = np.array([equilibrio_pnto['x'], equilibrio_pnto['y']])
|
|
272
|
+
Xp_sym = sp.Matrix(Xp)
|
|
273
|
+
sol_particular_latex = [f"X_p = \\begin{{pmatrix}} {Xp[0]:g} \\\\ {Xp[1]:g} \\end{{pmatrix}}"]
|
|
274
|
+
|
|
275
|
+
constantes = {"c1": "C1", "c2": "C2"}
|
|
276
|
+
|
|
277
|
+
sol_general_vectorial = sp.simplify(sol_homogenea_vectorial + Xp_sym)
|
|
278
|
+
sol_general_vectorial_unmultiplied_latex = f"X(t) = C_1 {exp_latex} \\left( {term1_latex} \\right) + C_2 {exp_latex} \\left( {term2_latex} \\right) + {sp.latex(Xp_sym)}"
|
|
279
|
+
sol_general_vectorial_latex = f"X(t) = {sp.latex(sol_general_vectorial)}"
|
|
280
|
+
|
|
281
|
+
x_t = sp.simplify(sol_general_vectorial[0])
|
|
282
|
+
y_t = sp.simplify(sol_general_vectorial[1])
|
|
283
|
+
sol_general_latex = [sp.latex(sp.Eq(xs(t_sym), x_t)), sp.latex(sp.Eq(ys(t_sym), y_t))]
|
|
284
|
+
else:
|
|
285
|
+
# Extraer autovalores y autovectores simples
|
|
286
|
+
# evects: list of tuples (eigenvalue, multiplicity, [eigenvectors])
|
|
287
|
+
# Queremos alinear el orden simbólico con el orden de autovalores de numpy
|
|
288
|
+
def simplify_sym_vector_to_ints(vec_sym: sp.Matrix):
|
|
289
|
+
vec = sp.Matrix(vec_sym)
|
|
290
|
+
dens = [sp.denom(el) for el in vec]
|
|
291
|
+
l = 1
|
|
292
|
+
for d in dens:
|
|
293
|
+
l = sp.ilcm(l, d)
|
|
294
|
+
vec_int = (vec * l).applyfunc(sp.simplify)
|
|
295
|
+
# convertir a enteros y reducir por GCD
|
|
296
|
+
nums = [int(sp.sign(el) * abs(sp.Integer(el.as_numer_denom()[0]))) for el in vec_int]
|
|
297
|
+
g = 0
|
|
298
|
+
for n in nums:
|
|
299
|
+
g = abs(n) if g == 0 else math.gcd(g, abs(n))
|
|
300
|
+
if g > 1:
|
|
301
|
+
nums = [int(n // g) for n in nums]
|
|
302
|
+
# evitar vector cero
|
|
303
|
+
if all(n == 0 for n in nums):
|
|
304
|
+
return None
|
|
305
|
+
# Preferir primer componente no negativo para presentación
|
|
306
|
+
if nums[0] < 0:
|
|
307
|
+
nums = [-n for n in nums]
|
|
308
|
+
return nums
|
|
309
|
+
|
|
310
|
+
# Construir lista de autovalores simbólicos en el mismo orden que numpy devuelve autovalores
|
|
311
|
+
eigen_syms_ordered = []
|
|
312
|
+
used_evects = [False] * len(evects)
|
|
313
|
+
for idx_num, av in enumerate(autovalores):
|
|
314
|
+
matched = False
|
|
315
|
+
for j, ev in enumerate(evects):
|
|
316
|
+
if used_evects[j]:
|
|
317
|
+
continue
|
|
318
|
+
try:
|
|
319
|
+
if abs(float(np.real(ev[0])) - float(np.real(av))) < 1e-6:
|
|
320
|
+
eigen_syms_ordered.append(ev[0])
|
|
321
|
+
used_evects[j] = True
|
|
322
|
+
matched = True
|
|
323
|
+
break
|
|
324
|
+
except Exception:
|
|
325
|
+
continue
|
|
326
|
+
if not matched:
|
|
327
|
+
# si no encontramos por cercanía, usar el siguiente no usado
|
|
328
|
+
for j, ev in enumerate(evects):
|
|
329
|
+
if not used_evects[j]:
|
|
330
|
+
eigen_syms_ordered.append(ev[0])
|
|
331
|
+
used_evects[j] = True
|
|
332
|
+
break
|
|
333
|
+
|
|
334
|
+
# Preferir autovectores simbólicos (SymPy) si dan entradas racionales simples.
|
|
335
|
+
v1_nums = None
|
|
336
|
+
v2_nums = None
|
|
337
|
+
try:
|
|
338
|
+
# intentar extraer autovectores simbólicos de SymPy en el orden de numpy
|
|
339
|
+
sym_vecs_ordered = []
|
|
340
|
+
for lam_sym in eigen_syms_ordered:
|
|
341
|
+
# buscar el evects que corresponde a lam_sym
|
|
342
|
+
found = False
|
|
343
|
+
for ev in evects:
|
|
344
|
+
try:
|
|
345
|
+
if abs(float(ev[0]) - float(lam_sym)) < 1e-8:
|
|
346
|
+
sym_vecs_ordered.append(ev[2][0])
|
|
347
|
+
found = True
|
|
348
|
+
break
|
|
349
|
+
except Exception:
|
|
350
|
+
continue
|
|
351
|
+
if not found:
|
|
352
|
+
sym_vecs_ordered.append(None)
|
|
353
|
+
|
|
354
|
+
# simplificar cada vector simbólico si existe
|
|
355
|
+
v_nums_list = []
|
|
356
|
+
for sv in sym_vecs_ordered:
|
|
357
|
+
if sv is not None:
|
|
358
|
+
v_try = simplify_sym_vector_to_ints(sv)
|
|
359
|
+
v_nums_list.append(v_try)
|
|
360
|
+
else:
|
|
361
|
+
v_nums_list.append(None)
|
|
362
|
+
|
|
363
|
+
# asignar por posición (v1 = primero, v2 = segundo) si disponibles
|
|
364
|
+
if len(v_nums_list) >= 1:
|
|
365
|
+
v1_nums = v_nums_list[0]
|
|
366
|
+
if len(v_nums_list) >= 2:
|
|
367
|
+
v2_nums = v_nums_list[1]
|
|
368
|
+
except Exception:
|
|
369
|
+
v1_nums = None
|
|
370
|
+
v2_nums = None
|
|
371
|
+
|
|
372
|
+
# Si no obtuvimos vectores simbólicos simples, caer al método numérico
|
|
373
|
+
from fractions import Fraction
|
|
374
|
+
|
|
375
|
+
def numeric_to_simple_ints(v: np.ndarray, max_den: int = 12):
|
|
376
|
+
comps = [float(x) for x in v]
|
|
377
|
+
frs = [Fraction(c).limit_denominator(max_den) for c in comps]
|
|
378
|
+
dens = [fr.denominator for fr in frs]
|
|
379
|
+
l = 1
|
|
380
|
+
for d in dens:
|
|
381
|
+
l = l * d // math.gcd(l, d)
|
|
382
|
+
ints = [fr.numerator * (l // fr.denominator) for fr in frs]
|
|
383
|
+
# reducir por GCD
|
|
384
|
+
g = 0
|
|
385
|
+
for val in ints:
|
|
386
|
+
g = abs(val) if g == 0 else math.gcd(g, abs(val))
|
|
387
|
+
if g > 1:
|
|
388
|
+
ints = [int(val // g) for val in ints]
|
|
389
|
+
# evitar el vector cero
|
|
390
|
+
if all(val == 0 for val in ints):
|
|
391
|
+
if abs(comps[0]) >= abs(comps[1]):
|
|
392
|
+
ints = [1, 0]
|
|
393
|
+
else:
|
|
394
|
+
ints = [0, 1]
|
|
395
|
+
# Preferir primer componente no negativo
|
|
396
|
+
if ints[0] < 0:
|
|
397
|
+
ints = [-val for val in ints]
|
|
398
|
+
return ints
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
if v1_nums is None:
|
|
402
|
+
v1_nums = numeric_to_simple_ints(autovectores[:, 0].real)
|
|
403
|
+
except Exception:
|
|
404
|
+
v1_nums = [1, 0]
|
|
405
|
+
try:
|
|
406
|
+
if v2_nums is None:
|
|
407
|
+
v2_nums = numeric_to_simple_ints(autovectores[:, 1].real) if len(autovalores) > 1 else [0, 1]
|
|
408
|
+
except Exception:
|
|
409
|
+
v2_nums = [0, 1]
|
|
410
|
+
|
|
411
|
+
# Mantener la versión simplificada numérica (temporal), se reemplazará
|
|
412
|
+
# por la versión normalizada más abajo para la salida y las soluciones.
|
|
413
|
+
temp_autovectores = [
|
|
414
|
+
{"vx": int(v1_nums[0]), "vy": int(v1_nums[1])},
|
|
415
|
+
{"vx": int(v2_nums[0]), "vy": int(v2_nums[1])}
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
# Generar pasos simbólicos para despejar la relación entre componentes del autovector
|
|
419
|
+
k1_sym, k2_sym = sp.symbols('k_1 k_2')
|
|
420
|
+
autovectores_pasos_latex = []
|
|
421
|
+
# guardamos la relación simbólica y el objeto relacional para derivar luego el vector
|
|
422
|
+
autovectores_relaciones_text_sym = []
|
|
423
|
+
autovectores_relaciones_obj = []
|
|
424
|
+
# usar la lista de autovalores simbólicos ordenada para coincidir con numpy
|
|
425
|
+
eigen_syms = eigen_syms_ordered if 'eigen_syms_ordered' in locals() else []
|
|
426
|
+
for lam in eigen_syms:
|
|
427
|
+
M = A_sym - lam * sp.eye(2)
|
|
428
|
+
eq1 = sp.Eq(M[0, 0] * k1_sym + M[0, 1] * k2_sym, 0)
|
|
429
|
+
eq2 = sp.Eq(M[1, 0] * k1_sym + M[1, 1] * k2_sym, 0)
|
|
430
|
+
basis = M.nullspace()
|
|
431
|
+
basis_vec = basis[0] if basis else None
|
|
432
|
+
if basis_vec is not None:
|
|
433
|
+
basis_nums = simplify_sym_vector_to_ints(basis_vec)
|
|
434
|
+
if basis_nums is None:
|
|
435
|
+
basis_nums = [1, 0]
|
|
436
|
+
else:
|
|
437
|
+
basis_nums = [1, 0]
|
|
438
|
+
|
|
439
|
+
if basis_nums[0] == 0 and basis_nums[1] != 0:
|
|
440
|
+
rel = sp.Eq(k1_sym, 0)
|
|
441
|
+
elif basis_nums[1] == 0 and basis_nums[0] != 0:
|
|
442
|
+
rel = sp.Eq(k2_sym, 0)
|
|
443
|
+
elif basis_nums[0] != 0:
|
|
444
|
+
coeff_rel = sp.Rational(basis_nums[1], basis_nums[0])
|
|
445
|
+
rel = sp.Eq(k2_sym, sp.simplify(coeff_rel * k1_sym))
|
|
446
|
+
else:
|
|
447
|
+
rel = sp.Eq(k2_sym, sp.Integer(0))
|
|
448
|
+
autovectores_pasos_latex.append({
|
|
449
|
+
"sistema_latex": [sp.latex(eq1), sp.latex(eq2)],
|
|
450
|
+
"relacion_latex": sp.latex(rel)
|
|
451
|
+
})
|
|
452
|
+
# construir una versión en texto plano de la relación, p.ej. 'k2 = -3 k1'
|
|
453
|
+
try:
|
|
454
|
+
lhs = str(rel.lhs).replace('k_1', 'k1').replace('k_2', 'k2')
|
|
455
|
+
rhs_expr = rel.rhs
|
|
456
|
+
# si rhs es multiplicación de coeficiente racional y k1/k2, extraer coef y variable
|
|
457
|
+
if rhs_expr.is_Mul:
|
|
458
|
+
coeff = None
|
|
459
|
+
var = None
|
|
460
|
+
for arg in rhs_expr.args:
|
|
461
|
+
if arg.is_Number or arg.is_Rational or arg.is_Integer:
|
|
462
|
+
coeff = arg
|
|
463
|
+
else:
|
|
464
|
+
var = arg
|
|
465
|
+
# formatear coeficiente
|
|
466
|
+
if coeff is None:
|
|
467
|
+
coeff_str = '1'
|
|
468
|
+
elif isinstance(coeff, sp.Rational):
|
|
469
|
+
if coeff.q == 1:
|
|
470
|
+
coeff_str = str(int(coeff.p))
|
|
471
|
+
else:
|
|
472
|
+
coeff_str = f"{int(coeff.p)}/{int(coeff.q)}"
|
|
473
|
+
else:
|
|
474
|
+
coeff_str = str(float(coeff))
|
|
475
|
+
var_str = str(var).replace('k_1', 'k1').replace('k_2', 'k2') if var is not None else ''
|
|
476
|
+
rhs_text = f"{coeff_str} {var_str}".strip()
|
|
477
|
+
else:
|
|
478
|
+
rhs_text = str(rhs_expr).replace('k_1', 'k1').replace('k_2', 'k2').replace('*', ' ')
|
|
479
|
+
rel_text = f"{lhs} = {rhs_text}"
|
|
480
|
+
except Exception:
|
|
481
|
+
rel_text = str(rel).replace('k_1', 'k1').replace('k_2', 'k2')
|
|
482
|
+
# guardar la versión en texto plano de la relación simbólica
|
|
483
|
+
autovectores_relaciones_text_sym.append(rel_text)
|
|
484
|
+
autovectores_relaciones_obj.append(rel)
|
|
485
|
+
|
|
486
|
+
# Normalizar a partir de la relación del sistema: primero se despeja, luego se fija la componente libre.
|
|
487
|
+
autovectores_normalizados = []
|
|
488
|
+
autovectores_relaciones_latex = []
|
|
489
|
+
autovectores_parametricos_latex = []
|
|
490
|
+
|
|
491
|
+
def relation_to_vector(rel, fallback_nums):
|
|
492
|
+
def simplificar_coefs(coeff):
|
|
493
|
+
coeff = sp.nsimplify(coeff)
|
|
494
|
+
if coeff.is_Rational:
|
|
495
|
+
return coeff
|
|
496
|
+
try:
|
|
497
|
+
return sp.Rational(coeff)
|
|
498
|
+
except Exception:
|
|
499
|
+
return coeff
|
|
500
|
+
|
|
501
|
+
def vector_from_coeff(lhs_is_k2, coeff):
|
|
502
|
+
coeff = simplificar_coefs(coeff)
|
|
503
|
+
if coeff == 0:
|
|
504
|
+
return [1, 0] if lhs_is_k2 else [0, 1]
|
|
505
|
+
if isinstance(coeff, sp.Rational):
|
|
506
|
+
p, q = int(coeff.p), int(coeff.q)
|
|
507
|
+
if lhs_is_k2:
|
|
508
|
+
return [q, p]
|
|
509
|
+
return [p, q]
|
|
510
|
+
try:
|
|
511
|
+
val = float(coeff)
|
|
512
|
+
rounded = int(round(val))
|
|
513
|
+
return [1, rounded] if lhs_is_k2 else [rounded, 1]
|
|
514
|
+
except Exception:
|
|
515
|
+
a, b = fallback_nums[0], fallback_nums[1]
|
|
516
|
+
return [int(a), int(b)]
|
|
517
|
+
|
|
518
|
+
if rel is None:
|
|
519
|
+
a, b = fallback_nums[0], fallback_nums[1]
|
|
520
|
+
if a == 0 and b == 0:
|
|
521
|
+
return [1, 0]
|
|
522
|
+
if a == 0:
|
|
523
|
+
return [0, 1]
|
|
524
|
+
ratio = Fraction(b, a)
|
|
525
|
+
if ratio.denominator == 1:
|
|
526
|
+
return [1, int(ratio)]
|
|
527
|
+
return [ratio.denominator, ratio.numerator]
|
|
528
|
+
|
|
529
|
+
if rel.lhs == k2_sym:
|
|
530
|
+
coeff = sp.simplify(rel.rhs / k1_sym) if rel.rhs != 0 else sp.Integer(0)
|
|
531
|
+
vec = vector_from_coeff(True, coeff)
|
|
532
|
+
elif rel.lhs == k1_sym:
|
|
533
|
+
coeff = sp.simplify(rel.rhs / k2_sym) if rel.rhs != 0 else sp.Integer(0)
|
|
534
|
+
vec = vector_from_coeff(False, coeff)
|
|
535
|
+
else:
|
|
536
|
+
a, b = fallback_nums[0], fallback_nums[1]
|
|
537
|
+
vec = [int(a), int(b)]
|
|
538
|
+
|
|
539
|
+
if vec[0] == 0 and vec[1] == 0:
|
|
540
|
+
vec = [1, 0]
|
|
541
|
+
if vec[0] != 0:
|
|
542
|
+
gcd = math.gcd(abs(int(vec[0])), abs(int(vec[1]))) if vec[1] != 0 else abs(int(vec[0]))
|
|
543
|
+
if gcd > 1:
|
|
544
|
+
vec = [int(vec[0] // gcd), int(vec[1] // gcd)]
|
|
545
|
+
return vec
|
|
546
|
+
|
|
547
|
+
def relation_text_from_vector(vec):
|
|
548
|
+
vx, vy = int(vec[0]), int(vec[1])
|
|
549
|
+
if vx == 0 and vy == 0:
|
|
550
|
+
return ""
|
|
551
|
+
if vy == 0:
|
|
552
|
+
return "k2 = 0"
|
|
553
|
+
if vx == 0:
|
|
554
|
+
return "k1 = 0"
|
|
555
|
+
ratio = Fraction(vy, vx)
|
|
556
|
+
if ratio.denominator == 1:
|
|
557
|
+
return f"k2 = {ratio.numerator} k1"
|
|
558
|
+
return f"k2 = {ratio.numerator}/{ratio.denominator} k1"
|
|
559
|
+
|
|
560
|
+
rel_v1 = autovectores_relaciones_obj[0] if len(autovectores_relaciones_obj) > 0 else None
|
|
561
|
+
rel_v2 = autovectores_relaciones_obj[1] if len(autovectores_relaciones_obj) > 1 else None
|
|
562
|
+
|
|
563
|
+
v1_norm = relation_to_vector(rel_v1, v1_nums)
|
|
564
|
+
v2_norm = relation_to_vector(rel_v2, v2_nums)
|
|
565
|
+
v1_rel = relation_text_from_vector(v1_norm)
|
|
566
|
+
v2_rel = relation_text_from_vector(v2_norm)
|
|
567
|
+
v1_param = f"\\mathbf{{v}} = k \\begin{{pmatrix}}{v1_norm[0]}\\\\{v1_norm[1]}\\end{{pmatrix}}"
|
|
568
|
+
v2_param = f"\\mathbf{{v}} = k \\begin{{pmatrix}}{v2_norm[0]}\\\\{v2_norm[1]}\\end{{pmatrix}}"
|
|
569
|
+
|
|
570
|
+
autovectores_normalizados.append({"vx": v1_norm[0], "vy": v1_norm[1]})
|
|
571
|
+
autovectores_normalizados.append({"vx": v2_norm[0], "vy": v2_norm[1]})
|
|
572
|
+
autovectores_relaciones_latex.append(v1_rel)
|
|
573
|
+
autovectores_relaciones_latex.append(v2_rel)
|
|
574
|
+
autovectores_parametricos_latex.append(v1_param)
|
|
575
|
+
autovectores_parametricos_latex.append(v2_param)
|
|
576
|
+
|
|
577
|
+
# La relación textual se deriva del autovector normalizado.
|
|
578
|
+
autovectores_relaciones_text = [v1_rel, v2_rel]
|
|
579
|
+
|
|
580
|
+
# Usar los autovectores *normalizados* para construir las expresiones simbólicas
|
|
581
|
+
v1_sym = sp.Matrix([autovectores_normalizados[0]["vx"], autovectores_normalizados[0]["vy"]])
|
|
582
|
+
v2_sym = sp.Matrix([autovectores_normalizados[1]["vx"], autovectores_normalizados[1]["vy"]])
|
|
583
|
+
|
|
584
|
+
# Actualizar la lista de autovectores de salida a las versiones normalizadas
|
|
585
|
+
list_autovectores = autovectores_normalizados
|
|
586
|
+
|
|
587
|
+
# Asegurar que lambda1_sym/lambda2_sym estén definidos (usar el orden simbólico si existe)
|
|
588
|
+
if 'eigen_syms_ordered' in locals() and len(eigen_syms_ordered) > 0:
|
|
589
|
+
lambda1_sym = eigen_syms_ordered[0]
|
|
590
|
+
lambda2_sym = eigen_syms_ordered[1] if len(eigen_syms_ordered) > 1 else eigen_syms_ordered[0]
|
|
591
|
+
else:
|
|
592
|
+
# fallback: usar primeros autovalores simbólicos de evects
|
|
593
|
+
lambda1_sym = evects[0][0]
|
|
594
|
+
lambda2_sym = evects[1][0] if len(evects) > 1 else evects[0][0]
|
|
595
|
+
|
|
596
|
+
# Solución homogénea: forma con autovectores sin multiplicar (C1 e^{λ1 t} v1 + C2 e^{λ2 t} v2)
|
|
597
|
+
scalar1 = sp.simplify(C1 * sp.exp(lambda1_sym * t_sym))
|
|
598
|
+
scalar2 = sp.simplify(C2 * sp.exp(lambda2_sym * t_sym))
|
|
599
|
+
vec1_latex = sp.latex(v1_sym)
|
|
600
|
+
vec2_latex = sp.latex(v2_sym)
|
|
601
|
+
sol_homogenea_vectorial_unmultiplied_latex = f"X_h(t) = {sp.latex(scalar1)} {vec1_latex} + {sp.latex(scalar2)} {vec2_latex}"
|
|
602
|
+
|
|
603
|
+
# Versión multiplicada y simplificada (componentes explícitas)
|
|
604
|
+
sol_homogenea_vectorial = C1 * v1_sym * sp.exp(lambda1_sym * t_sym) + C2 * v2_sym * sp.exp(lambda2_sym * t_sym)
|
|
605
|
+
sol_homogenea_vectorial_latex = f"X_h(t) = {sp.latex(sol_homogenea_vectorial)}"
|
|
606
|
+
|
|
607
|
+
# Componentes x_h(t) y y_h(t) de la solución homogénea (multiplicada)
|
|
608
|
+
xh_t = sp.simplify(sol_homogenea_vectorial[0])
|
|
609
|
+
yh_t = sp.simplify(sol_homogenea_vectorial[1])
|
|
610
|
+
sol_homogenea_componentes_latex = [sp.latex(sp.Eq(xs(t_sym), xh_t)), sp.latex(sp.Eq(ys(t_sym), yh_t))]
|
|
611
|
+
|
|
612
|
+
# 2. Solución Particular
|
|
613
|
+
if forcing_is_time_varying and abs(b) < 1e-12 and abs(c) < 1e-12:
|
|
614
|
+
# Caso diagonal: resolver componente por componente con factor integrante.
|
|
615
|
+
def trig_particular(forcing_expr, coeff):
|
|
616
|
+
# Solución particular de y' - coeff*y = forcing(t) por factor
|
|
617
|
+
# integrante: y_p(t) = e^{coeff t} ∫ e^{-coeff t} forcing(t) dt.
|
|
618
|
+
# Vale para CUALQUIER forzado, incluidas frecuencias ω ≠ 1
|
|
619
|
+
# (antes, el ensayo u·cos(t)+v·sin(t) solo era correcto para ω=1
|
|
620
|
+
# y devolvía soluciones que no satisfacían la EDO para ω≠1).
|
|
621
|
+
raw = sp.simplify(
|
|
622
|
+
sp.exp(coeff * t_sym)
|
|
623
|
+
* sp.integrate(sp.exp(-coeff * t_sym) * forcing_expr, t_sym)
|
|
624
|
+
)
|
|
625
|
+
return sp.expand_trig(raw)
|
|
626
|
+
|
|
627
|
+
x_p_t = trig_particular(e_expr, a)
|
|
628
|
+
y_p_t = trig_particular(f_expr, d)
|
|
629
|
+
Xp_sym = sp.Matrix([x_p_t, y_p_t])
|
|
630
|
+
sol_particular_latex = [f"X_p(t) = {sp.latex(Xp_sym)}"]
|
|
631
|
+
Xp = np.array([0.0, 0.0], dtype=float)
|
|
632
|
+
else:
|
|
633
|
+
Xp = np.zeros(2)
|
|
634
|
+
if equilibrio_pnto:
|
|
635
|
+
Xp = np.array([equilibrio_pnto['x'], equilibrio_pnto['y']])
|
|
636
|
+
# Mostrar la solución particular redondeada a 0 decimales para presentación
|
|
637
|
+
sol_particular_latex = [f"X_p = \\begin{{pmatrix}} {Xp[0]:g} \\\\ {Xp[1]:g} \\end{{pmatrix}}"]
|
|
638
|
+
|
|
639
|
+
# 3. No calculamos numéricamente C1, C2: los dejamos simbólicos
|
|
640
|
+
constantes = {"c1": "C1", "c2": "C2"}
|
|
641
|
+
|
|
642
|
+
# 4. Solución general vectorial simbólica: mostrar primero la parte homogénea sin multiplicar
|
|
643
|
+
if forcing_is_time_varying and abs(b) < 1e-12 and abs(c) < 1e-12:
|
|
644
|
+
sol_general_vectorial_unmultiplied_latex = f"X(t) = {sp.latex(scalar1)} {vec1_latex} + {sp.latex(scalar2)} {vec2_latex} + {sp.latex(Xp_sym)}"
|
|
645
|
+
else:
|
|
646
|
+
Xp_sym = sp.Matrix(Xp)
|
|
647
|
+
sol_general_vectorial_unmultiplied_latex = f"X(t) = {sp.latex(scalar1)} {vec1_latex} + {sp.latex(scalar2)} {vec2_latex} + {sp.latex(Xp_sym)}"
|
|
648
|
+
|
|
649
|
+
# 5. Solución general multiplicada y simplificada (componentes explícitas)
|
|
650
|
+
if forcing_is_time_varying and abs(b) < 1e-12 and abs(c) < 1e-12:
|
|
651
|
+
sol_general_vectorial = sol_homogenea_vectorial + Xp_sym
|
|
652
|
+
else:
|
|
653
|
+
sol_general_vectorial = sol_homogenea_vectorial + Xp_sym
|
|
654
|
+
sol_general_vectorial_latex = f"X(t) = {sp.latex(sol_general_vectorial)}"
|
|
655
|
+
|
|
656
|
+
x_t = sp.simplify(sol_general_vectorial[0])
|
|
657
|
+
y_t = sp.simplify(sol_general_vectorial[1])
|
|
658
|
+
sol_general_latex = [sp.latex(sp.Eq(xs(t_sym), x_t)), sp.latex(sp.Eq(ys(t_sym), y_t))]
|
|
659
|
+
|
|
660
|
+
# Si definimos autovectores_pasos_latex en el flujo anterior, úsalo; si no, vacío
|
|
661
|
+
try:
|
|
662
|
+
autovectores_pasos_latex
|
|
663
|
+
except NameError:
|
|
664
|
+
autovectores_pasos_latex = []
|
|
665
|
+
try:
|
|
666
|
+
autovectores_relaciones_text
|
|
667
|
+
except NameError:
|
|
668
|
+
autovectores_relaciones_text = []
|
|
669
|
+
|
|
670
|
+
real_eigenvectors_lines = []
|
|
671
|
+
if equilibrio_unico and equilibrio_pnto:
|
|
672
|
+
for i in range(len(autovalores)):
|
|
673
|
+
if abs(np.imag(autovalores[i])) < 1e-10:
|
|
674
|
+
vx, vy = float(np.real(autovectores[0, i])), float(np.real(autovectores[1, i]))
|
|
675
|
+
norm = math.sqrt(vx**2 + vy**2)
|
|
676
|
+
if norm > 1e-9:
|
|
677
|
+
vx, vy = vx / norm, vy / norm
|
|
678
|
+
real_eigenvectors_lines.append([
|
|
679
|
+
[equilibrio_pnto["x"] - 20 * vx, equilibrio_pnto["y"] - 20 * vy],
|
|
680
|
+
[equilibrio_pnto["x"] + 20 * vx, equilibrio_pnto["y"] + 20 * vy]
|
|
681
|
+
])
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
"matrix_a": [[a, b], [c, d]],
|
|
685
|
+
"vector_b": [e_raw, f_raw],
|
|
686
|
+
"traza": traza,
|
|
687
|
+
"determinante": det_A,
|
|
688
|
+
"clasificacion_homogenea": clasificacion,
|
|
689
|
+
"comportamiento": comportamiento,
|
|
690
|
+
"equilibrio": equilibrio_pnto,
|
|
691
|
+
"autovalores": list_autovalores,
|
|
692
|
+
"autovectores": list_autovectores,
|
|
693
|
+
"autovectores_normalizados": autovectores_normalizados,
|
|
694
|
+
"autovectores_relaciones_latex": autovectores_relaciones_latex,
|
|
695
|
+
"autovectores_parametricos_latex": autovectores_parametricos_latex,
|
|
696
|
+
"autovectores_pasos_latex": autovectores_pasos_latex,
|
|
697
|
+
"autovectores_relaciones_text": autovectores_relaciones_text,
|
|
698
|
+
"constantes": constantes,
|
|
699
|
+
"solucion_homogenea_vectorial_latex": sol_homogenea_vectorial_latex,
|
|
700
|
+
"solucion_homogenea_vectorial_unmultiplied_latex": sol_homogenea_vectorial_unmultiplied_latex,
|
|
701
|
+
"solucion_homogenea_componentes_latex": sol_homogenea_componentes_latex,
|
|
702
|
+
"solucion_particular_latex": sol_particular_latex,
|
|
703
|
+
"solucion_general_vectorial_unmultiplied_latex": sol_general_vectorial_unmultiplied_latex,
|
|
704
|
+
"solucion_general_vectorial_latex": sol_general_vectorial_latex,
|
|
705
|
+
"solucion_general_latex": sol_general_latex,
|
|
706
|
+
"principal_trajectory": principal_trajectory,
|
|
707
|
+
"automatic_trajectories": automatic_trajectories,
|
|
708
|
+
"real_eigenvectors_lines": real_eigenvectors_lines
|
|
709
|
+
}
|
|
710
|
+
|