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,339 @@
1
+ """
2
+ Métodos para búsqueda de raíces
3
+ - Bisección
4
+ - Punto Fijo
5
+ - Newton-Raphson
6
+ - Aitken (Aceleración)
7
+ """
8
+ import numpy as np
9
+ from typing import Dict, List, Tuple, Callable
10
+ import sympy as sp
11
+ import math
12
+ from .utils import safe_sympify
13
+
14
+ class RootFindingService:
15
+
16
+ @staticmethod
17
+ def compilar_funcion(texto_funcion: str, variables: str = 'x') -> Callable:
18
+ """Convierte string matemático a función callable con NumPy."""
19
+ try:
20
+ x = sp.Symbol(variables.split()[0])
21
+ diccionario_local = {'e': sp.E, 'pi': sp.pi}
22
+ expr = safe_sympify(texto_funcion, local_dict=diccionario_local, allowed_symbols=variables.split())
23
+ return sp.lambdify(x, expr, 'numpy')
24
+ except Exception as e:
25
+ raise ValueError(f"Error compilando función: {str(e)}")
26
+
27
+ @staticmethod
28
+ def biseccion(f: Callable, a: float, b: float,
29
+ tol: float = 1e-6, max_iter: int = 100,
30
+ precision: int = 8) -> Dict:
31
+ """Método de Bisección - acecha cambios de signo."""
32
+ try:
33
+ fa = float(f(a))
34
+ fb = float(f(b))
35
+ except:
36
+ raise ValueError("Error al evaluar f(a) o f(b). Asegúrese de que la función es válida en el intervalo.")
37
+
38
+ if not (math.isfinite(fa) and math.isfinite(fb)):
39
+ raise ValueError("f(a) o f(b) no es finito. La función no está definida o diverge en el intervalo.")
40
+ if fa * fb >= 0:
41
+ raise ValueError("f(a) y f(b) deben tener signos opuestos (f(a) * f(b) < 0).")
42
+
43
+ iteraciones = []
44
+
45
+ for i in range(max_iter):
46
+ c = (a + b) / 2.0
47
+ try:
48
+ fc = float(f(c))
49
+ if math.isnan(fc) or math.isinf(fc):
50
+ raise OverflowError("El valor de f(c) divergió a infinito o no existe.")
51
+ except Exception as e:
52
+ return {
53
+ "metodo": "Bisección", "raiz": None, "iteraciones": iteraciones,
54
+ "convergencia": False, "num_iter": i, "error_msg": f"Divergencia matemática: {str(e)}"
55
+ }
56
+
57
+ # ELIMINAMOS TODOS LOS REDONDEOS ACÁ (Enviamos datos crudos)
58
+ error_calc = abs(b - a) / 2.0
59
+ iteraciones.append({
60
+ "i": i + 1,
61
+ "a": a,
62
+ "b": b,
63
+ "c": c,
64
+ "f_c": fc,
65
+ "error": error_calc
66
+ })
67
+
68
+ if abs(fc) < tol or error_calc < tol:
69
+ return {
70
+ "metodo": "Bisección",
71
+ "raiz": round(c, precision), # La respuesta final sí se puede redondear a gusto
72
+ "iteraciones": iteraciones,
73
+ "convergencia": True,
74
+ "num_iter": i + 1
75
+ }
76
+
77
+ if fa * fc < 0:
78
+ b = c
79
+ fb = fc
80
+ else:
81
+ a = c
82
+ fa = fc
83
+
84
+ return {
85
+ "metodo": "Bisección",
86
+ "raiz": round((a + b) / 2.0, precision),
87
+ "iteraciones": iteraciones,
88
+ "convergencia": False,
89
+ "num_iter": max_iter,
90
+ "error_msg": "Se alcanzó el límite de iteraciones sin converger."
91
+ }
92
+
93
+ @staticmethod
94
+ def punto_fijo(g: Callable, x0: float,
95
+ tol: float = 1e-6, max_iter: int = 100,
96
+ precision: int = 8) -> Dict:
97
+ """Método de Punto Fijo - x = g(x)."""
98
+ x = x0
99
+ iteraciones = []
100
+ verificacion = RootFindingService._verificar_lipschitz(g, x0)
101
+
102
+ for i in range(max_iter):
103
+ try:
104
+ x_new = float(g(x))
105
+ if math.isnan(x_new) or math.isinf(x_new):
106
+ raise OverflowError("La iteración explotó hacia el infinito.")
107
+ except Exception as e:
108
+ return {
109
+ "metodo": "Punto Fijo",
110
+ "raiz": None,
111
+ "iteraciones": iteraciones,
112
+ "verificacion": verificacion,
113
+ "convergencia": False,
114
+ "num_iter": i,
115
+ "error_msg": f"¡La función diverge! Asegúrate de que |g'(x)| < 1. ({str(e)})"
116
+ }
117
+
118
+ error = abs(x_new - x)
119
+
120
+ # ELIMINAMOS TODOS LOS REDONDEOS ACÁ
121
+ iteraciones.append({
122
+ "i": i + 1,
123
+ "x": x,
124
+ "g_x": x_new,
125
+ "error": error
126
+ })
127
+
128
+ if error < tol:
129
+ return {
130
+ "metodo": "Punto Fijo",
131
+ "raiz": round(x_new, precision),
132
+ "iteraciones": iteraciones,
133
+ "verificacion": verificacion,
134
+ "convergencia": True,
135
+ "num_iter": i + 1
136
+ }
137
+
138
+ x = x_new
139
+
140
+ return {
141
+ "metodo": "Punto Fijo",
142
+ "raiz": round(x, precision),
143
+ "iteraciones": iteraciones,
144
+ "verificacion": verificacion,
145
+ "convergencia": False,
146
+ "num_iter": max_iter,
147
+ "error_msg": "No converge. Revisa si |g'(x)| < 1 en el intervalo."
148
+ }
149
+
150
+ @staticmethod
151
+ def _derivada_numerica(f: Callable, x: float, dx: float = 1e-6) -> float:
152
+ """Aproxima derivada con diferencias centrales."""
153
+ return (f(x + dx) - f(x - dx)) / (2.0 * dx)
154
+
155
+ @staticmethod
156
+ def _verificar_lipschitz(g: Callable, x0: float, dx: float = 1e-6) -> Dict:
157
+ """Verifica condición local de contracción en la semilla: |g'(x0)| < 1."""
158
+ g_prime = float(RootFindingService._derivada_numerica(g, x0, dx))
159
+ return {
160
+ "x0": x0,
161
+ "g_prime": g_prime,
162
+ "abs_g_prime": abs(g_prime),
163
+ "cumple": abs(g_prime) < 1,
164
+ }
165
+
166
+ @staticmethod
167
+ def newton_raphson(f: Callable, x0: float,
168
+ tol: float = 1e-6, max_iter: int = 100,
169
+ precision: int = 8) -> Dict:
170
+ """Método de Newton-Raphson - usa derivada numérica."""
171
+ x = x0
172
+ iteraciones = []
173
+
174
+ for i in range(max_iter):
175
+ try:
176
+ fx = float(f(x))
177
+ dfx = float(RootFindingService._derivada_numerica(f, x))
178
+
179
+ if abs(dfx) < 1e-10:
180
+ raise ValueError("La derivada se hizo cero (tangente horizontal).")
181
+
182
+ x_new = x - fx / dfx
183
+ if math.isnan(x_new) or math.isinf(x_new):
184
+ raise OverflowError("La iteración divergió a infinito.")
185
+ except Exception as e:
186
+ return {
187
+ "metodo": "Newton-Raphson",
188
+ "raiz": None,
189
+ "iteraciones": iteraciones,
190
+ "convergencia": False,
191
+ "num_iter": i,
192
+ "error_msg": f"Falla en la iteración: {str(e)}"
193
+ }
194
+
195
+ error = abs(x_new - x)
196
+
197
+ # ELIMINAMOS TODOS LOS REDONDEOS ACÁ
198
+ iteraciones.append({
199
+ "i": i + 1,
200
+ "x": x,
201
+ "f (x)": fx,
202
+ "f ' (x)": dfx,
203
+ "Resultado": x_new,
204
+ "error": error
205
+ })
206
+
207
+ if error < tol:
208
+ return {
209
+ "metodo": "Newton-Raphson",
210
+ "raiz": round(x_new, precision),
211
+ "iteraciones": iteraciones,
212
+ "convergencia": True,
213
+ "num_iter": i + 1
214
+ }
215
+
216
+ x = x_new
217
+
218
+ return {
219
+ "metodo": "Newton-Raphson",
220
+ "raiz": round(x, precision),
221
+ "iteraciones": iteraciones,
222
+ "convergencia": False,
223
+ "num_iter": max_iter,
224
+ "error_msg": "Se alcanzó el límite de iteraciones sin converger."
225
+ }
226
+
227
+ @staticmethod
228
+ def aitken(g: Callable, x0: float,
229
+ tol: float = 1e-6, max_iter: int = 100,
230
+ precision: int = 8) -> Dict:
231
+ """Aceleración de Aitken sobre punto fijo."""
232
+ x = x0
233
+ iteraciones = []
234
+ verificacion = RootFindingService._verificar_lipschitz(g, x0)
235
+
236
+ for i in range(max_iter):
237
+ try:
238
+ x1 = float(g(x))
239
+ x2 = float(g(x1))
240
+
241
+ denominador = x2 - 2*x1 + x
242
+ if abs(denominador) > 1e-12:
243
+ x_acelerado = x - (x1 - x)**2 / denominador
244
+ else:
245
+ x_acelerado = x2
246
+
247
+ if math.isnan(x_acelerado) or math.isinf(x_acelerado):
248
+ raise OverflowError("La aceleración divergió a infinito.")
249
+ except Exception as e:
250
+ return {
251
+ "metodo": "Aitken",
252
+ "raiz": None,
253
+ "iteraciones": iteraciones,
254
+ "verificacion": verificacion,
255
+ "convergencia": False,
256
+ "num_iter": i,
257
+ "error_msg": f"¡Divergencia detectada! {str(e)}"
258
+ }
259
+
260
+ error = abs(x_acelerado - x)
261
+
262
+ # ELIMINAMOS TODOS LOS REDONDEOS ACÁ
263
+ iteraciones.append({
264
+ "i": i + 1,
265
+ "x": x,
266
+ "g_x": x1,
267
+ "g_g_x": x2,
268
+ "x_acelerado": x_acelerado,
269
+ "error": error
270
+ })
271
+
272
+ if error < tol:
273
+ return {
274
+ "metodo": "Aitken",
275
+ "raiz": round(x_acelerado, precision),
276
+ "iteraciones": iteraciones,
277
+ "verificacion": verificacion,
278
+ "convergencia": True,
279
+ "num_iter": i + 1
280
+ }
281
+
282
+ x = x_acelerado
283
+
284
+ return {
285
+ "metodo": "Aitken",
286
+ "raiz": round(x, precision),
287
+ "iteraciones": iteraciones,
288
+ "verificacion": verificacion,
289
+ "convergencia": False,
290
+ "num_iter": max_iter,
291
+ "error_msg": "No converge."
292
+ }
293
+ @staticmethod
294
+ def comparar_metodos(func_str: str, g_str: str, a: float, b: float, x0: float,
295
+ tol: float = 1e-6, max_iter: int = 100, precision: int = 8) -> Dict:
296
+ """
297
+ Orquestador que ejecuta los 4 métodos en paralelo y devuelve un resumen comparativo.
298
+ """
299
+ resultados = {
300
+ "biseccion": None,
301
+ "punto_fijo": None,
302
+ "newton_raphson": None,
303
+ "aitken": None
304
+ }
305
+
306
+ # 1. Bisección
307
+ try:
308
+ f_bisec = RootFindingService.compilar_funcion(func_str)
309
+ resultados["biseccion"] = RootFindingService.biseccion(f_bisec, a, b, tol, max_iter, precision)
310
+ except Exception as e:
311
+ resultados["biseccion"] = {"convergencia": False, "error_msg": str(e), "iteraciones": []}
312
+
313
+ # 2. Punto Fijo
314
+ try:
315
+ g_pf = RootFindingService.compilar_funcion(g_str)
316
+ resultados["punto_fijo"] = RootFindingService.punto_fijo(g_pf, x0, tol, max_iter, precision)
317
+ except Exception as e:
318
+ resultados["punto_fijo"] = {"convergencia": False, "error_msg": str(e), "iteraciones": []}
319
+
320
+ # 3. Newton-Raphson
321
+ try:
322
+ f_nr = RootFindingService.compilar_funcion(func_str)
323
+ resultados["newton_raphson"] = RootFindingService.newton_raphson(f_nr, x0, tol, max_iter, precision)
324
+ except Exception as e:
325
+ resultados["newton_raphson"] = {"convergencia": False, "error_msg": str(e), "iteraciones": []}
326
+
327
+ # 4. Aitken
328
+ try:
329
+ g_aitken = RootFindingService.compilar_funcion(g_str)
330
+ resultados["aitken"] = RootFindingService.aitken(g_aitken, x0, tol, max_iter, precision)
331
+ except Exception as e:
332
+ resultados["aitken"] = {"convergencia": False, "error_msg": str(e), "iteraciones": []}
333
+
334
+ return {
335
+ "parametros": {
336
+ "f(x)": func_str, "g(x)": g_str, "a": a, "b": b, "x0": x0, "tol": tol
337
+ },
338
+ "comparativa": resultados
339
+ }
@@ -0,0 +1,226 @@
1
+ import numpy as np
2
+ from typing import Callable, Tuple, List, Dict, Any
3
+ import sympy as sp
4
+ import re
5
+ from sympy.parsing.sympy_parser import parse_expr, standard_transformations, implicit_multiplication_application
6
+
7
+ # ==========================================================================
8
+ # SECURE EXPRESSION PARSING
9
+ # --------------------------------------------------------------------------
10
+ # SymPy's ``sympify`` and ``parse_expr`` evaluate strings with ``eval`` under
11
+ # the hood and allow attribute access and ``__import__`` on recent versions
12
+ # (verified: sympify("__import__('os').system(...)") executes the command).
13
+ # Every user-supplied expression must go through ``safe_sympify`` /
14
+ # ``safe_parse_expr`` which run a strict lexical gate BEFORE parsing:
15
+ # * length cap (denial-of-service mitigation)
16
+ # * identifier whitelist (variables + known functions + constants)
17
+ # * no "." or "__" (blocks attribute access)
18
+ # * integer-digit cap (blocks memory-exhausting eager evaluation)
19
+ # ==========================================================================
20
+
21
+ MAX_EXPRESSION_LENGTH = 200
22
+ MAX_INTEGER_DIGITS = 6
23
+
24
+ ALLOWED_FUNCTIONS = frozenset(
25
+ {
26
+ # trigonometric
27
+ "sin", "cos", "tan", "cot", "sec", "csc",
28
+ "asin", "acos", "atan", "acot", "asec", "acsc", "atan2",
29
+ "sinh", "cosh", "tanh", "coth", "sech", "csch",
30
+ "asinh", "acosh", "atanh",
31
+ # exponential / logarithmic / roots
32
+ "exp", "log", "ln", "sqrt", "cbrt", "root",
33
+ # absolute value / sign / rounding
34
+ "Abs", "sign", "floor", "ceiling", "frac",
35
+ # gamma family
36
+ "gamma", "loggamma", "factorial", "binomial", "subfactorial",
37
+ # special functions
38
+ "erf", "erfc", "erfi",
39
+ "Min", "Max",
40
+ }
41
+ )
42
+
43
+ _LOGIC_KEYWORDS = frozenset({"and", "or", "not"})
44
+ _CONSTANTS = frozenset({"e", "E", "pi"})
45
+
46
+ _IDENTIFIER_RE = re.compile(r"[^\W\d_]\w*", re.UNICODE)
47
+ _NUMBER_RE = re.compile(r"(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?")
48
+ _SINGLE_VAR = re.compile(r"(?<![A-Za-z0-9_])[eE]\^")
49
+ _ALLOWED_PUNCTUATION = set("()+-,*/%<>!=:")
50
+
51
+
52
+ def normalize_expr(expr: str) -> str:
53
+ """Normalize common math notations to SymPy syntax.
54
+
55
+ ``e^x`` / ``e^(-x)`` become ``E**x`` / ``E**(-x)`` (fixes the old
56
+ unbalanced ``exp(`` replacement), ``sen`` becomes ``sin`` and every
57
+ remaining ``^`` becomes ``**``.
58
+ """
59
+ out = expr.replace("sen", "sin")
60
+ out = _SINGLE_VAR.sub("E**", out)
61
+ out = out.replace("^", "**")
62
+ return out
63
+
64
+
65
+ def _check_lexical(expr: str, allowed_identifiers) -> None:
66
+ """Reject any token that is not a number, allowed variable/constant or
67
+ allowed function, and any attribute access (``.`` / ``__``)."""
68
+ allowed_words = (
69
+ set(allowed_identifiers)
70
+ | ALLOWED_FUNCTIONS
71
+ | _CONSTANTS
72
+ | _LOGIC_KEYWORDS
73
+ )
74
+
75
+ i, n = 0, len(expr)
76
+ while i < n:
77
+ ch = expr[i]
78
+ if ch.isspace():
79
+ i += 1
80
+ continue
81
+
82
+ number = _NUMBER_RE.match(expr, i)
83
+ if number:
84
+ text = number.group()
85
+ int_part = text.split(".")[0].split("e")[0].split("E")[0].lstrip("+-")
86
+ if len(int_part) > MAX_INTEGER_DIGITS:
87
+ raise ValueError(
88
+ f"Integer literal too large ({len(int_part)} digits, max {MAX_INTEGER_DIGITS})."
89
+ )
90
+ i = number.end()
91
+ continue
92
+
93
+ ident = _IDENTIFIER_RE.match(expr, i)
94
+ if ident:
95
+ token = ident.group()
96
+ if token not in allowed_words:
97
+ raise ValueError(f"Unknown or forbidden token: {token}")
98
+ i = ident.end()
99
+ continue
100
+
101
+ if ch in _ALLOWED_PUNCTUATION:
102
+ i += 1
103
+ continue
104
+ if ch == ".":
105
+ raise ValueError("Attribute access is not allowed.")
106
+ if ch == "_":
107
+ raise ValueError("Underscore identifiers are not allowed.")
108
+ raise ValueError(f"Invalid character: {ch!r}")
109
+
110
+
111
+ def safe_sympify(expr_str: str, local_dict: Dict = None, allowed_symbols=()) -> sp.Expr:
112
+ """Lexically vetted ``sp.sympify``. ``allowed_symbols`` lists the variable
113
+ names the expression may reference (plus the constants e/pi/E)."""
114
+ if not expr_str or not expr_str.strip():
115
+ raise ValueError("Expression cannot be empty.")
116
+ if len(expr_str) > MAX_EXPRESSION_LENGTH:
117
+ raise ValueError(
118
+ f"Expression too long ({len(expr_str)} chars, max {MAX_EXPRESSION_LENGTH})."
119
+ )
120
+ normalized = normalize_expr(expr_str)
121
+ _check_lexical(normalized, allowed_symbols)
122
+ try:
123
+ return sp.sympify(normalized, locals=local_dict)
124
+ except Exception as e:
125
+ raise ValueError(f"Invalid math expression: {str(e)}") from e
126
+
127
+
128
+ def safe_parse_expr(expr_str: str, local_dict: Dict = None, allowed_symbols=(),
129
+ transformations: Tuple = ()) -> sp.Expr:
130
+ """Lexically vetted ``parse_expr``. ``allowed_symbols`` lists the variable
131
+ names the expression may reference (plus the constants e/pi/E)."""
132
+ if not expr_str or not expr_str.strip():
133
+ raise ValueError("Expression cannot be empty.")
134
+ if len(expr_str) > MAX_EXPRESSION_LENGTH:
135
+ raise ValueError(
136
+ f"Expression too long ({len(expr_str)} chars, max {MAX_EXPRESSION_LENGTH})."
137
+ )
138
+ normalized = normalize_expr(expr_str)
139
+ _check_lexical(normalized, allowed_symbols)
140
+ try:
141
+ return parse_expr(normalized, local_dict=local_dict, transformations=transformations)
142
+ except Exception as e:
143
+ raise ValueError(f"Invalid math expression: {str(e)}") from e
144
+
145
+
146
+ # ==========================================================================
147
+ # Legacy helpers (kept for backwards compatibility)
148
+ # ==========================================================================
149
+
150
+ def parse_function(func_str: str) -> Callable:
151
+ """
152
+ Convierte string a función vectorizada NumPy.
153
+ Ejemplo: "sin(x) + x**2" -> función callable
154
+ """
155
+ try:
156
+ x = sp.Symbol('x')
157
+ expr = safe_parse_expr(
158
+ func_str,
159
+ local_dict={'e': sp.E, 'pi': sp.pi},
160
+ allowed_symbols=['x'],
161
+ transformations=(standard_transformations + (implicit_multiplication_application,))
162
+ )
163
+ f_numpy = sp.lambdify(x, expr, modules=['numpy'])
164
+ return f_numpy
165
+ except Exception as e:
166
+ raise ValueError(f"Error al parsear función: {str(e)}")
167
+
168
+
169
+ def numerical_derivative(f: Callable, x: float, order: int = 1, h: float = 1e-6) -> float:
170
+ """
171
+ Calcula derivada numérica usando diferencias centrales.
172
+ order: 1 (primera derivada), 2 (segunda), 3 (tercera), 4 (cuarta)
173
+ """
174
+ if order == 1:
175
+ return (f(x + h) - f(x - h)) / (2 * h)
176
+ elif order == 2:
177
+ return (f(x + h) - 2 * f(x) + f(x - h)) / (h**2)
178
+ elif order == 3:
179
+ return (f(x + 2*h) - 2*f(x + h) + 2*f(x - h) - f(x - 2*h)) / (2 * h**3)
180
+ elif order == 4:
181
+ return (f(x + 2*h) - 4*f(x + h) + 6*f(x) - 4*f(x - h) + f(x - 2*h)) / (h**4)
182
+ else:
183
+ raise ValueError("Order debe ser 1, 2, 3 o 4")
184
+
185
+
186
+ def generate_x_values(a: float, b: float, n: int = 100) -> np.ndarray:
187
+ """Genera n valores equidistantes entre a y b"""
188
+ return np.linspace(a, b, n)
189
+
190
+
191
+ def safe_eval(f: Callable, x: float | np.ndarray) -> float | np.ndarray:
192
+ """Evalúa función de forma segura, manejando excepciones"""
193
+ try:
194
+ result = f(x)
195
+ if np.isnan(result) or np.isinf(result):
196
+ return None
197
+ return result
198
+ except:
199
+ return None
200
+
201
+
202
+ def format_number(value: float, precision: int = 6) -> str:
203
+ """Formatea número con precisión específica"""
204
+ if value is None or np.isnan(value) or np.isinf(value):
205
+ return "N/A"
206
+ return f"{value:.{precision}f}"
207
+
208
+
209
+ def create_plotly_json(x_data: List, y_data: List, title: str, x_label: str = "x",
210
+ y_label: str = "y", mode: str = "lines") -> Dict[str, Any]:
211
+ """Crea JSON compatible con Plotly"""
212
+ return {
213
+ "data": [{
214
+ "x": x_data,
215
+ "y": y_data,
216
+ "mode": mode,
217
+ "type": "scatter",
218
+ "name": title
219
+ }],
220
+ "layout": {
221
+ "title": title,
222
+ "xaxis": {"title": x_label},
223
+ "yaxis": {"title": y_label},
224
+ "hovermode": "closest"
225
+ }
226
+ }