Skip to content

Commit 574b54f

Browse files
committed
adding tests. starting support for complex functions. starting support for scipy methods
1 parent d11bf47 commit 574b54f

2 files changed

Lines changed: 206 additions & 44 deletions

File tree

mathics/builtin/numeric.py

Lines changed: 156 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
PrecisionReal,
4444
Rational,
4545
Real,
46+
String,
4647
Symbol,
4748
SymbolFalse,
4849
SymbolTrue,
@@ -256,7 +257,38 @@ def apply_other(self, expr, prec, evaluation):
256257
)
257258
return Expression(head, *leaves)
258259

260+
def _scipy_interface(integrator, options_map, mandatory=None, adapt_func=None):
261+
"""
262+
This function provides a proxy for scipy.integrate
263+
functions, adapting the parameters.
264+
"""
265+
def _scipy_proxy_func_filter(fun, a, b, **opts):
266+
native_opts = {}
267+
if mandatory:
268+
native_opts.update(mandatory)
269+
for opt, val in opts.items():
270+
native_opt = options_map.get(opt, None)
271+
if native_opt:
272+
if native_opt[1]:
273+
val = native_opt[1](val)
274+
native_opts[native_opt[0]] = val
275+
return adapt_func(integrator(fun, a, b, **native_opts))
276+
277+
def _scipy_proxy_func(fun, a, b, **opts):
278+
native_opts = {}
279+
if mandatory:
280+
native_opts.update(mandatory)
281+
for opt, val in opts.items():
282+
native_opt = options_map.get(opt, None)
283+
if native_opt:
284+
if native_opt[1]:
285+
val = native_opt[1](val)
286+
native_opts[native_opt[0]] = val
287+
return integrator(fun, a, b, **native_opts)
288+
289+
return _scipy_proxy_func_filter if adapt_func else _scipy_proxy_func
259290

291+
260292

