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,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
+ }