Skip to content

Commit aa3331d

Browse files
committed
trying to improve performance for NumericQ/NumberQ tests
adding benchmark Negative avoids to call `evaluate(evaluation)` by calling the `Less.sapply` method directly. Positive 'Positive[Sqrt[2]]' 100 loops, avg: 19.7 ms, best: 18.6 ms, median: 19.5 ms per loop 'Positive[Sqrt[-2]]' 100 loops, avg: 31.5 ms, best: 28.9 ms, median: 29.8 ms per loop 'Positive[Sqrt[2.]]' 5000 loops, avg: 949 us, best: 825 us, median: 890 us per loop 'Positive[Sqrt[-2.]]' 5000 loops, avg: 956 us, best: 820 us, median: 887 us per loop 'Positive[q]' 10000 loops, avg: 164 us, best: 141 us, median: 147 us per loop 'Positive["q"]' 10000 loops, avg: 159 us, best: 137 us, median: 143 us per loop Negative 'Negative[Sqrt[2]]' 100 loops, avg: 19.7 ms, best: 18.9 ms, median: 19.5 ms per loop 'Negative[Sqrt[-2]]' 100 loops, avg: 31.8 ms, best: 29.2 ms, median: 30.3 ms per loop 'Negative[Sqrt[2.]]' 5000 loops, avg: 954 us, best: 836 us, median: 901 us per loop 'Negative[Sqrt[-2.]]' 5000 loops, avg: 952 us, best: 832 us, median: 898 us per loop 'Negative[q]' 10000 loops, avg: 165 us, best: 145 us, median: 150 us per loop 'Negative["q"]' 10000 loops, avg: 159 us, best: 140 us, median: 144 us per loop NumericQ 'NumericQ[Sqrt[2]]' 5000 loops, avg: 987 us, best: 863 us, median: 934 us per loop 'NumericQ[Sqrt[-2]]' 1000 loops, avg: 1.68 ms, best: 1.47 ms, median: 1.65 ms per loop 'NumericQ[Sqrt[2.]]' 5000 loops, avg: 917 us, best: 802 us, median: 862 us per loop 'NumericQ[Sqrt[-2.]]' 5000 loops, avg: 948 us, best: 810 us, median: 878 us per loop 'NumericQ[q]' 10000 loops, avg: 81.5 us, best: 71.2 us, median: 73.2 us per loop 'NumericQ["q"]' 10000 loops, avg: 77.2 us, best: 67.8 us, median: 69.6 us per loop fixup: Format Python code with Black Now `expression.is_numeric` accepts an `evaluation` object as an argument and `NumericQ` calls directly that method. Positive 'Positive[Sqrt[2]]' 100 loops, avg: 19.4 ms, best: 18.4 ms, median: 19.2 ms per loop 'Positive[Sqrt[-2]]' 100 loops, avg: 31.5 ms, best: 28.7 ms, median: 29.8 ms per loop 'Positive[Sqrt[2.]]' 5000 loops, avg: 948 us, best: 822 us, median: 890 us per loop 'Positive[Sqrt[-2.]]' 5000 loops, avg: 943 us, best: 813 us, median: 882 us per loop 'Positive[q]' 10000 loops, avg: 160 us, best: 138 us, median: 144 us per loop 'Positive["q"]' 10000 loops, avg: 157 us, best: 133 us, median: 139 us per loop Negative 'Negative[Sqrt[2]]' 100 loops, avg: 19.4 ms, best: 18.3 ms, median: 19.1 ms per loop 'Negative[Sqrt[-2]]' 100 loops, avg: 31.5 ms, best: 28.6 ms, median: 29.6 ms per loop 'Negative[Sqrt[2.]]' 5000 loops, avg: 951 us, best: 813 us, median: 890 us per loop 'Negative[Sqrt[-2.]]' 5000 loops, avg: 928 us, best: 807 us, median: 876 us per loop 'Negative[q]' 10000 loops, avg: 87.2 us, best: 75.9 us, median: 78.4 us per loop 'Negative["q"]' 10000 loops, avg: 84 us, best: 73.4 us, median: 75.7 us per loop NumericQ 'NumericQ[Sqrt[2]]' 1000 loops, avg: 1.01 ms, best: 866 us, median: 942 us per loop 'NumericQ[Sqrt[-2]]' 1000 loops, avg: 1.66 ms, best: 1.45 ms, median: 1.62 ms per loop 'NumericQ[Sqrt[2.]]' 5000 loops, avg: 904 us, best: 787 us, median: 851 us per loop 'NumericQ[Sqrt[-2.]]' 5000 loops, avg: 912 us, best: 794 us, median: 859 us per loop 'NumericQ[q]' 10000 loops, avg: 78 us, best: 69.7 us, median: 71.4 us per loop 'NumericQ["q"]' 10000 loops, avg: 75.5 us, best: 66.2 us, median: 68.3 us per loop listing changes
1 parent d929af3 commit aa3331d

