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,396 @@
1
+ """
2
+ Integracion Numerica - Formulas de Newton-Cotes
3
+ - Trapecio (simple y compuesto)
4
+ - Rectangulo (punto medio compuesto)
5
+ - Simpson 1/3 (simple y compuesto)
6
+ - Simpson 3/8 (simple y compuesto)
7
+ """
8
+ import numpy as np
9
+ import sympy as sp
10
+ from typing import Dict, Callable
11
+ import warnings
12
+ from .utils import safe_sympify
13
+
14
+ class IntegrationService:
15
+
16
+ @staticmethod
17
+ def compilar_funcion(texto_funcion: str) -> Callable:
18
+ try:
19
+ x_sym = sp.Symbol('x')
20
+ diccionario_local = {'e': sp.E, 'pi': sp.pi}
21
+ expr = safe_sympify(texto_funcion, local_dict=diccionario_local, allowed_symbols=['x'])
22
+ func_base = sp.lambdify(x_sym, expr, 'numpy')
23
+
24
+ def func_inteligente(x_val):
25
+ with warnings.catch_warnings():
26
+ warnings.simplefilter("ignore")
27
+ y_val = func_base(x_val)
28
+
29
+ # Intentar convertir a float, si genera complejos pasa a NaN
30
+ try:
31
+ y_array = np.asarray(y_val, dtype=float)
32
+ except TypeError:
33
+ y_array = np.full_like(x_val, np.nan, dtype=float)
34
+
35
+ if np.isscalar(x_val) and y_array.ndim == 0:
36
+ if np.isnan(y_array) or np.isinf(y_array):
37
+ lim = sp.limit(expr, x_sym, x_val)
38
+ # Solo aceptamos limites reales finitos
39
+ if lim.is_finite and lim.is_real:
40
+ val = float(lim)
41
+ if getattr(func_inteligente, 'grabar_rescates', False):
42
+ func_inteligente.limites_calculados.append((float(x_val), val))
43
+ return val
44
+ else:
45
+ raise ValueError(f"IMPROPIA: Indeterminación o resultado imaginario en x = {float(x_val):.4f}")
46
+ return float(y_array)
47
+
48
+ else:
49
+ if y_array.ndim == 0:
50
+ y_array = np.full_like(x_val, float(y_array))
51
+
52
+ if np.any(np.isnan(y_array)) or np.any(np.isinf(y_array)):
53
+ intentos = 0
54
+ for i in range(len(y_array)):
55
+ if np.isnan(y_array[i]) or np.isinf(y_array[i]):
56
+ intentos += 1
57
+ if intentos > 15:
58
+ raise ValueError("IMPROPIA: Función no definida o compleja en gran parte del intervalo.")
59
+
60
+ lim = sp.limit(expr, x_sym, x_val[i])
61
+ if lim.is_finite and lim.is_real:
62
+ val = float(lim)
63
+ y_array[i] = val
64
+ if getattr(func_inteligente, 'grabar_rescates', False):
65
+ func_inteligente.limites_calculados.append((float(x_val[i]), val))
66
+ else:
67
+ raise ValueError(f"IMPROPIA: Asíntota o raíz imaginaria en x = {x_val[i]:.4f}")
68
+ return y_array
69
+
70
+ func_inteligente.limites_calculados = []
71
+ func_inteligente.grabar_rescates = False
72
+ func_inteligente.expr_sympy = expr
73
+ func_inteligente.x_symbol = x_sym
74
+ return func_inteligente
75
+ except Exception as e:
76
+ raise ValueError(f"Error compilando funcion: {str(e)}")
77
+
78
+ @staticmethod
79
+ def _orden_derivada_et(metodo: str) -> int:
80
+ if metodo in ["rectangulo", "trapecio"]:
81
+ return 2
82
+ if metodo in ["simpson13", "simpson38"]:
83
+ return 4
84
+ raise ValueError("Método no soportado para error de truncamiento")
85
+
86
+ @staticmethod
87
+ def _coeficiente_et(a: float, b: float, n: int, metodo: str) -> float:
88
+ if metodo == "rectangulo":
89
+ return ((b - a) ** 3) / (24 * (n ** 2))
90
+ if metodo == "trapecio":
91
+ return -((b - a) ** 3) / (12 * (n ** 2))
92
+ if metodo == "simpson13":
93
+ return -((b - a) ** 5) / (180 * (n ** 4))
94
+ if metodo == "simpson38":
95
+ return -((b - a) ** 5) / (80 * (n ** 4))
96
+ raise ValueError("Método no soportado para error de truncamiento")
97
+
98
+ @staticmethod
99
+ def _derivada_n_valor(expr: sp.Expr, x_sym: sp.Symbol, n: int, punto: float) -> float:
100
+ return float(sp.diff(expr, x_sym, n).subs(x_sym, punto))
101
+
102
+ @staticmethod
103
+ def _calcular_error_truncamiento_epsilon(
104
+ f: Callable,
105
+ a: float,
106
+ b: float,
107
+ n: int,
108
+ metodo: str,
109
+ epsilon: float,
110
+ precision: int,
111
+ ):
112
+ if epsilon is None:
113
+ return "No definido (falta epsilon)"
114
+
115
+ if epsilon < min(a, b) or epsilon > max(a, b):
116
+ return "No definido (epsilon fuera del intervalo [a,b])"
117
+
118
+ expr = getattr(f, "expr_sympy", None)
119
+ x_sym = getattr(f, "x_symbol", None)
120
+ if expr is None or x_sym is None:
121
+ return "No definido (función simbólica no disponible)"
122
+
123
+ try:
124
+ orden = IntegrationService._orden_derivada_et(metodo)
125
+ coeficiente = IntegrationService._coeficiente_et(a, b, n, metodo)
126
+ derivada = IntegrationService._derivada_n_valor(expr, x_sym, orden, epsilon)
127
+ et = coeficiente * derivada
128
+ return {
129
+ "epsilon": round(float(epsilon), precision),
130
+ "orden_derivada": orden,
131
+ "derivada_en_epsilon": round(float(derivada), precision),
132
+ "error_truncamiento": round(float(et), precision),
133
+ }
134
+ except Exception:
135
+ return "Indefinido matemáticamente en epsilon"
136
+
137
+ @staticmethod
138
+ def _verificar_asintota(y_array: np.ndarray):
139
+ if np.any(np.isnan(y_array)) or np.any(np.isinf(y_array)):
140
+ raise ValueError("IMPROPIA: Indeterminación o valores imaginarios detectados.")
141
+ if np.any(np.abs(y_array) > 1e10):
142
+ raise ValueError("IMPROPIA: Asíntota vertical detectada. El valor se dispara al infinito.")
143
+
144
+ @staticmethod
145
+ def _validar_integral(f: Callable, a: float, b: float):
146
+ # Escaneo estrictamente interior para evitar colapsos con bordes complejos
147
+ margen = (b - a) * 0.001
148
+ x_test = np.linspace(a + margen, b - margen, 1000)
149
+ with warnings.catch_warnings():
150
+ warnings.simplefilter("ignore")
151
+ y_test = f(x_test)
152
+ IntegrationService._verificar_asintota(y_test)
153
+
154
+ @staticmethod
155
+ def _segunda_derivada_numerica(f: Callable, x: np.ndarray, dx: float = 1e-4) -> np.ndarray:
156
+ return (f(x + dx) - 2 * f(x) + f(x - dx)) / (dx**2)
157
+
158
+ @staticmethod
159
+ def _cuarta_derivada_numerica(f: Callable, x: np.ndarray, dx: float = 1e-3) -> np.ndarray:
160
+ return (f(x + 2*dx) - 4*f(x + dx) + 6*f(x) - 4*f(x - dx) + f(x - 2*dx)) / (dx**4)
161
+
162
+ @staticmethod
163
+ def _calcular_cota_error(f: Callable, a: float, b: float, n: int, metodo: str, precision: int) -> str:
164
+ """Calcula el error de forma super blindada evitando salir de los bordes."""
165
+ try:
166
+ # Apagamos los avisos de rescate para no ensuciar la libreta
167
+ estado_previo = getattr(f, 'grabar_rescates', False)
168
+ f.grabar_rescates = False
169
+
170
+ if metodo in ["rectangulo", "trapecio"]:
171
+ dx = 1e-4
172
+ margen = 2 * dx
173
+ if b - a <= 2 * margen: return "Intervalo muy chico"
174
+ x_fino = np.linspace(a + margen, b - margen, 1000)
175
+ deriv = IntegrationService._segunda_derivada_numerica(f, x_fino, dx)
176
+ deriv = deriv[~np.isnan(deriv)]
177
+ if len(deriv) == 0: return "Indefinido"
178
+ max_deriv = np.max(np.abs(deriv))
179
+
180
+ if max_deriv > 1e7: return "Diverge (Derivada no acotada)"
181
+ val = ((b - a)**3 / (24 * n**2)) if metodo == "rectangulo" else ((b - a)**3 / (12 * n**2))
182
+ ans = round(float(val * max_deriv), precision)
183
+
184
+ elif metodo in ["simpson13", "simpson38"]:
185
+ dx = 1e-3
186
+ margen = 3 * dx
187
+ if b - a <= 2 * margen: return "Intervalo muy chico"
188
+ x_fino = np.linspace(a + margen, b - margen, 1000)
189
+ deriv = IntegrationService._cuarta_derivada_numerica(f, x_fino, dx)
190
+ deriv = deriv[~np.isnan(deriv)]
191
+ if len(deriv) == 0: return "Indefinido"
192
+ max_deriv = np.max(np.abs(deriv))
193
+
194
+ if max_deriv > 1e7: return "Diverge (Derivada no acotada)"
195
+ val = ((b - a)**5 / (180 * n**4)) if metodo == "simpson13" else ((b - a)**5 / (80 * n**4))
196
+ ans = round(float(val * max_deriv), precision)
197
+
198
+ f.grabar_rescates = estado_previo
199
+ return str(ans)
200
+ except Exception:
201
+ return "Indefinido matemáticamente"
202
+
203
+ @staticmethod
204
+ def rectangulo_compuesto(f: Callable, a: float, b: float, n: int, precision: int = 8, epsilon: float = None) -> Dict:
205
+ IntegrationService._validar_integral(f, a, b)
206
+ h = (b - a) / n
207
+ x = np.linspace(a, b, n + 1)
208
+ x_medio = np.linspace(a + h/2, b - h/2, n)
209
+
210
+ f.limites_calculados = []
211
+ f.grabar_rescates = True
212
+ with warnings.catch_warnings():
213
+ warnings.simplefilter("ignore")
214
+ y_medio = f(x_medio)
215
+ f.grabar_rescates = False
216
+
217
+ IntegrationService._verificar_asintota(y_medio)
218
+ integral = h * np.sum(y_medio)
219
+ cota_error = IntegrationService._calcular_cota_error(f, a, b, n, "rectangulo", precision)
220
+ et_puntual = IntegrationService._calcular_error_truncamiento_epsilon(f, a, b, n, "rectangulo", epsilon, precision)
221
+
222
+ tabla = [{"i": 0, "x_n": round(float(x[0]), precision), "x_medio": None, "f_x_medio": None}]
223
+ for i in range(n):
224
+ tabla.append({"i": i + 1, "x_n": round(float(x[i+1]), precision), "x_medio": round(float(x_medio[i]), precision), "f_x_medio": round(float(y_medio[i]), precision)})
225
+
226
+ intermedios = " + ".join([f"{yi:.{precision}f}" for yi in y_medio])
227
+ desarrollo = f"I ≈ {h:.4f} [ {intermedios} ]"
228
+
229
+ rescates_crudos = getattr(f, 'limites_calculados', [])
230
+ notas_unicas = list(dict.fromkeys([f"Indeterminación en x = {pt:.4f}. Se aplicó Límite: f(x) = {val:.4f}" for pt, val in rescates_crudos]))
231
+
232
+ return {
233
+ "metodo": "Rectangulo Compuesto", "a": a, "b": b, "n": n, "h": round(h, precision),
234
+ "integral": round(float(integral), precision), "cota_error": cota_error,
235
+ "error_truncamiento": et_puntual["error_truncamiento"] if isinstance(et_puntual, dict) else et_puntual,
236
+ "detalle_error_truncamiento": et_puntual if isinstance(et_puntual, dict) else None,
237
+ "tabla": tabla, "desarrollo": desarrollo, "notas": notas_unicas
238
+ }
239
+
240
+ @staticmethod
241
+ def trapecio_compuesto(f: Callable, a: float, b: float, n: int, precision: int = 8, epsilon: float = None) -> Dict:
242
+ IntegrationService._validar_integral(f, a, b)
243
+ h = (b - a) / n
244
+ x = np.linspace(a, b, n + 1)
245
+
246
+ f.limites_calculados = []
247
+ f.grabar_rescates = True
248
+ with warnings.catch_warnings():
249
+ warnings.simplefilter("ignore")
250
+ y = f(x)
251
+ f.grabar_rescates = False
252
+
253
+ IntegrationService._verificar_asintota(y)
254
+ S = y[0] + y[-1] + 2 * np.sum(y[1:-1])
255
+ integral = (h / 2) * S
256
+ cota_error = IntegrationService._calcular_cota_error(f, a, b, n, "trapecio", precision)
257
+ et_puntual = IntegrationService._calcular_error_truncamiento_epsilon(f, a, b, n, "trapecio", epsilon, precision)
258
+
259
+ tabla = [{"i": i, "x_n": round(float(x[i]), precision), "f_x_n": round(float(y[i]), precision)} for i in range(len(x))]
260
+ desarrollo = f"I ≈ {h:.4f}/2 [ {y[0]:.{precision}f} + {y[-1]:.{precision}f} ]" if n == 1 else f"I ≈ {h:.4f}/2 [ {y[0]:.{precision}f} + 2({' + '.join([f'{yi:.{precision}f}' for yi in y[1:-1]])}) + {y[-1]:.{precision}f} ]"
261
+
262
+ rescates_crudos = getattr(f, 'limites_calculados', [])
263
+ notas_unicas = list(dict.fromkeys([f"Indeterminación en x = {pt:.4f}. Se aplicó Límite: f(x) = {val:.4f}" for pt, val in rescates_crudos]))
264
+
265
+ return {
266
+ "metodo": "Trapecio Compuesto" if n > 1 else "Trapecio Simple", "a": a, "b": b, "n": n, "h": round(h, precision),
267
+ "integral": round(float(integral), precision), "cota_error": cota_error,
268
+ "error_truncamiento": et_puntual["error_truncamiento"] if isinstance(et_puntual, dict) else et_puntual,
269
+ "detalle_error_truncamiento": et_puntual if isinstance(et_puntual, dict) else None,
270
+ "tabla": tabla, "desarrollo": desarrollo, "notas": notas_unicas
271
+ }
272
+
273
+ @staticmethod
274
+ def simpson_13_compuesto(f: Callable, a: float, b: float, n: int, precision: int = 8, epsilon: float = None) -> Dict:
275
+ IntegrationService._validar_integral(f, a, b)
276
+ if n % 2 != 0: raise ValueError("n debe ser PAR para Simpson 1/3")
277
+
278
+ h = (b - a) / n
279
+ x = np.linspace(a, b, n + 1)
280
+
281
+ f.limites_calculados = []
282
+ f.grabar_rescates = True
283
+ with warnings.catch_warnings():
284
+ warnings.simplefilter("ignore")
285
+ y = f(x)
286
+ f.grabar_rescates = False
287
+
288
+ IntegrationService._verificar_asintota(y)
289
+ impares = y[1:n:2]
290
+ pares = y[2:n-1:2]
291
+
292
+ S = y[0] + y[-1] + 4 * np.sum(impares) + 2 * np.sum(pares)
293
+ integral = (h / 3) * S
294
+ cota_error = IntegrationService._calcular_cota_error(f, a, b, n, "simpson13", precision)
295
+ et_puntual = IntegrationService._calcular_error_truncamiento_epsilon(f, a, b, n, "simpson13", epsilon, precision)
296
+
297
+ tabla = [{"i": i, "x_n": round(float(x[i]), precision), "f_x_n": round(float(y[i]), precision)} for i in range(len(x))]
298
+ desarrollo = f"I ≈ {h:.4f}/3 [ {y[0]:.{precision}f} + 4({y[1]:.{precision}f}) + {y[2]:.{precision}f} ]" if n == 2 else f"I ≈ {h:.4f}/3 [ {y[0]:.{precision}f} + 4({' + '.join([f'{yi:.{precision}f}' for yi in impares])}) + 2({' + '.join([f'{yi:.{precision}f}' for yi in pares])}) + {y[-1]:.{precision}f} ]"
299
+
300
+ rescates_crudos = getattr(f, 'limites_calculados', [])
301
+ notas_unicas = list(dict.fromkeys([f"Indeterminación en x = {pt:.4f}. Se aplicó Límite: f(x) = {val:.4f}" for pt, val in rescates_crudos]))
302
+
303
+ return {
304
+ "metodo": "Simpson 1/3 Compuesto" if n > 2 else "Simpson 1/3 Simple", "a": a, "b": b, "n": n, "h": round(h, precision),
305
+ "integral": round(float(integral), precision), "cota_error": cota_error,
306
+ "error_truncamiento": et_puntual["error_truncamiento"] if isinstance(et_puntual, dict) else et_puntual,
307
+ "detalle_error_truncamiento": et_puntual if isinstance(et_puntual, dict) else None,
308
+ "tabla": tabla, "desarrollo": desarrollo, "notas": notas_unicas
309
+ }
310
+
311
+ @staticmethod
312
+ def simpson_38_compuesto(f: Callable, a: float, b: float, n: int, precision: int = 8, epsilon: float = None) -> Dict:
313
+ IntegrationService._validar_integral(f, a, b)
314
+ if n % 3 != 0: raise ValueError("n debe ser multiplo de 3 para Simpson 3/8")
315
+
316
+ h = (b - a) / n
317
+ x = np.linspace(a, b, n + 1)
318
+
319
+ f.limites_calculados = []
320
+ f.grabar_rescates = True
321
+ with warnings.catch_warnings():
322
+ warnings.simplefilter("ignore")
323
+ y = f(x)
324
+ f.grabar_rescates = False
325
+
326
+ IntegrationService._verificar_asintota(y)
327
+ grupo_1, grupo_2, grupo_3 = y[1:n:3], y[2:n:3], y[3:n-1:3]
328
+
329
+ S = y[0] + y[-1] + 3 * (np.sum(grupo_1) + np.sum(grupo_2)) + 2 * np.sum(grupo_3)
330
+ integral = (3 * h / 8) * S
331
+ cota_error = IntegrationService._calcular_cota_error(f, a, b, n, "simpson38", precision)
332
+ et_puntual = IntegrationService._calcular_error_truncamiento_epsilon(f, a, b, n, "simpson38", epsilon, precision)
333
+
334
+ tabla = [{"i": i, "x_n": round(float(x[i]), precision), "f_x_n": round(float(y[i]), precision)} for i in range(len(x))]
335
+ if n == 3: desarrollo = f"I ≈ 3({h:.4f})/8 [ {y[0]:.{precision}f} + 3({y[1]:.{precision}f} + {y[2]:.{precision}f}) + {y[3]:.{precision}f} ]"
336
+ else: desarrollo = f"I ≈ 3({h:.4f})/8 [ {y[0]:.{precision}f} + 3({' + '.join([f'{yi:.{precision}f}' for yi in np.concatenate((grupo_1, grupo_2))])}){(' + 2(' + ' + '.join([f'{yi:.{precision}f}' for yi in grupo_3]) + ')') if len(grupo_3) > 0 else ''} + {y[-1]:.{precision}f} ]"
337
+
338
+ rescates_crudos = getattr(f, 'limites_calculados', [])
339
+ notas_unicas = list(dict.fromkeys([f"Indeterminación en x = {pt:.4f}. Se aplicó Límite: f(x) = {val:.4f}" for pt, val in rescates_crudos]))
340
+
341
+ return {
342
+ "metodo": "Simpson 3/8 Compuesto" if n > 3 else "Simpson 3/8 Simple", "a": a, "b": b, "n": n, "h": round(h, precision),
343
+ "integral": round(float(integral), precision), "cota_error": cota_error,
344
+ "error_truncamiento": et_puntual["error_truncamiento"] if isinstance(et_puntual, dict) else et_puntual,
345
+ "detalle_error_truncamiento": et_puntual if isinstance(et_puntual, dict) else None,
346
+ "tabla": tabla, "desarrollo": desarrollo, "notas": notas_unicas
347
+ }
348
+ @staticmethod
349
+ def comparar_metodos(func_str: str, a: float, b: float, n: int, precision: int = 8, epsilon: float = None) -> Dict:
350
+ """
351
+ Orquestador que ejecuta los 4 métodos de integración y devuelve un resumen comparativo.
352
+ """
353
+ resultados = {
354
+ "rectangulo": None,
355
+ "trapecio": None,
356
+ "simpson_13": None,
357
+ "simpson_38": None
358
+ }
359
+
360
+ try:
361
+ f = IntegrationService.compilar_funcion(func_str)
362
+ except Exception as e:
363
+ return {"error_global": str(e)}
364
+
365
+ # 1. Rectángulo Medio Compuesto
366
+ try:
367
+ resultados["rectangulo"] = IntegrationService.rectangulo_compuesto(f, a, b, n, precision, epsilon)
368
+ resultados["rectangulo"]["exito"] = True
369
+ except Exception as e:
370
+ resultados["rectangulo"] = {"exito": False, "error_msg": str(e)}
371
+
372
+ # 2. Trapecio Compuesto
373
+ try:
374
+ resultados["trapecio"] = IntegrationService.trapecio_compuesto(f, a, b, n, precision, epsilon)
375
+ resultados["trapecio"]["exito"] = True
376
+ except Exception as e:
377
+ resultados["trapecio"] = {"exito": False, "error_msg": str(e)}
378
+
379
+ # 3. Simpson 1/3 Compuesto
380
+ try:
381
+ resultados["simpson_13"] = IntegrationService.simpson_13_compuesto(f, a, b, n, precision, epsilon)
382
+ resultados["simpson_13"]["exito"] = True
383
+ except Exception as e:
384
+ resultados["simpson_13"] = {"exito": False, "error_msg": str(e)}
385
+
386
+ # 4. Simpson 3/8 Compuesto
387
+ try:
388
+ resultados["simpson_38"] = IntegrationService.simpson_38_compuesto(f, a, b, n, precision, epsilon)
389
+ resultados["simpson_38"]["exito"] = True
390
+ except Exception as e:
391
+ resultados["simpson_38"] = {"exito": False, "error_msg": str(e)}
392
+
393
+ return {
394
+ "parametros": {"f(x)": func_str, "a": a, "b": b, "n": n, "epsilon": epsilon},
395
+ "comparativa": resultados
396
+ }
@@ -0,0 +1,126 @@
1
+ """
2
+ Interpolación Numérica - Polinomio de Lagrange
3
+ """
4
+ import numpy as np
5
+ from typing import Dict, List
6
+ import sympy as sp
7
+ import math
8
+ from .utils import safe_sympify
9
+
10
+ class InterpolationService:
11
+
12
+ @staticmethod
13
+ def compilar_funcion(texto_funcion: str):
14
+ """Convierte string a función simbólica."""
15
+ try:
16
+ x = sp.Symbol('x')
17
+ diccionario_local = {'e': sp.E, 'pi': sp.pi}
18
+ expr = safe_sympify(texto_funcion, local_dict=diccionario_local, allowed_symbols=['x'])
19
+ return sp.lambdify(x, expr, 'numpy'), expr
20
+ except Exception as e:
21
+ raise ValueError(f"Error compilando: {str(e)}")
22
+
23
+ @staticmethod
24
+ def lagrange(puntos_x: List[float], x_eval: float = None,
25
+ func_str: str = None, puntos_y: List[float] = None,
26
+ precision: int = 8) -> Dict:
27
+
28
+ x_sym = sp.Symbol('x')
29
+ n = len(puntos_x)
30
+
31
+ # Obtener los valores y
32
+ if func_str:
33
+ f, expr = InterpolationService.compilar_funcion(func_str)
34
+ puntos_y = [float(f(xi)) for xi in puntos_x]
35
+ elif puntos_y is None:
36
+ raise ValueError("Proveer func_str o puntos_y")
37
+
38
+ # Construir polinomio de Lagrange
39
+ P = 0
40
+ terminos_construccion = []
41
+
42
+ for i in range(n):
43
+ L_i = 1
44
+ for j in range(n):
45
+ if i != j:
46
+ L_i *= (x_sym - puntos_x[j]) / (puntos_x[i] - puntos_x[j])
47
+ term = puntos_y[i] * L_i
48
+ P += term
49
+
50
+ # Guardamos el término para mostrarlo en el frontend
51
+ terminos_construccion.append({
52
+ "i": i,
53
+ "l_i_y_i": str(sp.expand(term))
54
+ })
55
+
56
+ P_expanded = sp.expand(P)
57
+ grado_sym = sp.degree(P_expanded, x_sym)
58
+ # Parseo seguro del grado
59
+ grado = int(grado_sym) if hasattr(grado_sym, 'is_number') and grado_sym.is_number and not getattr(grado_sym, 'is_infinite', False) else 0
60
+
61
+ # Tabla de puntos
62
+ tabla = []
63
+ for xi, yi in zip(puntos_x, puntos_y):
64
+ tabla.append({
65
+ "x": round(xi, precision),
66
+ "y": round(float(yi), precision)
67
+ })
68
+
69
+ resultado = {
70
+ "metodo": "Lagrange",
71
+ "puntos": tabla,
72
+ "polinomio": str(P_expanded),
73
+ "grado": grado,
74
+ "terminos": terminos_construccion
75
+ }
76
+
77
+ # Si hay x_eval, evaluar
78
+ if x_eval is not None:
79
+ P_eval = float(P_expanded.subs(x_sym, x_eval).evalf())
80
+ resultado["x_eval"] = x_eval
81
+ resultado["P_eval"] = round(P_eval, precision)
82
+
83
+ # Error local y cota global si tenemos función
84
+ if func_str:
85
+ f_eval = float(f(x_eval))
86
+ error_local = abs(f_eval - P_eval)
87
+ resultado["f_eval"] = round(f_eval, precision)
88
+ resultado["error_local"] = round(error_local, 8)
89
+
90
+ # --- CÁLCULO DE COTA DE ERROR GLOBAL ---
91
+ derivada_orden = n
92
+ derivada_expr = sp.diff(expr, x_sym, derivada_orden)
93
+
94
+ # Rango para buscar máximos [min_x, max_x]
95
+ intervalo_a = min(min(puntos_x), x_eval)
96
+ intervalo_b = max(max(puntos_x), x_eval)
97
+ x_vals = np.linspace(intervalo_a, intervalo_b, 1000)
98
+
99
+ # Máximo de la derivada
100
+ df_lambdify = sp.lambdify(x_sym, derivada_expr, 'numpy')
101
+ df_eval = df_lambdify(x_vals)
102
+ if np.isscalar(df_eval):
103
+ df_eval = np.full_like(x_vals, df_eval, dtype=float)
104
+ max_derivada = float(np.max(np.abs(df_eval)))
105
+
106
+ # Máximo de g(x) = prod(x - x_i)
107
+ g_vals = np.ones_like(x_vals, dtype=float)
108
+ for xi in puntos_x:
109
+ g_vals *= (x_vals - xi)
110
+ max_g = float(np.max(np.abs(g_vals)))
111
+
112
+ factorial_n = math.factorial(n)
113
+ cota_global = (max_derivada / factorial_n) * max_g
114
+ exito = cota_global >= error_local
115
+
116
+ resultado["analisis_error"] = {
117
+ "derivada_orden": n,
118
+ "derivada_expr": str(derivada_expr).replace('**', '^'),
119
+ "max_derivada": max_derivada,
120
+ "max_g": max_g,
121
+ "factorial": factorial_n,
122
+ "cota_global": cota_global,
123
+ "exito": exito
124
+ }
125
+
126
+ return resultado