Skip to content

Adds limit(expr, v, a) syntax #39812

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 11, 2025
272 changes: 220 additions & 52 deletions src/sage/calculus/calculus.py
Original file line number Diff line number Diff line change
Expand Up @@ -1156,28 +1156,45 @@
###################################################################
# limits
###################################################################
def limit(ex, dir=None, taylor=False, algorithm='maxima', **argv):
def limit(ex, *args, dir=None, taylor=False, algorithm='maxima', **kwargs):
r"""
Return the limit as the variable `v` approaches `a`
from the given direction.

::
SYNTAX:

expr.limit(x = a)
expr.limit(x = a, dir='+')
There are two ways of invoking limit. One can write
``limit(expr, x=a, <keywords>)`` or ``limit(expr, x, a, <keywords>)``.
In the first option, ``x`` must be a valid Python identifier. Its
string representation is used to create the corresponding symbolic
variable with respect to which to take the limit. In the second
option, ``x`` can simply be a symbolic variable. For symbolic
variables that do not have a string representation that is a valid
Python identifier (for instance, if ``x`` is an indexed symbolic
variable), the second option is required.

INPUT:

- ``dir`` -- (default: ``None``) may have the value
- ``ex`` -- the expression whose limit is computed. Must be convertible
to a symbolic expression.
- ``v`` -- The variable for the limit. Required for the
``limit(expr, v, a)`` syntax. Must be convertible to a symbolic
variable.
- ``a`` -- The value the variable approaches. Required for the
``limit(expr, v, a)`` syntax. Must be convertible to a symbolic
expression.
- ``dir`` -- (default: ``None``) direction for the limit:
``'plus'`` (or ``'+'`` or ``'right'`` or ``'above'``) for a limit from above,
``'minus'`` (or ``'-'`` or ``'left'`` or ``'below'``) for a limit from below, or may be omitted
(implying a two-sided limit is to be computed).

``'minus'`` (or ``'-'`` or ``'left'`` or ``'below'``) for a limit from below.
Omitted (``None``) implies a two-sided limit.
- ``taylor`` -- (default: ``False``) if ``True``, use Taylor
series, which allows more limits to be computed (but may also
crash in some obscure cases due to bugs in Maxima).

- ``**argv`` -- 1 named parameter
series via Maxima (may handle more cases but potentially less stable).
Setting this automatically uses the ``'maxima_taylor'`` algorithm.
- ``algorithm`` -- (default: ``'maxima'``) the backend algorithm to use.
Options include ``'maxima'``, ``'maxima_taylor'``, ``'sympy'``,
``'giac'``, ``'fricas'``, ``'mathematica_free'``.
- ``**kwargs`` -- (optional) single named parameter. Required for the
``limit(expr, v=a)`` syntax to specify variable and limit point.

.. NOTE::

Expand All @@ -1189,15 +1206,81 @@

sage: x = var('x')
sage: f = (1 + 1/x)^x
sage: f.limit(x=oo)
sage: limit(f, x=oo)
e
sage: limit(f, x, oo)
e
sage: f.limit(x=5)
7776/3125
sage: f.limit(x, 5)
7776/3125

The positional ``limit(expr, v, a)`` syntax is particularly useful
when the limit variable ``v`` is an indexed variable or another
expression that cannot be used as a keyword argument
(fixes :issue:`38761`)::

sage: y = var('y', n=3)
sage: g = sum(y); g
y0 + y1 + y2
sage: limit(g, y[1], 1)
y0 + y2 + 1
sage: g.limit(y[0], 5)
y1 + y2 + 5
sage: limit(y[0]^2 + y[1], y[0], y[2]) # Limit as y0 -> y2
y2^2 + y1

Directional limits work with both syntaxes::

sage: limit(1/x, x, 0, dir='+')
+Infinity
sage: limit(1/x, x=0, dir='-')
-Infinity
sage: limit(exp(-1/x), x, 0, dir='left')
+Infinity

Using different algorithms::

sage: limit(sin(x)/x, x, 0, algorithm='sympy')
1
sage: limit(abs(x)/x, x, 0, algorithm='giac') # needs sage.libs.giac # Two-sided limit -> undefined
und
sage: limit(x^x, x, 0, dir='+', algorithm='fricas') # optional - fricas
1

Using Taylor series (can sometimes handle more complex limits)::

sage: limit((cos(x)-1)/x^2, x, 0, taylor=True)
-1/2

Error handling for incorrect syntax::

sage: limit(sin(x)/x, x=0, y=1) # Too many keyword args
Traceback (most recent call last):
...
ValueError: multiple keyword arguments specified
sage: limit(sin(x)/x, x, 0, y=1) # Mixed positional (v,a) and keyword variable
Traceback (most recent call last):
...
ValueError: cannot mix positional specification of limit variable and point with keyword variable arguments
sage: limit(sin(x)/x, x) # Not enough positional args
Traceback (most recent call last):
...
ValueError: three positional arguments (expr, v, a) or one positional and one keyword argument (expr, v=a) required
sage: limit(sin(x)/x) # No variable specified
Traceback (most recent call last):
...
ValueError: invalid limit specification
sage: limit(sin(x)/x, x, 0, x=0) # Mixing both syntaxes
Traceback (most recent call last):
...
ValueError: cannot mix positional specification of limit variable and point with keyword variable arguments

Domain to real, a regression in 5.46.0, see https://sf.net/p/maxima/bugs/4138 ::

sage: maxima_calculus.eval("domain:real")
...
sage: f = (1 + 1/x)^x
sage: f.limit(x=1.2).n()
2.06961575467...
sage: maxima_calculus.eval("domain:complex");
Expand Down Expand Up @@ -1326,8 +1409,7 @@
sage: lim(x^2, x=2, dir='nugget')
Traceback (most recent call last):
...
ValueError: dir must be one of None, 'plus', '+', 'above', 'right',
'minus', '-', 'below', 'left'
ValueError: dir must be one of None, 'plus', '+', 'above', 'right', 'minus', '-', 'below', 'left'

sage: x.limit(x=3, algorithm='nugget')
Traceback (most recent call last):
Expand Down Expand Up @@ -1384,6 +1466,7 @@
sage: sequence = -(3*n^2 + 1)*(-1)^n / sqrt(n^5 + 8*n^3 + 8)
sage: limit(sequence, n=infinity)
0
sage: forget() # Clean up assumption

Check if :issue:`23048` is fixed::

Expand All @@ -1410,76 +1493,159 @@

sage: limit(x / (x + 2^x + cos(x)), x=-infinity)
1

# Added specific tests for argument parsing logic to ensure coverage
sage: limit(x+1, x=1)
2
sage: limit(x+1, x, 1)
2
sage: limit(x+1, 'x', 1)
2
sage: limit(x+1, v=x, a=1) # using v=, a= keywords triggers multiple keyword error
Traceback (most recent call last):
...
ValueError: multiple keyword arguments specified
sage: limit(x+1, v=x, a=1, algorithm='sympy') # as above
Traceback (most recent call last):
...
ValueError: multiple keyword arguments specified
sage: limit(x+1, x=1, algorithm='sympy')
2
sage: limit(x+1, x, 1, algorithm='sympy')
2

# Test that var() is not called unnecessarily on symbolic input v
sage: y = var('y', n=3)
sage: limit(sum(y), y[1], 1) # Should work directly
y0 + y2 + 1

# Test conversion of v if not symbolic
sage: limit(x**2, 'x', 3)
9
sage: y = var('y')
sage: limit(x**2 + y, "y", x) # Need y=var('y') defined for this test
x^2 + x

# Test conversion of a if not symbolic
sage: limit(x**2, x, "3")
9

# Test using a constant number as variable 'v' fails
sage: limit(x**2 + 5, 5, 10)
Traceback (most recent call last):
...
TypeError: limit variable must be a variable, not a constant
"""
# Process expression
if not isinstance(ex, Expression):
ex = SR(ex)