21 files changed

+123
-68
lines changed

CHANGES.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
CHANGES
22
=======
33

4+
Internals
5+
=========
6+
7+
* now `Expression.is_numeric()` accepts an `Evaluation` object as a parameter,
8+
to use the definitions.
9+
* To numerify expressions, the function `_numeric_evaluation_with_prec` was introduced in the module `mathics.builtin.numeric` to avoid the idiom
10+
`Expression("N", expr, prec).evaluate(evaluation)`. The idea is to avoid when it is possible to call the Pattern matching routines to obtain the numeric value of an expression.
11+
* `Positive`, `Negative`, `NonPositive` and `NonNegative` do not check their arguments using the Pattern matching mechanism, but inside the Python code.
12+
* special cases for `quick_pattern_test` were reduced as much as possible.
13+
* A bug comming from a failure in the order in which `mathics.core.definitions` stores the rules was fixed.
14+
15+
416
4.0.1
517
-----
618

mathics/benchmark.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,41 @@
2626

2727
# Mathics expressions to benchmark
2828
BENCHMARKS = {
29+
"NumericQ": [
30+
"NumericQ[Sqrt[2]]",
31+
"NumericQ[Sqrt[-2]]",
32+
"NumericQ[Sqrt[2.]]",
33+
"NumericQ[Sqrt[-2.]]",
34+
"NumericQ[q]",
35+
'NumericQ["q"]',
36+
],
37+
# This function checks for numericQ in ints argument before calling
38+
"Positive": [
39+
"Positive[Sqrt[2]]",
40+
"Positive[Sqrt[-2]]",
41+
"Positive[Sqrt[2.]]",
42+
"Positive[Sqrt[-2.]]",
43+
"Positive[q]",
44+
'Positive["q"]',
45+
],
46+
# This function uses the WL rules definition, like in master
47+
"NonNegative": [
48+
"NonNegative[Sqrt[2]]",
49+
"NonNegative[Sqrt[-2]]",
50+
"NonNegative[Sqrt[2.]]",
51+
"NonNegative[Sqrt[-2.]]",
52+
"NonNegative[q]",
53+
'NonNegative["q"]',
54+
],
55+
# This function does the check inside the method.
56+
"Negative": [
57+
"Negative[Sqrt[2]]",
58+
"Negative[Sqrt[-2]]",
59+
"Negative[Sqrt[2.]]",
60+
"Negative[Sqrt[-2.]]",
61+
"Negative[q]",
62+
'Negative["q"]',
63+
],
2964
"Arithmetic": ["1 + 2", "5 * 3"],
3065
"Plot": [
3166
"Plot[0, {x, -3, 3}]",

mathics/builtin/arithmetic.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from mathics.core.convert import from_sympy, SympyExpression, sympy_symbol_prefix
4848
from mathics.builtin.scoping import dynamic_scoping
4949
from mathics.builtin.inference import get_assumptions_list, evaluate_predicate
50+
from mathics.builtin.numeric import _numeric_evaluation_with_prec
5051

5152

5253
@lru_cache(maxsize=1024)
@@ -133,7 +134,7 @@ def apply(self, z, evaluation):
133134
prec = min_prec(*args)
134135
d = dps(prec)
135136
args = [
136-
Expression(SymbolN, arg, Integer(d)).evaluate(evaluation)
137+
_numeric_evaluation_with_prec(arg, evaluation, Integer(d))
137138
for arg in args
138139
]
139140
with mpmath.workprec(prec):

mathics/builtin/base.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -527,11 +527,13 @@ def __init__(self, *args, **kwargs):
527527
class Test(Builtin):
528528
def apply(self, expr, evaluation) -> Symbol:
529529
"%(name)s[expr_]"
530-
531-
if self.test(expr):
530+
tst = self.test(expr)
531+
if tst:
532532
return SymbolTrue
533-
else:
533+
elif tst is False:
534534
return SymbolFalse
535+
else:
536+
return
535537

536538

537539
class SympyFunction(SympyObject):

mathics/builtin/comparison.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ class _InequalityOperator(BinaryOperator):
198198
def numerify_args(items, evaluation):
199199
items_sequence = items.get_sequence()
200200
all_numeric = all(
201-
item.is_numeric() and item.get_precision() is None
201+
item.is_numeric(evaluation) and item.get_precision() is None
202202
for item in items_sequence
203203
)
204204

mathics/builtin/datentime.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ def apply_any(self, args, evaluation, options):
630630
timezone = Real(-time.timezone / 3600.0)
631631
else:
632632
timezone = options["System`TimeZone"].evaluate(evaluation)
633-
if not timezone.is_numeric():
633+
if not timezone.is_numeric(evaluation):
634634
evaluation.message("DateObject", "notz", timezone)
635635

636636
# TODO: if tz != timezone, shift the datetime list.
@@ -1123,7 +1123,7 @@ def apply_2(self, expr, t, evaluation):
11231123
def apply_3(self, expr, t, failexpr, evaluation):
11241124
"TimeConstrained[expr_, t_, failexpr_]"
11251125
t = t.evaluate(evaluation)
1126-
if not t.is_numeric():
1126+
if not t.is_numeric(evaluation):
11271127
evaluation.message("TimeConstrained", "timc", t)
11281128
return
11291129
try:

mathics/builtin/inference.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def logical_expand_assumptions(assumptions_list, evaluation):
156156
evaluation.message("Assumption", "faas")
157157
changed = True
158158
continue
159-
if assumption.is_numeric():
159+
if assumption.is_numeric(evaluation):
160160
evaluation.message("Assumption", "baas")
161161
changed = True
162162
continue
@@ -306,7 +306,7 @@ def get_assumption_rules_dispatch(evaluation):
306306
if pat.has_form("Equal", 2):
307307
if value:
308308
lhs, rhs = pat._leaves
309-
if lhs.is_numeric():
309+
if lhs.is_numeric(evaluation):
310310
assumption_rules.append(Rule(rhs, lhs))
311311
else:
312312
assumption_rules.append(Rule(lhs, rhs))

mathics/builtin/intfns/divlike.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ class QuotientRemainder(Builtin):
367367

368368
def apply(self, m, n, evaluation):
369369
"QuotientRemainder[m_, n_]"
370-
if m.is_numeric() and n.is_numeric():
370+
if m.is_numeric(evaluation) and n.is_numeric():
371371
py_m = m.to_python()
372372
py_n = n.to_python()
373373
if py_n == 0:

mathics/builtin/lists.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2704,7 +2704,7 @@ def convert_vectors(p):
27042704
raise _IllegalDataPoint
27052705
yield v
27062706

2707-
if dist_p[0].is_numeric():
2707+
if dist_p[0].is_numeric(evaluation):
27082708
numeric_p = [[x] for x in convert_scalars(dist_p)]
27092709
else:
27102710
numeric_p = list(convert_vectors(dist_p))

mathics/builtin/moments/basic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def apply(self, l, evaluation):
4949
return self.rect(l)
5050
except _NotRectangularException:
5151
evaluation.message("Median", "rectn", Expression("Median", l))
52-
elif all(leaf.is_numeric() for leaf in l.leaves):
52+
elif all(leaf.is_numeric(evaluation) for leaf in l.leaves):
5353
v = l.get_mutable_leaves() # copy needed for introselect
5454
n = len(v)
5555
if n % 2 == 0: # even number of elements?

mathics/builtin/numbers/algebra.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1476,7 +1476,7 @@ def coeff_power_internal(self, expr, var_exprs, filt, evaluation, form="expr"):
14761476
def key_powers(lst):
14771477
key = Expression("Plus", *lst)
14781478
key = key.evaluate(evaluation)
1479-
if key.is_numeric():
1479+
if key.is_numeric(evaluation):
14801480
return key.to_python()
14811481
return 0
14821482

mathics/builtin/numbers/calculus.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
SymbolTrue,
1919
SymbolFalse,
2020
SymbolList,
21-
SymbolN,
2221
SymbolRule,
2322
SymbolUndefined,
2423
from_python,
@@ -27,6 +26,7 @@
2726
from mathics.core.rules import Pattern
2827
from mathics.core.numbers import dps
2928
from mathics.builtin.scoping import dynamic_scoping
29+
from mathics.builtin.numeric import _numeric_evaluation_with_prec
3030
from mathics import Symbol
3131

3232
import sympy
@@ -1280,9 +1280,8 @@ def sub(evaluation):
12801280
# TODO: use Precision goal...
12811281
if x1 == x0:
12821282
break
1283-
x0 = Expression(SymbolN, x1).evaluate(
1284-
evaluation
1285-
) # N required due to bug in sympy arithmetic
1283+
x0 = _numeric_evaluation_with_prec(x1, evaluation)
1284+
# N required due to bug in sympy arithmetic
12861285
count += 1
12871286
else:
12881287
evaluation.message("FindRoot", "maxiter")
@@ -1377,7 +1376,7 @@ class FindRoot(Builtin):
13771376
def apply(self, f, x, x0, evaluation, options):
13781377
"FindRoot[f_, {x_, x0_}, OptionsPattern[]]"
13791378
# First, determine x0 and x
1380-
x0 = Expression(SymbolN, x0).evaluate(evaluation)
1379+
x0 = _numeric_evaluation_with_prec(x0, evaluation)
13811380
if not isinstance(x0, Number):
13821381
evaluation.message("FindRoot", "snum", x0)
13831382
return
@@ -1545,7 +1544,7 @@ def apply_makeboxes(self, x, x0, data, nmin, nmax, den, form, evaluation):
15451544

15461545
expansion = []
15471546
for i, leaf in enumerate(data.leaves):
1548-
if leaf.is_numeric() and leaf.is_zero:
1547+
if leaf.is_numeric(evaluation) and leaf.is_zero:
15491548
continue
15501549
if powers[i].is_zero:
15511550
expansion.append(leaf)

mathics/builtin/numbers/numbertheory.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
Rational,
1717
Symbol,
1818
from_python,
19-
SymbolN,
2019
)
2120
from mathics.core.convert import from_sympy, SympyPrime
2221
import mpmath
2322

23+
from mathics.builtin.numeric import _numeric_evaluation_with_prec
24+
2425

2526
class ContinuedFraction(SympyFunction):
2627
"""
@@ -412,13 +413,13 @@ def apply(self, n, b, evaluation):
412413
return expr
413414

414415
if n_sympy.is_constant():
415-
temp_n = Expression(SymbolN, n).evaluate(evaluation)
416+
temp_n = _numeric_evaluation_with_prec(n, evaluation)
416417
py_n = temp_n.to_python()
417418
else:
418419
return expr
419420

420421
if b_sympy.is_constant():
421-
temp_b = Expression(SymbolN, b).evaluate(evaluation)
422+
temp_b = _numeric_evaluation_with_prec(b, evaluation)
422423
py_b = temp_b.to_python()
423424
else:
424425
return expr
@@ -443,7 +444,7 @@ def apply_2(self, n, evaluation):
443444
return expr
444445
# Handle Input with special cases such as PI and E
445446
if n_sympy.is_constant():
446-
temp_n = Expression(SymbolN, n).evaluate(evaluation)
447+
temp_n = _numeric_evaluation_with_prec(n, evaluation)
447448
py_n = temp_n.to_python()
448449
else:
449450
return expr

mathics/builtin/numbers/randomnumbers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ class _RandomBase(Builtin):
251251

252252
def _size_to_python(self, domain, size, evaluation):
253253
is_proper_spec = size.get_head_name() == "System`List" and all(
254-
n.is_numeric() for n in size.leaves
254+
n.is_numeric(evaluation) for n in size.leaves
255255
)
256256

257257
py_size = size.to_python() if is_proper_spec else None
@@ -589,7 +589,7 @@ def _weights_to_python(self, weights, evaluation):
589589
# we need to normalize weights as numpy.rand.randchoice expects this and as we can limit
590590
# accuracy problems with very large or very small weights by normalizing with sympy
591591
is_proper_spec = weights.get_head_name() == "System`List" and all(
592-
w.is_numeric() for w in weights.leaves
592+
w.is_numeric(evaluation) for w in weights.leaves
593593
)
594594

595595
if (
@@ -599,7 +599,7 @@ def _weights_to_python(self, weights, evaluation):
599599
"Divide", weights, Expression("Total", weights)
600600
).evaluate(evaluation)
601601
if norm_weights is None or not all(
602-
w.is_numeric() for w in norm_weights.leaves
602+
w.is_numeric(evaluation) for w in norm_weights.leaves
603603
):
604604
return evaluation.message(self.get_name(), "wghtv", weights), None
605605
weights = norm_weights

mathics/builtin/numeric.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
SymbolFalse,
4343
SymbolTrue,
4444
SymbolList,
45+
SymbolMachinePrecision,
4546
SymbolN,
4647
from_python,
4748
)
@@ -61,6 +62,10 @@ def log_n_b(py_n, py_b) -> int:
6162
return int(mpmath.ceil(mpmath.log(py_n, py_b))) if py_n != 0 and py_n != 1 else 1
6263

6364

65+
def _numeric_evaluation_with_prec(expression, evaluation, prec=SymbolMachinePrecision):
66+
return Expression("N", expression, prec).evaluate(evaluation)
67+
68+
6469
def _scipy_interface(integrator, options_map, mandatory=None, adapt_func=None):
6570
"""
6671
This function provides a proxy for scipy.integrate
@@ -1126,7 +1131,7 @@ def apply_with_func_domain(self, func, domain, evaluation, options):
11261131
(np.arctanh, lambda u: 1.0 / (1.0 - u ** 2))
11271132
)
11281133
else:
1129-
if not b.is_numeric():
1134+
if not b.is_numeric(evaluation):
11301135
evaluation.message("nlim", coords[i], b)
11311136
return
11321137
z = a.leaves[0].value
@@ -1136,7 +1141,7 @@ def apply_with_func_domain(self, func, domain, evaluation, options):
11361141
(lambda u: b - z + z / u, lambda u: -z * u ** (-2.0))
11371142
)
11381143
elif b.get_head_name() == "System`DirectedInfinity":
1139-
if not a.is_numeric():
1144+
if not a.is_numeric(evaluation):
11401145
evaluation.message("nlim", coords[i], a)
11411146
return
11421147
a = a.value
@@ -1145,14 +1150,14 @@ def apply_with_func_domain(self, func, domain, evaluation, options):
11451150
coordtransform.append(
11461151
(lambda u: a - z + z / u, lambda u: z * u ** (-2.0))
11471152
)
1148-
elif a.is_numeric() and b.is_numeric():
1153+
elif a.is_numeric(evaluation) and b.is_numeric(evaluation):
11491154
a = Expression(SymbolN, a).evaluate(evaluation).value
11501155
b = Expression(SymbolN, b).evaluate(evaluation).value
11511156
subdomain2.append([a, b])
11521157
coordtransform.append(None)
11531158
else:
11541159
for x in (a, b):
1155-
if not x.is_numeric():
1160+
if not x.is_numeric(evaluation):
11561161
evaluation.message("nlim", coords[i], x)
11571162
return
11581163

