|
14 | 14 |
|
15 | 15 | import sympy |
16 | 16 | import mpmath |
| 17 | +import numpy as np |
17 | 18 | from mpmath import mpf |
18 | 19 | import math |
19 | 20 | import hashlib |
20 | 21 | import zlib |
21 | 22 | import math |
22 | 23 | from collections import namedtuple |
23 | 24 | from contextlib import contextmanager |
24 | | -from itertools import chain |
| 25 | +from itertools import chain, product |
25 | 26 |
|
26 | 27 |
|
27 | 28 | from mathics.builtin.base import Builtin, Predefined |
|
45 | 46 | Symbol, |
46 | 47 | SymbolFalse, |
47 | 48 | SymbolTrue, |
| 49 | + SymbolFailed, |
48 | 50 | from_python, |
49 | 51 | ) |
50 | 52 | from mathics.core.convert import from_sympy |
@@ -255,6 +257,300 @@ def apply_other(self, expr, prec, evaluation): |
255 | 257 | return Expression(head, *leaves) |
256 | 258 |
|
257 | 259 |
|
| 260 | + |
| 261 | +def _internal_adaptative_simpsons_rule(f, a, b, **opts): |
| 262 | + """ |
| 263 | + 1D adaptative Simpson's rule integrator |
| 264 | + Adapted from https://en.wikipedia.org/wiki/Adaptive_Simpson%27s_method |
| 265 | + by @mmatera |
| 266 | +
|
| 267 | + TODO: handle weak divergences |
| 268 | + """ |
| 269 | + wsr = 1./6. |
| 270 | + |
| 271 | + tol = opts.get("tol") |
| 272 | + if not tol: |
| 273 | + tol = 1.e-10 |
| 274 | + |
| 275 | + maxrec = opts.get("maxrec") |
| 276 | + if not maxrec: |
| 277 | + maxrec = 150 |
| 278 | + |
| 279 | + def _quad_simpsons_mem(f, a, fa, b, fb): |
| 280 | + """Evaluates the Simpson's Rule, also returning m and f(m) to reuse""" |
| 281 | + m = .5 * (a + b) |
| 282 | + try: |
| 283 | + fm = f(m) |
| 284 | + except ZeroDivisionError: |
| 285 | + fm = None |
| 286 | + |
| 287 | + if fm is None or np.isinf(fm): |
| 288 | + m = m + 1e-10 |
| 289 | + fm = f(m) |
| 290 | + return (m, fm, wsr * abs(b - a) * (fa + 4. * fm + fb)) |
| 291 | + |
| 292 | + def _quad_asr(f, a, fa, b, fb, eps, whole, m, fm, maxrec): |
| 293 | + """ |
| 294 | + Efficient recursive implementation of adaptive Simpson's rule. |
| 295 | + Function values at the start, middle, end of the intervals are retained. |
| 296 | + """ |
| 297 | + maxrec = maxrec - 1 |
| 298 | + try: |
| 299 | + left = _quad_simpsons_mem(f, a, fa, m, fm) |
| 300 | + lm, flm, left = left |
| 301 | + right = _quad_simpsons_mem(f, m, fm, b, fb) |
| 302 | + rm, frm, right = right |
| 303 | + |
| 304 | + delta = left + right - whole |
| 305 | + err = abs(delta) |
| 306 | + if err <= 15 * eps or maxrec == 0: |
| 307 | + return (left + right + delta / 15, err) |
| 308 | + left = _quad_asr(f, a, fa, m, fm, eps/2, left , lm, flm, maxrec) |
| 309 | + right = _quad_asr(f, m, fm, b, fb, eps/2, right, rm, frm, maxrec) |
| 310 | + return (left[0] + right[0], left[1] + right[1]) |
| 311 | + except: |
| 312 | + raise |
| 313 | + |
| 314 | + def ensure_evaluation(f, x): |
| 315 | + try: |
| 316 | + val = f(x) |
| 317 | + except ZeroDivisionError: |
| 318 | + return None |
| 319 | + if np.isinf(val): |
| 320 | + return None |
| 321 | + return val |
| 322 | + |
| 323 | + invert_interval = False |
| 324 | + if a > b: |
| 325 | + b, a, invert_interval = a, b, True |
| 326 | + |
| 327 | + fa, fb = ensure_evaluation(f, a), ensure_evaluation(f,b) |
| 328 | + if fa is None: |
| 329 | + x = 10. * machine_epsilon if a == 0 else a*(1. + 10.*machine_epsilon) |
| 330 | + fa = ensure_evaluation(f, x) |
| 331 | + if fa is None: |
| 332 | + raise Exception(f"Function undefined around {a}. Cannot integrate") |
| 333 | + if fb is None: |
| 334 | + x = -10. * machine_epsilon if b == 0 else b*(1. - 10.*machine_epsilon) |
| 335 | + fb = ensure_evaluation(f, x) |
| 336 | + if fb is None: |
| 337 | + raise Exception(f"Function undefined around {b}. Cannot integrate") |
| 338 | + |
| 339 | + |
| 340 | + m, fm, whole = _quad_simpsons_mem(f, a, fa, b, fb) |
| 341 | + return _quad_asr(f, a, fa, b, fb, tol, whole, m, fm, maxrec) |
| 342 | + |
| 343 | +def _fubini(func, ranges, **opts): |
| 344 | + if not ranges: |
| 345 | + return 0. |
| 346 | + a, b = ranges[0] |
| 347 | + integrator = opts["integrator"] |
| 348 | + tol = opts.get("tol") |
| 349 | + if tol is None: |
| 350 | + opts["tol"] = 1.e-10 |
| 351 | + tol = 1.e-10 |
| 352 | + |
| 353 | + if len(ranges) > 1: |
| 354 | + def subintegral(*u): |
| 355 | + def ff(*z): |
| 356 | + return func(*(u+z)) |
| 357 | + |
| 358 | + return _fubini(ff, ranges[1:], **opts)[0] |
| 359 | + opts["tol"] = 4. * tol |
| 360 | + return integrator(subintegral, a, b, **opts) |
| 361 | + else: |
| 362 | + return integrator(func, a, b, **opts) |
| 363 | + |
| 364 | + |
| 365 | +class NIntegrate(Builtin): |
| 366 | + """ |
| 367 | + <dl> |
| 368 | + <dt>'NIntegrate[$expr$, $interval$]' |
| 369 | + <dd>evaluates the definite integral of $expr$ numerically with a precision of $prec$ digits. |
| 370 | + </dl> |
| 371 | +
|
| 372 | + >> Table[1./NIntegrate[x^k,{x,0,1},Tolerance->1*^-6], {k,0,6}] |
| 373 | + = {1., 2., 3., 4., 5., 6., 7.} |
| 374 | + >> NIntegrate[Exp[-x],{x,0,Infinity},Tolerance->1*^-6] |
| 375 | + = 1. |
| 376 | + >> NIntegrate[Exp[x],{x,-Infinity, 0},Tolerance->1*^-6] |
| 377 | + = 1. |
| 378 | + >> NIntegrate[Exp[-x^2/2.],{x,-Infinity, Infinity},Tolerance->1*^-6] |
| 379 | + = 2.50663 |
| 380 | +
|
| 381 | + Integrate singularities with weak divergences |
| 382 | + >> Table[NIntegrate[x^(1./k-1.),{x,0,1.},Tolerance->1*^-6], {k,1,7.}] |
| 383 | + = {1., 2., 3., 4., 5., 6., 7.} |
| 384 | +
|
| 385 | + Iterated Integral |
| 386 | + >> NIntegrate[x * y,{x, 0, 1},{y, 0, 1}] |
| 387 | + = 0.25 |
| 388 | + """ |
| 389 | +# rules = {"N[Integrate[f_,{x_, a_Real, b__}]": "NIntegrate[f,{x,a,b}]", |
| 390 | +# "N[Integrate[f_,{x_, a_Real, b_Real}, prec_?NumericQ]": |
| 391 | +# 'NIntegrate[f,{x,a,b}, "AccuracyGoal"->prec]' |
| 392 | +# } |
| 393 | + |
| 394 | + # messages = {'nnintegr':"The integrand `1` is not a numeric function", |
| 395 | + # 'nninflim': "The inferior limit `1` is not numeric", |
| 396 | + # 'nninflim': "The superior limit `1` is not numeric", |
| 397 | + # } |
| 398 | + |
| 399 | + options = {'"Method"': '"Automatic"', |
| 400 | + 'Tolerance': '1*^-10', |
| 401 | + 'Accuracy': '1*^-10', |
| 402 | + 'MaxRecursion': '10', |
| 403 | + } |
| 404 | + |
| 405 | + |
| 406 | + methods = {'Automatic': None,} |
| 407 | + |
| 408 | + def __init__(self, *args, **kwargs): |
| 409 | + super().__init__(*args, **kwargs) |
| 410 | + self.methods["Internal"] = _internal_adaptative_simpsons_rule |
| 411 | + try: |
| 412 | + from scipy.integrate import (romberg, quad) |
| 413 | + print("Using scipy.integrate for numeric integration") |
| 414 | + self.methods["Quadrature"] = quad |
| 415 | + self.methods["Quadrature"] = romberg |
| 416 | + self.methods["Automatic"] = self.methods["Internal"] |
| 417 | + except Exception as e: |
| 418 | + print(e) |
| 419 | + print("Scipy not available. Using internal integrator") |
| 420 | + self.methods["Automatic"] = self.methods["Internal"] |
| 421 | + self.methods["Simpson"] = self.methods["Internal"] |
| 422 | + |
| 423 | + @staticmethod |
| 424 | + def decompose_domain(interval, evaluation): |
| 425 | + if interval.has_form("System`Sequence", 1, None): |
| 426 | + intervals = [] |
| 427 | + for leaf in interval.leaves: |
| 428 | + inner_interval = NIntegrate.decompose_domain(leaf, evaluation) |
| 429 | + if inner_interval: |
| 430 | + intervals.append(inner_interval) |
| 431 | + else: |
| 432 | + evaluation.message("Malformed interval") |
| 433 | + return None |
| 434 | + return intervals |
| 435 | + |
| 436 | + if interval.has_form('System`List',3, None): |
| 437 | + intervals = [] |
| 438 | + intvar = interval.leaves[0] |
| 439 | + if not isinstance(intvar, Symbol): |
| 440 | + return None |
| 441 | + boundaries = [a for a in interval.leaves[1:]] |
| 442 | + for i in range(len(boundaries)-1): |
| 443 | + intervals.append((boundaries[i],boundaries[i+1])) |
| 444 | + if len(intervals)>0: |
| 445 | + return (intvar, intervals) |
| 446 | + |
| 447 | + return None |
| 448 | + evaluation.message("Domain not properly formatted") |
| 449 | + return None |
| 450 | + |
| 451 | + def apply_2(self, func, domain, evaluation, options): |
| 452 | + "NIntegrate[func_, domain__, OptionsPattern[%(name)s]]" |
| 453 | + method = options['System`"Method"'].value |
| 454 | + tolerance = options['System`Tolerance'].evaluate(evaluation) |
| 455 | + tolerance = float(tolerance.value) |
| 456 | + accuracy = options['System`Accuracy'].evaluate(evaluation) |
| 457 | + accuracy = accuracy.value |
| 458 | + maxrecursion = options['System`MaxRecursion'].evaluate(evaluation) |
| 459 | + maxrecursion = maxrecursion.value |
| 460 | + |
| 461 | + nintegrate_method = self.methods.get(method) |
| 462 | + if nintegrate_method is None: |
| 463 | + nintegrate_method = self.methods.get("Automatic") |
| 464 | + |
| 465 | + domain = self.decompose_domain(domain, evaluation) |
| 466 | + if not domain: |
| 467 | + return |
| 468 | + if not isinstance(domain,list): |
| 469 | + domain = [domain] |
| 470 | + |
| 471 | + coords = [axis[0] for axis in domain] |
| 472 | + intvars = Expression("List", *coords) |
| 473 | + integrand = Expression("Compile", intvars, func).evaluate(evaluation) |
| 474 | + if len(integrand.leaves)>=3: |
| 475 | + integrand = integrand.leaves[2].cfunc |
| 476 | + else: |
| 477 | + print(func, " could not be compiled") |
| 478 | + return |
| 479 | + results = [] |
| 480 | + for subdomain in product(*[axis[1] for axis in domain]): |
| 481 | + # On each subdomain, check if the region is bounded. |
| 482 | + # If not, implement a coordinate map |
| 483 | + func2 = integrand |
| 484 | + subdomain2 = [] |
| 485 | + coordtransform = [] |
| 486 | + nulldomain = False |
| 487 | + for i, r in enumerate(subdomain): |
| 488 | + a = r[0].evaluate(evaluation) |
| 489 | + b = r[1].evaluate(evaluation) |
| 490 | + if a == b: |
| 491 | + nulldomain = True |
| 492 | + break |
| 493 | + elif a.get_head_name() == 'System`DirectedInfinity': |
| 494 | + if b.get_head_name() == 'System`DirectedInfinity': |
| 495 | + a = a.to_python() |
| 496 | + b = b.to_python() |
| 497 | + l = 1 - machine_epsilon |
| 498 | + if a == b: |
| 499 | + nulldomain = True |
| 500 | + break |
| 501 | + elif a < b: |
| 502 | + subdomain2.append([-l, l]) |
| 503 | + else: |
| 504 | + subdomain2.append([l, -l]) |
| 505 | + coordtransform.append((np.arctanh, lambda u:1./(1.-u**2))) |
| 506 | + else: |
| 507 | + if not b.is_numeric(): |
| 508 | + evaluation.message("Boundary is not numeric.") |
| 509 | + return SymbolFailed |
| 510 | + z = a.leaves[0].value |
| 511 | + b = b.value |
| 512 | + subdomain2.append( [machine_epsilon, 1.]) |
| 513 | + coordtransform.append((lambda u: b-z + z/u, lambda u: -z*u**(-2.))) |
| 514 | + elif b.get_head_name() == 'System`DirectedInfinity': |
| 515 | + if not a.is_numeric(): |
| 516 | + evaluation.message("Boundary is not numeric.") |
| 517 | + return SymbolFailed |
| 518 | + a = a.value |
| 519 | + z = b.leaves[0].value |
| 520 | + subdomain2.append([machine_epsilon, 1.]) |
| 521 | + coordtransform.append((lambda u: a-z + z/u, lambda u: z*u**(-2.))) |
| 522 | + elif a.is_numeric() and b.is_numeric(): |
| 523 | + a = Expression("N", a).evaluate(evaluation).value |
| 524 | + b = Expression("N", b).evaluate(evaluation).value |
| 525 | + subdomain2.append([a, b]) |
| 526 | + coordtransform.append(None) |
| 527 | + else: |
| 528 | + evaluation.message("Boundary is not numeric.") |
| 529 | + return SymbolFailed |
| 530 | + |
| 531 | + if nulldomain: |
| 532 | + continue |
| 533 | + |
| 534 | + if any(coordtransform): |
| 535 | + func2 = lambda *u: (integrand(*[x[0](u[i]) if x else u[i] |
| 536 | + for i, x in enumerate(coordtransform)]) |
| 537 | + * np.prod([ jac[1](u[i] ) |
| 538 | + for i, jac in enumerate(coordtransform) if jac])) |
| 539 | + opts = {#"" : accuracy, |
| 540 | + "tol" : tolerance, |
| 541 | + #"maxrec" : maxrecursion, |
| 542 | + } |
| 543 | + if len(subdomain2) >1: |
| 544 | + val = _fubini(func2, subdomain2, |
| 545 | + integrator=nintegrate_method, **opts) |
| 546 | + else: |
| 547 | + val = nintegrate_method(func2, *(subdomain2[0]), **opts) |
| 548 | + results.append(val) |
| 549 | + result = sum([r[0] for r in results]) |
| 550 | + error = sum([r[1] for r in results]) |
| 551 | + # print("result=", result, " error=",error) |
| 552 | + return from_python(result) |
| 553 | + |
258 | 554 | class MachinePrecision(Predefined): |
259 | 555 | """ |
260 | 556 | <dl> |
|
0 commit comments