if len(argv) != 1:
raise ValueError("call the limit function like this, e.g. limit(expr, x=2).")
else:
k, = argv.keys()
v = var(k)
a = argv[k]

# Argument parsing: Determining v and a based on syntax used
v = None
a = None

if len(args) == 2: # Syntax: limit(ex, v, a, ...)
if kwargs: # Cannot mix positional v, a with keyword args
raise ValueError("cannot mix positional specification of limit variable and point with keyword variable arguments")
v = args[0]
a = args[1]
elif len(args) == 1:
if kwargs:
raise ValueError("cannot mix positional specification of limit variable and point with keyword variable arguments")

Check warning on line 1554 in src/sage/calculus/calculus.py

View check run for this annotation

Codecov / codecov/patch

src/sage/calculus/calculus.py#L1554

Added line #L1554 was not covered by tests
else:
raise ValueError("three positional arguments (expr, v, a) or one positional and one keyword argument (expr, v=a) required")
elif len(args) == 0: # Potential syntax: limit(ex, v=a, ...) or limit(ex)
if len(kwargs) == 1:
k, = kwargs.keys()
v = var(k)
a = kwargs[k]
elif len(kwargs) == 0: # For No variable specified at all
raise ValueError("invalid limit specification")
else: # For Multiple keyword arguments like x=1, y=2
raise ValueError("multiple keyword arguments specified")