@@ -1228,17 +1233,7 @@ class NumericQ(Builtin):
12281233

12291234
def apply(self, expr, evaluation):
12301235
"NumericQ[expr_]"
1231-
1232-
def test(expr):
1233-
if isinstance(expr, Expression):
1234-
attr = evaluation.definitions.get_attributes(expr.head.get_name())
1235-
return "System`NumericFunction" in attr and all(
1236-
test(leaf) for leaf in expr.leaves
1237-
)
1238-
else:
1239-
return expr.is_numeric()
1240-
1241-
return SymbolTrue if test(expr) else SymbolFalse
1236+
return SymbolTrue if expr.is_numeric(evaluation) else SymbolFalse
12421237

12431238

12441239
class Precision(Builtin):
@@ -1648,7 +1643,7 @@ def apply(self, n, evaluation):
16481643
if isinstance(n, Symbol) and n.name.startswith("System`"):
16491644
return evaluation.message("RealDigits", "ndig", n)
16501645

1651-
if n.is_numeric():
1646+
if n.is_numeric(evaluation):
16521647
return self.apply_with_base(n, from_python(10), evaluation)
16531648

16541649
def apply_with_base(self, n, b, evaluation, nr_elements=None, pos=None):

mathics/builtin/patterns.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ def apply_list(self, expr, rules, evaluation, options):
358358
return rules
359359

360360
maxit = self.get_option(options, "MaxIterations", evaluation)
361-
if maxit.is_numeric():
361+
if maxit.is_numeric(evaluation):
362362
maxit = maxit.get_int_value()
363363
else:
364364
maxit = -1

mathics/builtin/procedural.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ def apply(self, f, expr, n, evaluation, options):
433433

434434
if count is None:
435435
count = self.get_option(options, "MaxIterations", evaluation)
436-
if count.is_numeric():
436+
if count.is_numeric(evaluation):
437437
count = count.get_int_value()
438438
else:
439439
count = None

mathics/builtin/recurrence.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def is_relation(eqn):
100100
left.get_head_name() == func.get_head_name()
101101
and len(left.leaves) == 1 # noqa
102102
and isinstance(l.leaves[0].to_python(), int)
103-
and r.is_numeric()
103+
and r.is_numeric(evaluation)
104104
):
105105

106106
r_sympy = r.to_sympy()

0 commit comments

Comments
 (0)