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