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,312 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import sympy as sp
|
|
3
|
+
import warnings
|
|
4
|
+
from scipy.stats import norm
|
|
5
|
+
from numpy.polynomial.legendre import leggauss
|
|
6
|
+
from typing import Dict, Tuple, List
|
|
7
|
+
from .utils import safe_sympify
|
|
8
|
+
|
|
9
|
+
class MonteCarloService:
|
|
10
|
+
@staticmethod
|
|
11
|
+
def aplicar_semilla(seed=None):
|
|
12
|
+
if seed is not None:
|
|
13
|
+
np.random.seed(int(seed))
|
|
14
|
+
else:
|
|
15
|
+
np.random.seed(None)
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def calcular_z_score(nivel_confianza=0.95):
|
|
19
|
+
alfa = 1.0 - nivel_confianza
|
|
20
|
+
z_score = norm.ppf(1.0 - (alfa / 2.0))
|
|
21
|
+
return z_score
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def compilar_funcion(texto_funcion: str, variables: str = 'x'):
|
|
25
|
+
try:
|
|
26
|
+
vars_sympy = sp.symbols(variables)
|
|
27
|
+
expr = safe_sympify(texto_funcion, allowed_symbols=variables.split())
|
|
28
|
+
f_compilada = sp.lambdify(vars_sympy, expr, modules=['numpy'])
|
|
29
|
+
return f_compilada
|
|
30
|
+
except Exception as e:
|
|
31
|
+
raise ValueError(f"Error compilando funcion: {str(e)}")
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def hit_or_miss_1d(f, a: float, b: float, N: int, seed: int = None, precision: int = 8, nivel_confianza: float = 0.95) -> Dict:
|
|
35
|
+
MonteCarloService.aplicar_semilla(seed)
|
|
36
|
+
|
|
37
|
+
# Le pedimos a Python que calcule los puntos de la línea para el gráfico
|
|
38
|
+
x_test = np.linspace(a, b, 200)
|
|
39
|
+
with warnings.catch_warnings():
|
|
40
|
+
warnings.simplefilter("ignore")
|
|
41
|
+
y_test_raw = f(x_test)
|
|
42
|
+
|
|
43
|
+
# Detección de singularidades en el barrido (para no silenciarlas)
|
|
44
|
+
con_singularidades = bool(np.any(~np.isfinite(y_test_raw)))
|
|
45
|
+
y_test = np.nan_to_num(y_test_raw, nan=0.0, posinf=0.0, neginf=0.0)
|
|
46
|
+
|
|
47
|
+
y_min, y_max = float(np.min(y_test)), float(np.max(y_test))
|
|
48
|
+
y_base = min(0, y_min) * 1.1 if min(0, y_min) < 0 else 0
|
|
49
|
+
y_techo = max(0, y_max) * 1.1
|
|
50
|
+
|
|
51
|
+
x_rand = np.random.uniform(a, b, N)
|
|
52
|
+
y_rand = np.random.uniform(y_base, y_techo, N)
|
|
53
|
+
|
|
54
|
+
with warnings.catch_warnings():
|
|
55
|
+
warnings.simplefilter("ignore")
|
|
56
|
+
f_eval = f(x_rand)
|
|
57
|
+
|
|
58
|
+
exitos_pos = (y_rand > 0) & (y_rand <= f_eval)
|
|
59
|
+
exitos_neg = (y_rand < 0) & (y_rand >= f_eval)
|
|
60
|
+
exitos_totales = exitos_pos | exitos_neg
|
|
61
|
+
n_exitos = int(np.sum(exitos_totales))
|
|
62
|
+
n_pos = int(np.sum(exitos_pos))
|
|
63
|
+
n_neg = int(np.sum(exitos_neg))
|
|
64
|
+
|
|
65
|
+
# Estimación con signo: la caja total [y_base, y_techo] contiene la región
|
|
66
|
+
# positiva y la negativa. El estimador de Hit-or-Miss con signo es
|
|
67
|
+
# I ≈ (b - a) * (y_techo - y_base) * (n_pos - n_neg) / N
|
|
68
|
+
# que reproduce ∫f⁺ - ∫f⁻ (antes devolvía ±∫|f| para integrandos con signo).
|
|
69
|
+
area_caja = (b - a) * (y_techo - y_base)
|
|
70
|
+
integral_aprox = area_caja * (n_pos - n_neg) / N
|
|
71
|
+
|
|
72
|
+
n_no_finitos = int(np.sum(~np.isfinite(f_eval)))
|
|
73
|
+
p_exito = n_exitos / N
|
|
74
|
+
notas = []
|
|
75
|
+
if con_singularidades:
|
|
76
|
+
notas.append(
|
|
77
|
+
"Se detectaron singularidades en el barrido de la caja; los límites se "
|
|
78
|
+
"calcularon con los valores finitos."
|
|
79
|
+
)
|
|
80
|
+
if n_no_finitos > 0:
|
|
81
|
+
notas.append(
|
|
82
|
+
f"{n_no_finitos} muestra(s) no finita(s) se descartaron; el resultado puede estar sesgado."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Para hit-or-miss, la media muestral natural es la media del indicador con signo.
|
|
86
|
+
media_muestral = (n_pos - n_neg) / N
|
|
87
|
+
|
|
88
|
+
# Cálculo Estadístico para Hit-or-Miss (Distribución Binomial)
|
|
89
|
+
desv_std = np.sqrt(p_exito * (1 - p_exito)) if N > 1 else 0
|
|
90
|
+
error_est = desv_std / np.sqrt(N) if N > 0 else 0
|
|
91
|
+
|
|
92
|
+
z_score = MonteCarloService.calcular_z_score(nivel_confianza)
|
|
93
|
+
ic_inf = integral_aprox - z_score * error_est * area_caja
|
|
94
|
+
ic_sup = integral_aprox + z_score * error_est * area_caja
|
|
95
|
+
|
|
96
|
+
# Capeamos a 2000 y ponemos los tildes
|
|
97
|
+
limite = min(N, 2000)
|
|
98
|
+
historial = [
|
|
99
|
+
{"i": i+1, "x": round(float(x_rand[i]), precision), "y_rand": round(float(y_rand[i]), precision),
|
|
100
|
+
"f_x": round(float(f_eval[i]), precision), "exito": "✓" if exitos_totales[i] else "✗"}
|
|
101
|
+
for i in range(limite)
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"metodo": "Hit-or-Miss 1D",
|
|
106
|
+
"integral": round(float(integral_aprox), precision),
|
|
107
|
+
"media_muestral": round(float(media_muestral), precision),
|
|
108
|
+
"N": N,
|
|
109
|
+
"area_caja": round(float(area_caja), precision),
|
|
110
|
+
"n_exitos": n_exitos,
|
|
111
|
+
"escala": round(float(area_caja), precision),
|
|
112
|
+
"desv_estandar": round(float(desv_std), precision),
|
|
113
|
+
"error_std": round(float(error_est), precision),
|
|
114
|
+
"z_score": round(float(z_score), 4),
|
|
115
|
+
"ic_inf": round(float(ic_inf), precision),
|
|
116
|
+
"ic_sup": round(float(ic_sup), precision),
|
|
117
|
+
"historial": historial,
|
|
118
|
+
"x_line": [round(float(x), precision) for x in x_test], # Línea para el frontend
|
|
119
|
+
"y_line": [round(float(y), precision) for y in y_test],
|
|
120
|
+
"grafico_limites": {"y_base": float(y_base), "y_techo": float(y_techo)},
|
|
121
|
+
"notas": notas
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def convergencia_1d(f, a: float, b: float, N: int, seed: int = None, precision: int = 8) -> Dict:
|
|
127
|
+
MonteCarloService.aplicar_semilla(seed)
|
|
128
|
+
x_rand = np.random.uniform(a, b, N)
|
|
129
|
+
with warnings.catch_warnings():
|
|
130
|
+
warnings.simplefilter("ignore")
|
|
131
|
+
f_raw = f(x_rand)
|
|
132
|
+
n_no_finitos = int(np.sum(~np.isfinite(f_raw)))
|
|
133
|
+
f_eval = np.nan_to_num(f_raw, nan=0.0, posinf=0.0, neginf=0.0)
|
|
134
|
+
|
|
135
|
+
promedios_acumulados = np.cumsum(f_eval) / np.arange(1, N + 1)
|
|
136
|
+
integrales_acumuladas = (b - a) * promedios_acumulados
|
|
137
|
+
|
|
138
|
+
# Gauss-Legendre para la línea base de comparación exacta
|
|
139
|
+
x_g, w_g = leggauss(5)
|
|
140
|
+
x_trans = 0.5 * (x_g + 1) * (b - a) + a
|
|
141
|
+
with warnings.catch_warnings():
|
|
142
|
+
warnings.simplefilter("ignore")
|
|
143
|
+
f_gauss = f(x_trans)
|
|
144
|
+
valor_exacto = 0.5 * (b - a) * np.sum(w_g * f_gauss)
|
|
145
|
+
|
|
146
|
+
# Muestreamos el historial logarítmicamente para no matar a Plotly
|
|
147
|
+
step = max(1, N // 500)
|
|
148
|
+
historial = [
|
|
149
|
+
{"N": int(i), "integral": round(float(integrales_acumuladas[i-1]), precision)}
|
|
150
|
+
for i in range(1, N+1, step)
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
"metodo": "Convergencia 1D",
|
|
155
|
+
"N": N,
|
|
156
|
+
"valor_exacto_gauss": round(float(valor_exacto), precision),
|
|
157
|
+
"historial_convergencia": historial,
|
|
158
|
+
"notas": ([f"{n_no_finitos} muestra(s) no finita(s) se trataron como 0; el resultado puede estar sesgado."] if n_no_finitos > 0 else [])
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def valor_promedio_1d(f, a: float, b: float, N: int, seed: int = None, precision: int = 8, nivel_confianza: float = 0.95) -> Dict:
|
|
163
|
+
MonteCarloService.aplicar_semilla(seed)
|
|
164
|
+
x_rand = np.random.uniform(a, b, N)
|
|
165
|
+
with warnings.catch_warnings():
|
|
166
|
+
warnings.simplefilter("ignore")
|
|
167
|
+
f_raw = f(x_rand)
|
|
168
|
+
n_no_finitos = int(np.sum(~np.isfinite(f_raw)))
|
|
169
|
+
f_eval = np.nan_to_num(f_raw, nan=0.0, posinf=0.0, neginf=0.0)
|
|
170
|
+
|
|
171
|
+
promedio = np.mean(f_eval)
|
|
172
|
+
escala = b - a
|
|
173
|
+
integral = escala * promedio
|
|
174
|
+
|
|
175
|
+
desv_std = np.std(f_eval, ddof=1) if N > 1 else 0
|
|
176
|
+
error_est = desv_std / np.sqrt(N) if N > 0 else 0
|
|
177
|
+
|
|
178
|
+
z_score = MonteCarloService.calcular_z_score(nivel_confianza)
|
|
179
|
+
ic_inf = integral - z_score * error_est * escala
|
|
180
|
+
ic_sup = integral + z_score * error_est * escala
|
|
181
|
+
|
|
182
|
+
limite = min(N, 2000)
|
|
183
|
+
historial = [{"i": i+1, "x": round(float(x_rand[i]), precision), "f_x": round(float(f_eval[i]), precision)} for i in range(limite)]
|
|
184
|
+
|
|
185
|
+
notas = []
|
|
186
|
+
if n_no_finitos > 0:
|
|
187
|
+
notas.append(
|
|
188
|
+
f"{n_no_finitos} muestra(s) no finita(s) se trataron como 0; el resultado puede estar sesgado."
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
"metodo": "Valor Promedio 1D", "integral": round(float(integral), precision),
|
|
193
|
+
"media_muestral": round(float(promedio), precision),
|
|
194
|
+
"promedio_fx": round(float(promedio), precision), "escala": round(float(escala), precision),
|
|
195
|
+
"desv_estandar": round(float(desv_std), precision), "error_std": round(float(error_est), precision),
|
|
196
|
+
"z_score": round(float(z_score), 4), "ic_inf": round(float(ic_inf), precision),
|
|
197
|
+
"ic_sup": round(float(ic_sup), precision), "N": N, "historial": historial,
|
|
198
|
+
"notas": notas
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def valor_promedio_2d(f, lim_x: Tuple[float, float], lim_y: Tuple[float, float], N: int, seed: int = None, precision: int = 8, nivel_confianza: float = 0.95) -> Dict:
|
|
203
|
+
MonteCarloService.aplicar_semilla(seed)
|
|
204
|
+
ax, bx = lim_x; ay, by = lim_y
|
|
205
|
+
|
|
206
|
+
x_rand = np.random.uniform(ax, bx, N)
|
|
207
|
+
y_rand = np.random.uniform(ay, by, N)
|
|
208
|
+
|
|
209
|
+
with warnings.catch_warnings():
|
|
210
|
+
warnings.simplefilter("ignore")
|
|
211
|
+
f_raw = f(x_rand, y_rand)
|
|
212
|
+
n_no_finitos = int(np.sum(~np.isfinite(f_raw)))
|
|
213
|
+
f_eval = np.nan_to_num(f_raw, nan=0.0, posinf=0.0, neginf=0.0)
|
|
214
|
+
|
|
215
|
+
escala = (bx - ax) * (by - ay)
|
|
216
|
+
promedio = np.mean(f_eval)
|
|
217
|
+
integral = escala * promedio
|
|
218
|
+
|
|
219
|
+
desv_std = np.std(f_eval, ddof=1) if N > 1 else 0
|
|
220
|
+
error_est = desv_std / np.sqrt(N) if N > 0 else 0
|
|
221
|
+
|
|
222
|
+
z_score = MonteCarloService.calcular_z_score(nivel_confianza)
|
|
223
|
+
ic_inf = integral - z_score * error_est * escala
|
|
224
|
+
ic_sup = integral + z_score * error_est * escala
|
|
225
|
+
|
|
226
|
+
limite = min(N, 2000)
|
|
227
|
+
historial = [{"i": i+1, "x": round(float(x_rand[i]), precision), "y": round(float(y_rand[i]), precision), "f_xy": round(float(f_eval[i]), precision)} for i in range(limite)]
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
"metodo": "Valor Promedio 2D", "integral": round(float(integral), precision),
|
|
231
|
+
"media_muestral": round(float(promedio), precision),
|
|
232
|
+
"promedio_fxy": round(float(promedio), precision), "escala": round(float(escala), precision),
|
|
233
|
+
"desv_estandar": round(float(desv_std), precision), "error_std": round(float(error_est), precision),
|
|
234
|
+
"z_score": round(float(z_score), 4), "ic_inf": round(float(ic_inf), precision),
|
|
235
|
+
"ic_sup": round(float(ic_sup), precision), "N": N, "historial": historial,
|
|
236
|
+
"notas": ([f"{n_no_finitos} muestra(s) no finita(s) se trataron como 0; el resultado puede estar sesgado."] if n_no_finitos > 0 else [])
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def valor_promedio_3d(f, lim_x: Tuple[float, float], lim_y: Tuple[float, float], lim_z: Tuple[float, float], N: int, seed: int = None, precision: int = 8, nivel_confianza: float = 0.95) -> Dict:
|
|
241
|
+
MonteCarloService.aplicar_semilla(seed)
|
|
242
|
+
ax, bx = lim_x; ay, by = lim_y; az, bz = lim_z
|
|
243
|
+
|
|
244
|
+
x_rand = np.random.uniform(ax, bx, N)
|
|
245
|
+
y_rand = np.random.uniform(ay, by, N)
|
|
246
|
+
z_rand = np.random.uniform(az, bz, N)
|
|
247
|
+
|
|
248
|
+
with warnings.catch_warnings():
|
|
249
|
+
warnings.simplefilter("ignore")
|
|
250
|
+
f_raw = f(x_rand, y_rand, z_rand)
|
|
251
|
+
n_no_finitos = int(np.sum(~np.isfinite(f_raw)))
|
|
252
|
+
f_eval = np.nan_to_num(f_raw, nan=0.0, posinf=0.0, neginf=0.0)
|
|
253
|
+
|
|
254
|
+
escala = (bx - ax) * (by - ay) * (bz - az)
|
|
255
|
+
promedio = np.mean(f_eval)
|
|
256
|
+
integral = escala * promedio
|
|
257
|
+
|
|
258
|
+
desv_std = np.std(f_eval, ddof=1) if N > 1 else 0
|
|
259
|
+
error_est = desv_std / np.sqrt(N) if N > 0 else 0
|
|
260
|
+
z_score = MonteCarloService.calcular_z_score(nivel_confianza)
|
|
261
|
+
ic_inf = integral - z_score * error_est * escala
|
|
262
|
+
ic_sup = integral + z_score * error_est * escala
|
|
263
|
+
|
|
264
|
+
limite = min(N, 2000)
|
|
265
|
+
historial = [{"i": i+1, "x": round(float(x_rand[i]), precision), "y": round(float(y_rand[i]), precision), "z": round(float(z_rand[i]), precision), "f_xyz": round(float(f_eval[i]), precision)} for i in range(limite)]
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
"metodo": "Valor Promedio 3D", "integral": round(float(integral), precision),
|
|
269
|
+
"media_muestral": round(float(promedio), precision),
|
|
270
|
+
"promedio_fxyz": round(float(promedio), precision), "escala": round(float(escala), precision),
|
|
271
|
+
"desv_estandar": round(float(desv_std), precision), "error_std": round(float(error_est), precision),
|
|
272
|
+
"z_score": round(float(z_score), 4), "ic_inf": round(float(ic_inf), precision),
|
|
273
|
+
"ic_sup": round(float(ic_sup), precision), "N": N, "historial": historial,
|
|
274
|
+
"notas": ([f"{n_no_finitos} muestra(s) no finita(s) se trataron como 0; el resultado puede estar sesgado."] if n_no_finitos > 0 else [])
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def analisis_estadistico_1d(f, a: float, b: float, N: int, M: int, nivel_confianza: float = 0.95, seed: int = None, precision: int = 8) -> Dict:
|
|
279
|
+
MonteCarloService.aplicar_semilla(seed)
|
|
280
|
+
|
|
281
|
+
x_rand = np.random.uniform(a, b, (M, N))
|
|
282
|
+
with warnings.catch_warnings():
|
|
283
|
+
warnings.simplefilter("ignore")
|
|
284
|
+
f_eval = np.nan_to_num(f(x_rand), nan=0.0)
|
|
285
|
+
|
|
286
|
+
integrales = (b - a) * np.mean(f_eval, axis=1)
|
|
287
|
+
|
|
288
|
+
media = np.mean(integrales)
|
|
289
|
+
varianza = np.var(integrales, ddof=1)
|
|
290
|
+
desviacion = np.std(integrales, ddof=1)
|
|
291
|
+
|
|
292
|
+
z_score = MonteCarloService.calcular_z_score(nivel_confianza)
|
|
293
|
+
error_estandar = desviacion / np.sqrt(M)
|
|
294
|
+
ic_inf = media - z_score * error_estandar
|
|
295
|
+
ic_sup = media + z_score * error_estandar
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
"metodo": "Estadístico 1D",
|
|
299
|
+
"media": round(float(media), precision),
|
|
300
|
+
"integral": round(float(media), precision), # Alias para frontend
|
|
301
|
+
"varianza": round(float(varianza), precision),
|
|
302
|
+
"desv_estandar": round(float(desviacion), precision),
|
|
303
|
+
"error_std": round(float(error_estandar), precision),
|
|
304
|
+
"z_score": round(float(z_score), 4),
|
|
305
|
+
"ic_inf": round(float(ic_inf), precision),
|
|
306
|
+
"ic_sup": round(float(ic_sup), precision),
|
|
307
|
+
"ancho_ic": round(float(ic_sup - ic_inf), precision),
|
|
308
|
+
"nivel_confianza": nivel_confianza,
|
|
309
|
+
"M": M,
|
|
310
|
+
"N": N,
|
|
311
|
+
"distribucion": [round(float(val), precision) for val in integrales]
|
|
312
|
+
}
|
mathmethods/core/ode.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import sympy as sp
|
|
3
|
+
import math
|
|
4
|
+
from .utils import safe_sympify
|
|
5
|
+
|
|
6
|
+
class ODEService:
|
|
7
|
+
|
|
8
|
+
@staticmethod
|
|
9
|
+
def _round_numeric(value, precision):
|
|
10
|
+
if isinstance(value, dict):
|
|
11
|
+
return {k: ODEService._round_numeric(v, precision) for k, v in value.items()}
|
|
12
|
+
if isinstance(value, list):
|
|
13
|
+
return [ODEService._round_numeric(v, precision) for v in value]
|
|
14
|
+
if isinstance(value, (float, np.floating)):
|
|
15
|
+
return round(float(value), precision)
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def resolver_edo_exacta(texto_dy_dx, x0_val, y0_val):
|
|
20
|
+
"""Usa SymPy para resolver la EDO analíticamente y retorna un evaluador seguro."""
|
|
21
|
+
x = sp.Symbol('x')
|
|
22
|
+
y_func = sp.Function('y')(x)
|
|
23
|
+
|
|
24
|
+
diccionario_local = {'y': y_func, 'e': sp.E, 'pi': sp.pi}
|
|
25
|
+
f_expr = safe_sympify(texto_dy_dx, local_dict=diccionario_local, allowed_symbols=['x', 'y'])
|
|
26
|
+
|
|
27
|
+
edo = sp.Eq(y_func.diff(x), f_expr)
|
|
28
|
+
condiciones_iniciales = {y_func.subs(x, x0_val): y0_val}
|
|
29
|
+
|
|
30
|
+
solucion_general = sp.dsolve(edo, ics=condiciones_iniciales)
|
|
31
|
+
expr_exacta = solucion_general.rhs
|
|
32
|
+
|
|
33
|
+
f_exacta = sp.lambdify(x, expr_exacta, 'numpy')
|
|
34
|
+
|
|
35
|
+
def f_exacta_segura(x_val):
|
|
36
|
+
return float(f_exacta(x_val))
|
|
37
|
+
|
|
38
|
+
return f_exacta_segura, str(expr_exacta)
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def compilar_f(ecuacion_str):
|
|
42
|
+
"""Compila f(x,y) a función lambda de NumPy."""
|
|
43
|
+
x_sym, y_sym = sp.symbols('x y')
|
|
44
|
+
expr = safe_sympify(ecuacion_str, local_dict={'e': sp.E, 'pi': sp.pi}, allowed_symbols=['x', 'y'])
|
|
45
|
+
return sp.lambdify((x_sym, y_sym), expr, 'numpy')
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def ejecutar_metodo(metodo, ecuacion_str, x0, y0, xf, h, precision=8, tol=None):
|
|
49
|
+
"""
|
|
50
|
+
Orquestador principal. Ejecuta el método solicitado,
|
|
51
|
+
calcula la solución exacta, los errores y empaqueta todo para la API.
|
|
52
|
+
"""
|
|
53
|
+
f = ODEService.compilar_f(ecuacion_str)
|
|
54
|
+
|
|
55
|
+
f_exacta = None
|
|
56
|
+
expr_exacta_str = None
|
|
57
|
+
try:
|
|
58
|
+
f_exacta, expr_exacta_str = ODEService.resolver_edo_exacta(ecuacion_str, x0, y0)
|
|
59
|
+
except Exception:
|
|
60
|
+
f_exacta = None
|
|
61
|
+
expr_exacta_str = None
|
|
62
|
+
|
|
63
|
+
if h <= 0:
|
|
64
|
+
raise ValueError("h debe ser positivo.")
|
|
65
|
+
|
|
66
|
+
n_steps = int(round((xf - x0) / h))
|
|
67
|
+
x_values = np.linspace(x0, xf, n_steps + 1)
|
|
68
|
+
x_values = np.round(x_values, decimals=10)
|
|
69
|
+
n_steps = len(x_values)
|
|
70
|
+
|
|
71
|
+
if f_exacta is not None:
|
|
72
|
+
y_exacta = np.array([f_exacta(xi) for xi in x_values])
|
|
73
|
+
else:
|
|
74
|
+
y_exacta = np.full(n_steps, np.nan)
|
|
75
|
+
|
|
76
|
+
resultado = {
|
|
77
|
+
"ecuacion": ecuacion_str,
|
|
78
|
+
"solucion_exacta_str": expr_exacta_str,
|
|
79
|
+
"h": h,
|
|
80
|
+
"tol": tol,
|
|
81
|
+
"tabla": [],
|
|
82
|
+
"x_plot": x_values.tolist(),
|
|
83
|
+
"y_exacta_plot": y_exacta.tolist() if f_exacta is not None else []
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Arrays para los cálculos
|
|
87
|
+
y_n = np.zeros(n_steps)
|
|
88
|
+
y_n[0] = y0
|
|
89
|
+
|
|
90
|
+
if metodo == "euler":
|
|
91
|
+
y_n1 = np.zeros(n_steps)
|
|
92
|
+
for i in range(n_steps):
|
|
93
|
+
if i > 0: y_n[i] = y_n1[i-1]
|
|
94
|
+
y_n1[i] = y_n[i] + h * f(x_values[i], y_n[i])
|
|
95
|
+
|
|
96
|
+
err = abs(y_exacta[i] - y_n[i]) if f_exacta is not None else None
|
|
97
|
+
resultado["tabla"].append({
|
|
98
|
+
"i": i, "xn": x_values[i], "yn": y_n[i],
|
|
99
|
+
"yn1": y_n1[i], # <-- ACÁ ESTÁ EL ARREGLO: Ya no devolvemos None al final
|
|
100
|
+
"yr": y_exacta[i] if f_exacta is not None else None, "error": err if i > 0 else 0
|
|
101
|
+
})
|
|
102
|
+
resultado["y_plot"] = y_n.tolist()
|
|
103
|
+
|
|
104
|
+
elif metodo == "heun":
|
|
105
|
+
y_pred = np.zeros(n_steps)
|
|
106
|
+
y_corr = np.zeros(n_steps)
|
|
107
|
+
for i in range(n_steps):
|
|
108
|
+
if i > 0: y_n[i] = y_corr[i-1]
|
|
109
|
+
|
|
110
|
+
if i < n_steps - 1:
|
|
111
|
+
f_xy = f(x_values[i], y_n[i])
|
|
112
|
+
y_pred[i] = y_n[i] + h * f_xy
|
|
113
|
+
y_corr[i] = y_n[i] + (h / 2.0) * (f_xy + f(x_values[i+1], y_pred[i]))
|
|
114
|
+
|
|
115
|
+
err = abs(y_exacta[i] - y_n[i]) if f_exacta is not None else None
|
|
116
|
+
resultado["tabla"].append({
|
|
117
|
+
"i": i, "xn": x_values[i], "yn": y_n[i],
|
|
118
|
+
"y_pred": y_pred[i] if i < n_steps - 1 else None,
|
|
119
|
+
"y_corr": y_corr[i] if i < n_steps - 1 else None,
|
|
120
|
+
"yr": y_exacta[i] if f_exacta is not None else None, "error": err if i > 0 else 0
|
|
121
|
+
})
|
|
122
|
+
resultado["y_plot"] = y_n.tolist()
|
|
123
|
+
|
|
124
|
+
elif metodo == "rk4":
|
|
125
|
+
k1 = np.zeros(n_steps); k2 = np.zeros(n_steps)
|
|
126
|
+
k3 = np.zeros(n_steps); k4 = np.zeros(n_steps)
|
|
127
|
+
y_n1 = np.zeros(n_steps)
|
|
128
|
+
|
|
129
|
+
for i in range(n_steps):
|
|
130
|
+
if i > 0: y_n[i] = y_n1[i-1]
|
|
131
|
+
|
|
132
|
+
if i < n_steps - 1:
|
|
133
|
+
xn = x_values[i]; yn = y_n[i]
|
|
134
|
+
k1[i] = f(xn, yn)
|
|
135
|
+
k2[i] = f(xn + h/2.0, yn + (h/2.0) * k1[i])
|
|
136
|
+
k3[i] = f(xn + h/2.0, yn + (h/2.0) * k2[i])
|
|
137
|
+
k4[i] = f(xn + h, yn + h * k3[i])
|
|
138
|
+
y_n1[i] = yn + (h / 6.0) * (k1[i] + 2*k2[i] + 2*k3[i] + k4[i])
|
|
139
|
+
|
|
140
|
+
err = abs(y_exacta[i] - y_n[i]) if f_exacta is not None else None
|
|
141
|
+
resultado["tabla"].append({
|
|
142
|
+
"i": i, "xn": x_values[i], "yn": y_n[i],
|
|
143
|
+
"k1": k1[i] if i < n_steps - 1 else None, "k2": k2[i] if i < n_steps - 1 else None,
|
|
144
|
+
"k3": k3[i] if i < n_steps - 1 else None, "k4": k4[i] if i < n_steps - 1 else None,
|
|
145
|
+
"yn1": y_n1[i] if i < n_steps - 1 else None,
|
|
146
|
+
"yr": y_exacta[i] if f_exacta is not None else None, "error": err if i > 0 else 0
|
|
147
|
+
})
|
|
148
|
+
resultado["y_plot"] = y_n.tolist()
|
|
149
|
+
|
|
150
|
+
final_error = resultado["tabla"][-1]["error"] if resultado["tabla"] else None
|
|
151
|
+
resultado["acepta_tolerancia"] = bool(tol is not None and final_error is not None and final_error <= tol)
|
|
152
|
+
|
|
153
|
+
precision = max(1, min(int(precision), 15))
|
|
154
|
+
return ODEService._round_numeric(resultado, precision)
|