261293
def _internal_adaptative_simpsons_rule(f, a, b, **opts):
262294
"""
@@ -352,17 +384,22 @@ def _fubini(func, ranges, **opts):
352384
if tol is None:
353385
opts["tol"] = 1.e-10
354386
tol = 1.e-10
355-
387+
356388
if len(ranges) > 1:
357389
def subintegral(*u):
358390
def ff(*z):
359-
return func(*(u+z))
360-
361-
return _fubini(ff, ranges[1:], **opts)[0]
391+
val = func(*(u+z))
392+
return val
393+
394+
val = _fubini(ff, ranges[1:], **opts)[0]
395+
return val
396+
362397
opts["tol"] = 4. * tol
363-
return integrator(subintegral, a, b, **opts)
398+
val = integrator(subintegral, a, b, **opts)
399+
return val
364400
else:
365-
return integrator(func, a, b, **opts)
401+
val = integrator(func, a, b, **opts)
402+
return val
366403

367404

368405
class NIntegrate(Builtin):
@@ -373,6 +410,7 @@ class NIntegrate(Builtin):
373410
</dl>
374411
375412
>> Table[1./NIntegrate[x^k,{x,0,1},Tolerance->1*^-6], {k,0,6}]
413+
: The especified method failed to return a number. Falling back into the internal evaluator.
376414
= {1., 2., 3., 4., 5., 6., 7.}
377415
>> NIntegrate[Exp[-x],{x,0,Infinity},Tolerance->1*^-6]
378416
= 1.
@@ -381,6 +419,11 @@ class NIntegrate(Builtin):
381419
>> NIntegrate[Exp[-x^2/2.],{x,-Infinity, Infinity},Tolerance->1*^-6]
382420
= 2.50663
383421
422+
>> NIntegrate[1 / z, {z, -1 - I, 1 - I, 1 + I, -1 + I, -1 - I}, Tolerance->1.*^-4]
423+
: Integration over a complex domain is not implemented yet
424+
= NIntegrate[1 / z, {z, -1 - I, 1 - I, 1 + I, -1 + I, -1 - I}, Tolerance -> 0.0001]
425+
# = 6.2832 I
426+
384427
Integrate singularities with weak divergences
385428
>> Table[NIntegrate[x^(1./k-1.),{x,0,1.},Tolerance->1*^-6], {k,1,7.}]
386429
= {1., 2., 3., 4., 5., 6., 7.}
@@ -394,35 +437,51 @@ class NIntegrate(Builtin):
394437
# 'NIntegrate[f,{x,a,b}, "AccuracyGoal"->prec]'
395438
# }
396439

397-
# messages = {'nnintegr':"The integrand `1` is not a numeric function",
398-
# 'nninflim': "The inferior limit `1` is not numeric",
399-
# 'nninflim': "The superior limit `1` is not numeric",
400-
# }
401-
402-
options = {'"Method"': '"Automatic"',
440+
messages = {'bdmtd': "The Method option should be a built-in method name.",
441+
'inumr': " The integrand `1` has evaluated to non-numerical values for all sampling points in the region with boundaries `2`",
442+
'nlim' : '`1` = `2` is not a valid limit of integration.',
443+
'ilim' : 'Invalid integration variable or limit(s) in `1`.',
444+
'mtdfail' : "The especified method failed to return a number. Falling back into the internal evaluator.",
445+
'cmpint' : "Integration over a complex domain is not implemented yet",
446+
}
447+
448+
options = {'Method': '"Automatic"',
403449
'Tolerance': '1*^-10',
404450
'Accuracy': '1*^-10',
405451
'MaxRecursion': '10',
406452
}
407453

408454

409-
methods = {'Automatic': None,}
455+
methods = {'Automatic': (None, False),}
410456

411457
def __init__(self, *args, **kwargs):
412458
super().__init__(*args, **kwargs)
413-
self.methods["Internal"] = _internal_adaptative_simpsons_rule
459+
self.methods["Internal"] = (_internal_adaptative_simpsons_rule, False)
414460
try:
415-
from scipy.integrate import (romberg, quad)
461+
from scipy.integrate import (romberg, quad, nquad)
416462
print("Using scipy.integrate for numeric integration")
417-
self.methods["Quadrature"] = quad
418-
self.methods["Quadrature"] = romberg
419-
self.methods["Automatic"] = self.methods["Internal"]
463+
self.methods["NQuadrature"] = (_scipy_interface(nquad,
464+
{}, {"full_output": 1},
465+
lambda res: (res[0], res[1])), True)
466+
self.methods["Quadrature"] = (_scipy_interface(quad,
467+
{"tol": ("epsabs", None),
468+
"maxrec": ("limit",lambda maxrec: int(2**maxrec))
469+
}, {"full_output": 1},
470+
lambda res: (res[0], res[1])), False)
471+
self.methods["Romberg"] = (_scipy_interface(romberg,
472+
{"tol": ("tol", None),
473+
"maxrec": ("divmax", None)
474+
}, None, lambda x: (x, np.nan) ), False)
475+
self.methods["Automatic"] = self.methods["Quadrature"]
420476
except Exception as e:
421-
print(e)
422477
print("Scipy not available. Using internal integrator")
423478
self.methods["Automatic"] = self.methods["Internal"]
424479
self.methods["Simpson"] = self.methods["Internal"]
425480

481+
self.messages["bdmtd"] = "The Method option should be a built-in method name in {`" + \
482+
"`, `".join(list(self.methods)) + \
483+
"`}. Using `Automatic`"
484+
426485
@staticmethod
427486
def decompose_domain(interval, evaluation):
428487
if interval.has_form("System`Sequence", 1, None):
@@ -432,38 +491,59 @@ def decompose_domain(interval, evaluation):
432491
if inner_interval:
433492
intervals.append(inner_interval)
434493
else:
435-
evaluation.message("Malformed interval")
494+
evaluation.message("ilim", leaf)
436495
return None
437496
return intervals
438497

439-
if interval.has_form('System`List',3, None):
498+
if interval.has_form('System`List', 3, None):
440499
intervals = []
441500
intvar = interval.leaves[0]
442501
if not isinstance(intvar, Symbol):
502+
evaluation.message("ilim", interval)
443503
return None
444504
boundaries = [a for a in interval.leaves[1:]]
505+
if any([b.get_head_name() == "System`Complex" for b in boundaries]):
506+
intvar = Expression("List", intvar, Expression("Blank", Symbol("Complex")))
445507
for i in range(len(boundaries)-1):
446-
intervals.append((boundaries[i],boundaries[i+1]))
508+
intervals.append((boundaries[i], boundaries[i+1]))
447509
if len(intervals)>0:
448510
return (intvar, intervals)
449511

450-
return None
451-
evaluation.message("Domain not properly formatted")
512+
513+
evaluation.message("ilim", interval)
452514
return None
453515

454516
def apply_2(self, func, domain, evaluation, options):
455517
"NIntegrate[func_, domain__, OptionsPattern[%(name)s]]"
456-
method = options['System`"Method"'].value
518+
method = options['System`Method'].evaluate(evaluation)
519+
method_options = {}
520+
if method.has_form("System`List",2):
521+
method = method.leaves[0]
522+
method_options.update(method.leaves[1].get_option_values())
523+
524+
if isinstance(method, String):
525+
method = method.value
526+
elif isinstance(method, Symbol):
527+
method = method.get_name()
528+
else:
529+
evaluation.message("NIntegrate", "bdmtd", method)
530+
return
457531
tolerance = options['System`Tolerance'].evaluate(evaluation)
458532
tolerance = float(tolerance.value)
459533
accuracy = options['System`Accuracy'].evaluate(evaluation)
460534
accuracy = accuracy.value
461535
maxrecursion = options['System`MaxRecursion'].evaluate(evaluation)
462536
maxrecursion = maxrecursion.value
463537

464-
nintegrate_method = self.methods.get(method)
538+
nintegrate_method = self.methods.get(method, None)
465539
if nintegrate_method is None:
540+
evaluation.message("NIntegrate","bdmtd", method)
466541
nintegrate_method = self.methods.get("Automatic")
542+
543+
if type(nintegrate_method) is tuple:
544+
nintegrate_method, is_multidimensional = nintegrate_method
545+
else:
546+
is_multidimensional = False
467547

468548
domain = self.decompose_domain(domain, evaluation)
469549
if not domain:
@@ -472,12 +552,18 @@ def apply_2(self, func, domain, evaluation, options):
472552
domain = [domain]
473553

474554
coords = [axis[0] for axis in domain]
555+
# If any of the points in the integration domain is complex, stop the evaluation...
556+
if any([c.get_head_name() == "System`List" for c in coords]):
557+
evaluation.message("NIntegrate", "cmpint")
558+
return
559+
475560
intvars = Expression("List", *coords)
476561
integrand = Expression("Compile", intvars, func).evaluate(evaluation)
562+
477563
if len(integrand.leaves)>=3:
478564
integrand = integrand.leaves[2].cfunc
479565
else:
480-
print(func, " could not be compiled")
566+
evaluation.message("inumer", func, domain)
481567
return
482568
results = []
483569
for subdomain in product(*[axis[1] for axis in domain]):
@@ -508,28 +594,39 @@ def apply_2(self, func, domain, evaluation, options):
508594
coordtransform.append((np.arctanh, lambda u:1./(1.-u**2)))
509595
else:
510596
if not b.is_numeric():
511-
evaluation.message("Boundary is not numeric.")
512-
return SymbolFailed
597+
evaluation.message("nlim", coords[i], b)
598+
return
513599
z = a.leaves[0].value
514600
b = b.value
515601
subdomain2.append( [machine_epsilon, 1.])
516602
coordtransform.append((lambda u: b-z + z/u, lambda u: -z*u**(-2.)))
517603
elif b.get_head_name() == 'System`DirectedInfinity':
518604
if not a.is_numeric():
519-
evaluation.message("Boundary is not numeric.")
520-
return SymbolFailed
605+
evaluation.message("nlim", coords[i], a)
606+
return
521607
a = a.value
522608
z = b.leaves[0].value
523609
subdomain2.append([machine_epsilon, 1.])
524610
coordtransform.append((lambda u: a-z + z/u, lambda u: z*u**(-2.)))
525611
elif a.is_numeric() and b.is_numeric():
526-
a = Expression("N", a).evaluate(evaluation).value
527-
b = Expression("N", b).evaluate(evaluation).value
528-
subdomain2.append([a, b])
529-
coordtransform.append(None)
612+
if "System`Complex" in (a.get_head_name(), b.get_head_name()):
613+
evaluation.message("NIntegrate", "Integration over a complex domain is not implemented yet")
614+
return
615+
# a = a.to_python()
616+
# b = b.to_python()
617+
# dab = b - a
618+
# subdomain2.append([0, 1.])
619+
# coordtransform.append((lambda u: a + dab * u, lambda u: dab))
620+
else:
621+
a = Expression("N", a).evaluate(evaluation).value
622+
b = Expression("N", b).evaluate(evaluation).value
623+
subdomain2.append([a, b])
624+
coordtransform.append(None)
530625
else:
531-
evaluation.message("Boundary is not numeric.")
532-
return SymbolFailed
626+
for x in (a,b):
627+
if not x.is_numeric():
628+
evaluation.message("nlim", coords[i], x)
629+
return
533630

534631
if nulldomain:
535632
continue
@@ -539,19 +636,34 @@ def apply_2(self, func, domain, evaluation, options):
539636
for i, x in enumerate(coordtransform)])
540637
* np.prod([ jac[1](u[i] )
541638
for i, jac in enumerate(coordtransform) if jac]))
542-
opts = {#"" : accuracy,
639+
opts = {"acur" : accuracy,
543640
"tol" : tolerance,
544-
#"maxrec" : maxrecursion,
641+
"maxrec" : maxrecursion,
545642
}
546-
if len(subdomain2) >1:
547-
val = _fubini(func2, subdomain2,
643+
opts.update(method_options)
644+
try:
645+
if len(subdomain2) >1:
646+
if is_multidimensional:
647+
nintegrate_method(func2, subdomain2, **opts)
648+
else:
649+
val = _fubini(func2, subdomain2,
548650
integrator=nintegrate_method, **opts)
549-
else:
550-
val = nintegrate_method(func2, *(subdomain2[0]), **opts)
651+
else:
652+
val = nintegrate_method(func2, *(subdomain2[0]), **opts)
653+
except Exception as e:
654+
val = None
655+
656+
if val is None:
657+
evaluation.message("NIntegrate", "mtdfail" )
658+
if len(subdomain2) >1:
659+
val = _fubini(func2, subdomain2,
660+
integrator=_internal_adaptative_simpsons_rule, **opts)
661+
else:
662+
val = _internal_adaptative_simpsons_rule(func2, *(subdomain2[0]), **opts)
551663
results.append(val)
664+
552665
result = sum([r[0] for r in results])
553666
error = sum([r[1] for r in results])
554-
# print("result=", result, " error=",error)
555667
return from_python(result)
556668

557669
class MachinePrecision(Predefined):

test/test_nintegrate.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
5+
import unittest
6+
import pytest
7+
8+
9+
from mathics.session import MathicsSession
10+
11+
12+
session = MathicsSession()
13+
14+
try:
15+
import scipy.integrate
16+
usescipy = True
17+
except:
18+
usescipy = False
19+
20+
21+
if usescipy:
22+
methods = ["Automatic", "Romberg", "Internal", "NQuadrature"]
23+
24+
generic_tests_for_nintegrate = [
25+
(r'NIntegrate[x^2, {x,0,1}, {method} ]', r'1/3.', ''),
26+
(r'NIntegrate[x^2 y^(-1.+1/3.), {x,0,1},{y,0,1}, {method}]', r'1.', ''),
27+
]
28+
29+
tests_for_nintegrate = sum([
30+
[ (tst[0].replace("{method}", "Method->"+ method),
31+
tst[1],tst[2])
32+
for tst in generic_tests_for_nintegrate]
33+
for method in methods], [])
34+
else:
35+
tests_for_nintegrate = [(r'NIntegrate[x^2, {x,0,1}]', r'1/3.', ''),
36+
(r'NIntegrate[x^2 y^(-.5), {x,0,1},{y,0,1}]', r'1.', ''),
37+
]
38+
39+
@pytest.mark.parametrize(
40+
"str_expr, str_expected, msg",
41+
tests_for_nintegrate
42+
)
43+
def test_nintegrate(str_expr: str, str_expected: str, msg: str, message=""):
44+
result = session.evaluate(str_expr)
45+
expected = session.evaluate(str_expected)
46+
if msg:
47+
assert result == expected, msg
48+
else:
49+
assert result == expected
50+

0 commit comments

Comments
 (0)