Skip to content

Commit

Permalink
gh-36233: Implement integration and Taylor series for lazy series
Browse files Browse the repository at this point in the history
    
<!-- ^^^^^
Please provide a concise, informative and self-explanatory title.
Don't put issue numbers in there, do this in the PR body below.
For example, instead of "Fixes #1234" use "Introduce new method to
calculate 1+1"
-->
<!-- Describe your changes here in detail -->

We implement an `integral()` method for lazy Laurent and power series.
If we raise an error if we perform $\int t^{-1} dt$.

We also prove a way to construct a lazy (power) series as the Taylor
series of a function.

<!-- Why is this change required? What problem does it solve? -->
<!-- If this PR resolves an open issue, please link to it here. For
example "Fixes #12345". -->
<!-- If your change requires a documentation PR, please link it
appropriately. -->

### 📝 Checklist

<!-- Put an `x` in all the boxes that apply. -->
<!-- If your change requires a documentation PR, please link it
appropriately -->
<!-- If you're unsure about any of these, don't hesitate to ask. We're
here to help! -->
<!-- Feel free to remove irrelevant items. -->

- [x] The title is concise, informative, and self-explanatory.
- [x] The description explains in detail what this PR is about.
- [x] I have linked a relevant issue or discussion.
- [x] I have created tests covering the changes.
- [x] I have updated the documentation accordingly.

### ⌛ Dependencies

<!-- List all open PRs that this PR logically depends on
- #12345: short description why this is a dependency
- #34567: ...
-->

- #35485: Uses the `is_uninitialized()` checking.

<!-- If you're unsure about any of these, don't hesitate to ask. We're
here to help! -->
    
URL: #36233
Reported by: Travis Scrimshaw
Reviewer(s): Martin Rubey, Travis Scrimshaw
  • Loading branch information
Release Manager committed Jan 20, 2024
2 parents a9558bb + fe6bac8 commit 70af5c8
Show file tree
Hide file tree
Showing 3 changed files with 900 additions and 9 deletions.
310 changes: 306 additions & 4 deletions src/sage/data_structures/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,6 @@ class Stream_exact(Stream):
The convention for ``order`` is different to the one in
:class:`sage.rings.lazy_series_ring.LazySeriesRing`, where
the input is shifted to have the prescribed order.
"""
def __init__(self, initial_coefficients, constant=None, degree=None, order=None):
"""
Expand Down Expand Up @@ -1038,6 +1037,175 @@ def __eq__(self, other):
return isinstance(other, type(self)) and self.get_coefficient == other.get_coefficient


class Stream_taylor(Stream_inexact):
r"""
Class that returns a stream for the Taylor series of a function.
INPUT:
- ``function`` -- a function that has a ``derivative`` method and takes
a single input
- ``is_sparse`` -- boolean; specifies whether the stream is sparse
EXAMPLES::
sage: from sage.data_structures.stream import Stream_taylor
sage: g(x) = sin(x)
sage: f = Stream_taylor(g, False)
sage: f[3]
-1/6
sage: [f[i] for i in range(10)]
[0, 1, 0, -1/6, 0, 1/120, 0, -1/5040, 0, 1/362880]
sage: g(y) = cos(y)
sage: f = Stream_taylor(g, False)
sage: n = f.iterate_coefficients()
sage: [next(n) for _ in range(10)]
[1, 0, -1/2, 0, 1/24, 0, -1/720, 0, 1/40320, 0]
sage: g(z) = 1 / (1 - 2*z)
sage: f = Stream_taylor(g, True)
sage: [f[i] for i in range(4)]
[1, 2, 4, 8]
"""
def __init__(self, function, is_sparse):
"""
Initialize.
TESTS::
sage: from sage.data_structures.stream import Stream_taylor
sage: f = Stream_taylor(polygen(QQ, 'x')^3, False)
sage: TestSuite(f).run(skip="_test_pickling")
"""
from sage.symbolic.ring import SR
from sage.structure.element import parent
if parent(function) is SR:
self._is_symbolic = True
if function.number_of_arguments() != 1:
raise NotImplementedError("the function can only take a single input")
self._arg = function.arguments()[0]
else:
self._is_symbolic = False
self._func = function
super().__init__(is_sparse, False)
self._approximate_order = 0