# Ensuring v is a symbolic expression and a valid limit variable
if not isinstance(v, Expression):
v = SR(v)
if not v.is_symbol():
raise TypeError("limit variable must be a variable, not a constant")

# Ensuring a is a symbolic expression
if not isinstance(a, Expression):
a = SR(a)

# Processing algorithm and direction options
effective_algorithm = algorithm
if taylor and algorithm == 'maxima':
algorithm = 'maxima_taylor'
effective_algorithm = 'maxima_taylor'

dir_plus = ['plus', '+', 'above', 'right']
dir_minus = ['minus', '-', 'below', 'left']
dir_both = [None] + dir_plus + dir_minus
if dir not in dir_both:
raise ValueError("dir must be one of " + ", ".join(map(repr, dir_both)))

if algorithm == 'maxima':
# Calling the appropriate backend based on effective_algorithm
l = None
if effective_algorithm == 'maxima':
if dir is None:
l = maxima.sr_limit(ex, v, a)
elif dir in dir_plus:
l = maxima.sr_limit(ex, v, a, 'plus')
elif dir in dir_minus:
l = maxima.sr_limit(ex, v, a, 'minus')
elif algorithm == 'maxima_taylor':
elif effective_algorithm == 'maxima_taylor':
if dir is None:
l = maxima.sr_tlimit(ex, v, a)
elif dir in dir_plus:
l = maxima.sr_tlimit(ex, v, a, 'plus')
elif dir in dir_minus:
l = maxima.sr_tlimit(ex, v, a, 'minus')
elif algorithm == 'sympy':
elif effective_algorithm == 'sympy':
import sympy
if dir is None:
l = sympy.limit(ex._sympy_(), v._sympy_(), a._sympy_())
elif dir in dir_plus:
l = sympy.limit(ex._sympy_(), v._sympy_(), a._sympy_(), dir='+')
sympy_dir = '+-'
if dir in dir_plus:
sympy_dir = '+'

Check warning on line 1608 in src/sage/calculus/calculus.py

View check run for this annotation

Codecov / codecov/patch

src/sage/calculus/calculus.py#L1608

Added line #L1608 was not covered by tests
elif dir in dir_minus:
l = sympy.limit(ex._sympy_(), v._sympy_(), a._sympy_(), dir='-')
elif algorithm == 'fricas':
sympy_dir = '-'

Check warning on line 1610 in src/sage/calculus/calculus.py

View check run for this annotation

Codecov / codecov/patch

src/sage/calculus/calculus.py#L1610

Added line #L1610 was not covered by tests
l = sympy.limit(ex._sympy_(), v._sympy_(), a._sympy_(), dir=sympy_dir)
elif effective_algorithm == 'fricas':
from sage.interfaces.fricas import fricas
eq = fricas.equation(v._fricas_(), a._fricas_())
f = ex._fricas_()
if dir is None:
l = fricas.limit(f, eq).sage()
if isinstance(l, dict):
l = maxima("und")
elif dir in dir_plus:
l = fricas.limit(f, eq, '"right"').sage()
fricas_dir_arg = None
if dir in dir_plus:
fricas_dir_arg = '"right"'

