Skip to content

Commit bdcd6f7

Browse files
committed
Added minimalistic code for gruntz algo
1 parent 1cfb65c commit bdcd6f7

File tree

2 files changed

+338
-1
lines changed

2 files changed

+338
-1
lines changed

integration_tests/gruntz_demo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ def limitinf(e, x):
343343
c0, e0 = mrv_leadterm(e, x)
344344
sig = signinf(e0, x)
345345
if sig == 1:
346-
return Integer(0)
346+
return S(0)
347347
if sig == -1:
348348
return signinf(c0, x)*oo
349349
if sig == 0:

integration_tests/gruntz_demo2.py

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
"""
2+
Limits
3+
======
4+
5+
Implemented according to the PhD thesis
6+
https://www.cybertester.com/data/gruntz.pdf, which contains very thorough
7+
descriptions of the algorithm including many examples. We summarize here
8+
the gist of it.
9+
10+
All functions are sorted according to how rapidly varying they are at
11+
infinity using the following rules. Any two functions f and g can be
12+
compared using the properties of L:
13+
14+
L=lim log|f(x)| / log|g(x)| (for x -> oo)
15+
16+
We define >, < ~ according to::
17+
18+
1. f > g .... L=+-oo
19+
20+
we say that:
21+
- f is greater than any power of g
22+
- f is more rapidly varying than g
23+
- f goes to infinity/zero faster than g
24+
25+
2. f < g .... L=0
26+
27+
we say that:
28+
- f is lower than any power of g
29+
30+
3. f ~ g .... L!=0, +-oo
31+
32+
we say that:
33+
- both f and g are bounded from above and below by suitable integral
34+
powers of the other
35+
36+
Examples
37+
========
38+
::
39+
2 < x < exp(x) < exp(x**2) < exp(exp(x))
40+
2 ~ 3 ~ -5
41+
x ~ x**2 ~ x**3 ~ 1/x ~ x**m ~ -x
42+
exp(x) ~ exp(-x) ~ exp(2x) ~ exp(x)**2 ~ exp(x+exp(-x))
43+
f ~ 1/f
44+
45+
So we can divide all the functions into comparability classes (x and x^2
46+
belong to one class, exp(x) and exp(-x) belong to some other class). In
47+
principle, we could compare any two functions, but in our algorithm, we
48+
do not compare anything below the class 2~3~-5 (for example log(x) is
49+
below this), so we set 2~3~-5 as the lowest comparability class.
50+
51+
Given the function f, we find the list of most rapidly varying (mrv set)
52+
subexpressions of it. This list belongs to the same comparability class.
53+
Let's say it is {exp(x), exp(2x)}. Using the rule f ~ 1/f we find an
54+
element "w" (either from the list or a new one) from the same
55+
comparability class which goes to zero at infinity. In our example we
56+
set w=exp(-x) (but we could also set w=exp(-2x) or w=exp(-3x) ...). We
57+
rewrite the mrv set using w, in our case {1/w, 1/w^2}, and substitute it
58+
into f. Then we expand f into a series in w::
59+
60+
f = c0*w^e0 + c1*w^e1 + ... + O(w^en), where e0<e1<...<en, c0!=0
61+
62+
but for x->oo, lim f = lim c0*w^e0, because all the other terms go to zero,
63+
because w goes to zero faster than the ci and ei. So::
64+
65+
for e0>0, lim f = 0
66+
for e0<0, lim f = +-oo (the sign depends on the sign of c0)
67+
for e0=0, lim f = lim c0
68+
69+
We need to recursively compute limits at several places of the algorithm, but
70+
as is shown in the PhD thesis, it always finishes.
71+
72+
Important functions from the implementation:
73+
74+
compare(a, b, x) compares "a" and "b" by computing the limit L.
75+
mrv(e, x) returns list of most rapidly varying (mrv) subexpressions of "e"
76+
rewrite(e, Omega, x, wsym) rewrites "e" in terms of w
77+
leadterm(f, x) returns the lowest power term in the series of f
78+
mrv_leadterm(e, x) returns the lead term (c0, e0) for e
79+
limitinf(e, x) computes lim e (for x->oo)
80+
limit(e, z, z0) computes any limit by converting it to the case x->oo
81+
82+
All the functions are really simple and straightforward except
83+
rewrite(), which is the most difficult/complex part of the algorithm.
84+
When the algorithm fails, the bugs are usually in the series expansion
85+
(i.e. in SymPy) or in rewrite.
86+
87+
This code is almost exact rewrite of the Maple code inside the Gruntz
88+
thesis.
89+
90+
Debugging
91+
---------
92+
93+
Because the gruntz algorithm is highly recursive, it's difficult to
94+
figure out what went wrong inside a debugger. Instead, turn on nice
95+
debug prints by defining the environment variable SYMPY_DEBUG. For
96+
example:
97+
98+
[user@localhost]: SYMPY_DEBUG=True ./bin/isympy
99+
100+
In [1]: limit(sin(x)/x, x, 0)
101+
limitinf(_x*sin(1/_x), _x) = 1
102+
+-mrv_leadterm(_x*sin(1/_x), _x) = (1, 0)
103+
| +-mrv(_x*sin(1/_x), _x) = set([_x])
104+
| | +-mrv(_x, _x) = set([_x])
105+
| | +-mrv(sin(1/_x), _x) = set([_x])
106+
| | +-mrv(1/_x, _x) = set([_x])
107+
| | +-mrv(_x, _x) = set([_x])
108+
| +-mrv_leadterm(exp(_x)*sin(exp(-_x)), _x, set([exp(_x)])) = (1, 0)
109+
| +-rewrite(exp(_x)*sin(exp(-_x)), set([exp(_x)]), _x, _w) = (1/_w*sin(_w), -_x)
110+
| +-sign(_x, _x) = 1
111+
| +-mrv_leadterm(1, _x) = (1, 0)
112+
+-sign(0, _x) = 0
113+
+-limitinf(1, _x) = 1
114+
115+
And check manually which line is wrong. Then go to the source code and
116+
debug this function to figure out the exact problem.
117+
118+
"""
119+
from functools import reduce
120+
121+
from sympy.core import Basic, S, Mul, PoleError, expand_mul, evaluate
122+
from sympy.core.cache import cacheit
123+
from sympy.core.numbers import I, oo
124+
from sympy.core.symbol import Dummy, Wild, Symbol
125+
from sympy.core.traversal import bottom_up
126+
from sympy.core.sorting import ordered
127+
128+
from sympy.functions import log, exp, sign, sin
129+
from sympy.series.order import Order
130+
from sympy.utilities.exceptions import SymPyDeprecationWarning
131+
from sympy.utilities.misc import debug_decorator as debug
132+
from sympy.utilities.timeutils import timethis
133+
134+
def mrv(e, x):
135+
"""
136+
Calculate the MRV set of the expression.
137+
138+
Examples
139+
========
140+
141+
>>> mrv(log(x - log(x))/log(x), x)
142+
{x}
143+
144+
"""
145+
146+
if e == x:
147+
return {x}
148+
if e.is_Mul or e.is_Add:
149+
a, b = e.as_two_terms()
150+
ans1 = mrv(a, x)
151+
ans2 = mrv(b, x)
152+
return mrv_max(mrv(a, x), mrv(b, x), x)
153+
if e.is_Pow:
154+
return mrv(e.base, x)
155+
if e.is_Function:
156+
return reduce(lambda a, b: mrv_max(a, b, x), (mrv(a, x) for a in e.args))
157+
raise NotImplementedError(f"Can't calculate the MRV of {e}.")
158+
159+
def mrv_max(f, g, x):
160+
"""Compute the maximum of two MRV sets.
161+
162+
Examples
163+
========
164+
165+
>>> mrv_max({log(x)}, {x**5}, x)
166+
{x**5}
167+
168+
"""
169+
170+
if not f:
171+
return g
172+
if not g:
173+
return f
174+
if f & g:
175+
return f | g
176+
177+
def rewrite(e, x, w):
178+
r"""
179+
Rewrites the expression in terms of the MRV subexpression.
180+
181+
Parameters
182+
==========
183+
184+
e : Expr
185+
an expression
186+
x : Symbol
187+
variable of the `e`
188+
w : Symbol
189+
The symbol which is going to be used for substitution in place
190+
of the MRV in `x` subexpression.
191+
192+
Returns
193+
=======
194+
195+
tuple
196+
A pair: rewritten (in `w`) expression and `\log(w)`.
197+
198+
Examples
199+
========
200+
201+
>>> rewrite(exp(x)*log(x), x, y)
202+
(log(x)/y, -x)
203+
204+
"""
205+
206+
Omega = mrv(e, x)
207+
if not Omega:
208+
return e, None # e really does not depend on x
209+
210+
if x in Omega:
211+
# Moving up in the asymptotical scale:
212+
with evaluate(False):
213+
e = e.xreplace({x: exp(x)})
214+
Omega = {s.xreplace({x: exp(x)}) for s in Omega}
215+
216+
Omega = list(ordered(Omega, keys=lambda a: -len(mrv(a, x))))
217+
218+
for g in Omega:
219+
sig = signinf(g.exp, x)
220+
if sig not in (1, -1):
221+
raise NotImplementedError(f'Result depends on the sign of {sig}.')
222+
223+
if sig == 1:
224+
w = 1/w # if g goes to oo, substitute 1/w
225+
226+
# Rewrite and substitute subexpressions in the Omega.
227+
for a in Omega:
228+
c = limitinf(a.exp/g.exp, x)
229+
b = exp(a.exp - c*g.exp)*w**c # exponential must never be expanded here
230+
with evaluate(False):
231+
e = e.xreplace({a: b})
232+
233+
return e, -sig*g.exp
234+
235+
@cacheit
236+
def mrv_leadterm(e, x):
237+
"""
238+
Compute the leading term of the series.
239+
240+
Returns
241+
=======
242+
243+
tuple
244+
The leading term `c_0 w^{e_0}` of the series of `e` in terms
245+
of the most rapidly varying subexpression `w` in form of
246+
the pair ``(c0, e0)`` of Expr.
247+
248+
Examples
249+
========
250+
251+
>>> leadterm(1/exp(-x + exp(-x)) - exp(x), x)
252+
(-1, 0)
253+
254+
"""
255+
256+
w = Dummy('w', real=True, positive=True)
257+
e, logw = rewrite(e, x, w)
258+
return e.leadterm(w)
259+
260+
@cacheit
261+
def signinf(e, x):
262+
r"""
263+
Determine sign of the expression at the infinity.
264+
265+
Returns
266+
=======
267+
268+
{1, 0, -1}
269+
One or minus one, if `e > 0` or `e < 0` for `x` sufficiently
270+
large and zero if `e` is *constantly* zero for `x\to\infty`.
271+
272+
"""
273+
274+
if not e.has(x):
275+
return sign(e)
276+
if e == x or (e.is_Pow and signinf(e.base, x) == 1):
277+
return S(1)
278+
279+
@cacheit
280+
def limitinf(e, x):
281+
"""
282+
Compute the limit of the expression at the infinity.
283+
284+
Examples
285+
========
286+
287+
>>> limitinf(exp(x)*(exp(1/x - exp(-x)) - exp(1/x)), x)
288+
-1
289+
290+
"""
291+
292+
if not e.has(x):
293+
return e
294+
295+
c0, e0 = mrv_leadterm(e, x)
296+
sig = signinf(e0, x)
297+
if sig == 1:
298+
return Integer(0)
299+
if sig == -1:
300+
return signinf(c0, x)*oo
301+
if sig == 0:
302+
return limitinf(c0, x)
303+
raise NotImplementedError(f'Result depends on the sign of {sig}.')
304+
305+
306+
def gruntz(e, z, z0, dir="+"):
307+
"""
308+
Compute the limit of e(z) at the point z0 using the Gruntz algorithm.
309+
310+
Explanation
311+
===========
312+
313+
``z0`` can be any expression, including oo and -oo.
314+
315+
For ``dir="+"`` (default) it calculates the limit from the right
316+
(z->z0+) and for ``dir="-"`` the limit from the left (z->z0-). For infinite z0
317+
(oo or -oo), the dir argument does not matter.
318+
319+
This algorithm is fully described in the module docstring in the gruntz.py
320+
file. It relies heavily on the series expansion. Most frequently, gruntz()
321+
is only used if the faster limit() function (which uses heuristics) fails.
322+
"""
323+
324+
if str(dir) == "-":
325+
e0 = e.subs(z, z0 - 1/z)
326+
elif str(dir) == "+":
327+
e0 = e.subs(z, z0 + 1/z)
328+
else:
329+
raise NotImplementedError("dir must be '+' or '-'")
330+
331+
r = limitinf(e0, z)
332+
return r
333+
334+
# tests
335+
x = Symbol('x')
336+
ans = gruntz(sin(x)/x, x, 0)
337+
print(ans)

0 commit comments

Comments
 (0)