def __hash__(self):
"""
Return the hash of ``self``.
EXAMPLES::
sage: from sage.data_structures.stream import Stream_taylor
sage: x = polygen(QQ, 'x')
sage: f = Stream_taylor(x^3 + x - 2, True)
sage: g = Stream_taylor(x^3 + x - 2, False)
sage: hash(f) == hash(g)
True
"""
# We don't hash the function as it might not be hashable.
return hash(type(self))

def __eq__(self, other):
r"""
Return whether ``self`` and ``other`` are known to be equal.
INPUT:
- ``other`` -- a stream
EXAMPLES::
sage: from sage.data_structures.stream import Stream_taylor
sage: x = polygen(QQ, 'x')
sage: fun = x^2
sage: f = Stream_taylor(x^2, True)
sage: g = Stream_taylor(x^2, False)
sage: h = Stream_taylor((x^3 + x^2) / (x + 1), False)
sage: f == g
True
sage: f == h
True
sage: F(y) = cos(y)^2 + sin(y)^2 + y
sage: G(y) = y + 1
sage: f = Stream_taylor(F, True)
sage: g = Stream_taylor(G, False)
sage: f == g
True
"""
# The bool call is needed when the functions are in SR
return isinstance(other, type(self)) and bool(self._func == other._func)

def get_coefficient(self, n):
r"""
Return the ``n``-th coefficient of ``self``.
INPUT:
- ``n`` -- integer; the degree for the coefficient
EXAMPLES::
sage: from sage.data_structures.stream import Stream_taylor
sage: g(x) = exp(x)
sage: f = Stream_taylor(g, True)
sage: f.get_coefficient(5)
1/120
sage: from sage.data_structures.stream import Stream_taylor
sage: y = SR.var('y')
sage: f = Stream_taylor(sin(y), True)
sage: f.get_coefficient(0)
0
sage: f.get_coefficient(5)
1/120
"""
if n == 0:
if self._is_symbolic:
return self._func.subs({self._arg: ZZ.zero()})
return self._func(ZZ.zero())

from sage.functions.other import factorial
if self._is_symbolic:
num = self._func.derivative(n).subs({self._arg: ZZ.zero()})
else:
num = self._func.derivative(n)(ZZ.zero())
return num / factorial(n)

def iterate_coefficients(self):
r"""
A generator for the coefficients of ``self``.
EXAMPLES::
sage: from sage.data_structures.stream import Stream_taylor
sage: x = polygen(QQ, 'x')
sage: f = Stream_taylor(x^3, False)
sage: it = f.iterate_coefficients()
sage: [next(it) for _ in range(10)]
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
sage: y = SR.var('y')
sage: f = Stream_taylor(y^3, False)
sage: it = f.iterate_coefficients()
sage: [next(it) for _ in range(10)]
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
"""
cur = self._func
n = 0
denom = 1
while True:
if self._is_symbolic:
yield cur({self._arg: ZZ.zero()}) / denom
else:
yield cur(ZZ.zero()) / denom
cur = cur.derivative()
n += 1
denom *= n


class Stream_uninitialized(Stream_inexact):
r"""
Coefficient stream for an uninitialized series.
Expand All @@ -1051,7 +1219,7 @@ class Stream_uninitialized(Stream_inexact):
.. TODO::
shouldn't instances of this class share the cache with its
Should instances of this class share the cache with its
``_target``?
EXAMPLES::
Expand Down Expand Up @@ -1247,7 +1415,6 @@ class Stream_binary(Stream_inexact):
sage: [h[i] for i in range(10)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
"""