Check warning on line 1618 in src/sage/calculus/calculus.py

View check run for this annotation

Codecov / codecov/patch

src/sage/calculus/calculus.py#L1616-L1618

Added lines #L1616 - L1618 were not covered by tests
elif dir in dir_minus:
l = fricas.limit(f, eq, '"left"').sage()
elif algorithm == 'giac':
fricas_dir_arg = '"left"'

Check warning on line 1620 in src/sage/calculus/calculus.py

View check run for this annotation

Codecov / codecov/patch

src/sage/calculus/calculus.py#L1620

Added line #L1620 was not covered by tests

if fricas_dir_arg:
l = fricas.limit(f, eq, fricas_dir_arg).sage()

Check warning on line 1623 in src/sage/calculus/calculus.py

View check run for this annotation

Codecov / codecov/patch

src/sage/calculus/calculus.py#L1622-L1623

Added lines #L1622 - L1623 were not covered by tests
else:
l_raw = fricas.limit(f, eq).sage()
if isinstance(l_raw, dict):
l = SR('und')

Check warning on line 1627 in src/sage/calculus/calculus.py

View check run for this annotation

Codecov / codecov/patch

src/sage/calculus/calculus.py#L1625-L1627

Added lines #L1625 - L1627 were not covered by tests
else:
l = l_raw

Check warning on line 1629 in src/sage/calculus/calculus.py

View check run for this annotation

Codecov / codecov/patch

src/sage/calculus/calculus.py#L1629

Added line #L1629 was not covered by tests
elif effective_algorithm == 'giac':
from sage.libs.giac.giac import libgiac
v = v._giac_init_()
a = a._giac_init_()
if dir is None:
l = libgiac.limit(ex, v, a).sage()
elif dir in dir_plus:
l = libgiac.limit(ex, v, a, 1).sage()
giac_v = v._giac_init_()
giac_a = a._giac_init_()
giac_dir_arg = 0 # Default for two-sided
if dir in dir_plus:
giac_dir_arg = 1

Check warning on line 1636 in src/sage/calculus/calculus.py

View check run for this annotation

Codecov / codecov/patch

src/sage/calculus/calculus.py#L1632-L1636

Added lines #L1632 - L1636 were not covered by tests
elif dir in dir_minus:
l = libgiac.limit(ex, v, a, -1).sage()
elif algorithm == 'mathematica_free':
return mma_free_limit(ex, v, a, dir)
giac_dir_arg = -1
l = libgiac.limit(ex, giac_v, giac_a, giac_dir_arg).sage()

Check warning on line 1639 in src/sage/calculus/calculus.py

View check run for this annotation

Codecov / codecov/patch

src/sage/calculus/calculus.py#L1638-L1639

Added lines #L1638 - L1639 were not covered by tests
elif effective_algorithm == 'mathematica_free':
# Ensuring mma_free_limit exists
l = mma_free_limit(ex, v, a, dir)

Check warning on line 1642 in src/sage/calculus/calculus.py

View check run for this annotation

Codecov / codecov/patch

src/sage/calculus/calculus.py#L1642

Added line #L1642 was not covered by tests
else:
raise ValueError("Unknown algorithm: %s" % algorithm)
return ex.parent()(l)
raise ValueError("Unknown algorithm: %s" % effective_algorithm)

original_parent = ex.parent()

return original_parent(l)

# lim is alias for limit
lim = limit
Expand Down Expand Up @@ -1716,8 +1882,10 @@
sage: F.simplify()
s^(-n - 1)*gamma(n + 1)

Testing Maxima::

Testing Maxima::
sage: n = SR.var('n')
sage: assume(n > -1)
sage: laplace(t^n, t, s, algorithm='maxima')
s^(-n - 1)*gamma(n + 1)

Expand Down
Loading