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.
@@ -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
+