def __init__(self, left, right, is_sparse):
"""
Initialize ``self``.
Expand Down Expand Up @@ -3371,7 +3538,7 @@ def __getitem__(self, n):
sage: [f2[i] for i in range(-1, 4)]
[0, 2, 6, 12, 20]
"""
return (prod(n + k for k in range(1, self._shift + 1))
return (ZZ.prod(range(n + 1, n + self._shift + 1))
* self._series[n + self._shift])

def __hash__(self):
Expand Down Expand Up @@ -3433,6 +3600,141 @@ def is_nonzero(self):
return self._series.is_nonzero()


class Stream_integral(Stream_unary):
"""
Operator for taking integrals of a non-exact stream.
INPUT:
- ``series`` -- a :class:`Stream`
- ``integration_constants`` -- a list of integration constants
- ``is_sparse`` -- boolean
"""
def __init__(self, series, integration_constants, is_sparse):
"""
Initialize ``self``.
EXAMPLES::
sage: from sage.data_structures.stream import Stream_exact, Stream_integral
sage: f = Stream_exact([1, 2, 3])
sage: f2 = Stream_integral(f, [-2], True)
sage: TestSuite(f2).run()
"""
self._shift = len(integration_constants)
self._integration_constants = tuple(integration_constants)
super().__init__(series, is_sparse, False)

@lazy_attribute
def _approximate_order(self):
"""
Compute and return the approximate order of ``self``.
EXAMPLES::
sage: from sage.data_structures.stream import Stream_function, Stream_integral
sage: f = Stream_function(lambda n: (n+1)*(n+2), True, 2)
sage: h = Stream_integral(f, [0, 0], True)
sage: h._approximate_order
0
sage: [h[i] for i in range(10)]
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1]
sage: h._approximate_order
4
"""
# this is generally not the true order
return min(self._series._approximate_order + self._shift, 0)

def get_coefficient(self, n):
r"""
Return the ``n``-th coefficient of ``self``.
EXAMPLES::
sage: from sage.data_structures.stream import Stream_function, Stream_integral
sage: f = Stream_function(lambda n: n + 1, True, -3)
sage: [f[i] for i in range(-3, 4)]
[-2, -1, 0, 1, 2, 3, 4]
sage: f2 = Stream_integral(f, [0], True)
sage: [f2.get_coefficient(i) for i in range(-3, 5)]
[0, 1, 1, 0, 1, 1, 1, 1]
sage: f = Stream_function(lambda n: (n + 1)*(n+2), True, 2)
sage: [f[i] for i in range(-1, 4)]
[0, 0, 0, 12, 20]
sage: f2 = Stream_integral(f, [-1, -1, -1], True)
sage: [f2.get_coefficient(i) for i in range(-1, 7)]
[0, -1, -1, -1/2, 0, 0, 1/5, 1/6]
"""
if 0 <= n < self._shift:
return (self._integration_constants[n] / ZZ.prod(range(2, n + 1)))
return (self._series[n - self._shift] /
ZZ.prod(range(n - self._shift + 1, n + 1)))

def __hash__(self):
"""
Return the hash of ``self``.
EXAMPLES::
sage: from sage.data_structures.stream import Stream_function, Stream_integral
sage: a = Stream_function(lambda n: 2*n, False, 1)
sage: f = Stream_integral(a, [0, 1], True)
sage: g = Stream_integral(a, [0, 2], True)
sage: hash(f) == hash(f)
True
sage: hash(f) == hash(g)
False
"""
return hash((type(self), self._series, self._integration_constants))

def __eq__(self, other):
"""
Return whether ``self`` and ``other`` are known to be equal.
INPUT:
- ``other`` -- a stream
EXAMPLES::
sage: from sage.data_structures.stream import Stream_function, Stream_integral
sage: a = Stream_function(lambda n: 2*n, False, 1)
sage: f = Stream_integral(a, [1], True)
sage: g = Stream_integral(a, [2], True)
sage: f == g
False
sage: f == Stream_integral(a, [1], True)
True
"""
return (isinstance(other, type(self))
and self._integration_constants == other._integration_constants
and self._series == other._series)

def is_nonzero(self):
r"""
Return ``True`` if and only if this stream is known
to be non-zero.
EXAMPLES::
sage: from sage.data_structures.stream import Stream_function, Stream_integral
sage: f = Stream_function(lambda n: 2*n, True, 1)
sage: f[1]
2
sage: f.is_nonzero()
True
sage: Stream_integral(f, [0], True).is_nonzero()
True
sage: f = Stream_function(lambda n: 0, False, 1)
sage: Stream_integral(f, [0, 0, 0], False).is_nonzero()
False
sage: Stream_integral(f, [0, 2], False).is_nonzero()
True
"""
return self._series.is_nonzero() or any(self._integration_constants)


class Stream_infinite_operator(Stream):
r"""
Stream defined by applying an operator an infinite number of times.
Expand Down
Loading

0 comments on commit 70af5c8

Please sign in to comment.