From 844a70941de1c437c04b7ceefee36f862706fc47 Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Wed, 30 Aug 2023 16:25:50 +0900 Subject: [PATCH 1/8] Initial proof-of-concept for infinite products. --- src/sage/data_structures/stream.py | 127 +++++++++++++++++++++++++++++ src/sage/rings/lazy_series_ring.py | 42 ++++++++++ 2 files changed, 169 insertions(+) diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index 87b5b1e7ecb..caba146bd8a 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -3258,3 +3258,130 @@ def is_nonzero(self): True """ return self._series.is_nonzero() + +class Stream_infinite_product(Stream): + r""" + Stream defined by an infinite product. + + The ``iterator`` returns elements `p_i` to compute the product + `\prod_{i \in I} (1 + p_i)`. The valuation of `p_i` is weakly increasing + as we iterate over `I` and there are only finitely many terms with any + fixed valuation. In particular, this assumes the product is nonzero. + + For the ``_approximate_order``, this assumes the ring is a + lazy series. """ + def __init__(self, iterator, ring): + """ + Initialize ``self``. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_infinite_product + sage: TestSuite(f2).run() + """ + self._prod_iter = iterator + self._cur = None + self._cur_order = -infinity + self._ring = ring + super().__init__(False) + + @lazy_attribute + def _approximate_order(self): + """ + Compute and return the approximate order of ``self``. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_infinite_product + """ + if self._cur is None: + self._advance() + while self._cur_order <= 0: + self._advance() + self._true_order = True + return self._cur._coeff_stream.order() + + def _advance(self): + """ + Advance the iterator so that the approximate order increases by one. + """ + if self._cur is None: + temp = next(self._prod_iter) + self._cur = self._ring.one() + temp + self._cur_order = temp._coeff_stream._approximate_order + order = self._cur_order + while order == self._cur_order: + next_factor = next(self._prod_iter) + coeff_stream = next_factor._coeff_stream + while coeff_stream._approximate_order < order: + print(coeff_stream._approximate_order, coeff_stream[coeff_stream._approximate_order]) + # This check also updates the next_factor._approximate_order + if coeff_stream[coeff_stream._approximate_order]: + order = coeff_stream._approximate_order + raise ValueError(f"invalid product computation with invalid order {order} < {self._cur_order}") + self._cur *= self._ring.one() + next_factor + order = coeff_stream._approximate_order + self._cur_order = order + + def __getitem__(self, n): + """ + Return the ``n``-th coefficient of ``self``. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_infinite_product + """ + while n >= self._cur_order: + self._advance() + return self._cur[n] + + def order(self): + r""" + Return the order of ``self``, which is the minimum index ``n`` such + that ``self[n]`` is nonzero. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_infinite_product + """ + return self._approximate_order + + def __hash__(self): + """ + Return the hash of ``self``. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_infinite_product + """ + return hash((type(self), self._ring, self._prod_iter)) + + def __ne__(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_infinite_product + """ + if not (isinstance(other, type(self)) and other._ring == self._ring): + return True + ao = min(self._approximate_order, other._approximate_order) + if any(self[i] != other[i] for i in range(ao, min(self._cur_order, other._cur_order))): + return True + return False + + def is_nonzero(self): + r""" + Return ``True`` if and only if this stream is known + to be nonzero. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_infinite_product + """ + return True diff --git a/src/sage/rings/lazy_series_ring.py b/src/sage/rings/lazy_series_ring.py index 32545f65574..c5ff2537a17 100644 --- a/src/sage/rings/lazy_series_ring.py +++ b/src/sage/rings/lazy_series_ring.py @@ -890,6 +890,45 @@ def is_exact(self): """ return self.base_ring().is_exact() + def prod(self, args, index_set=None): + r""" + The product of elements of ``self``. + + INPUT: + + - ``args`` -- a list (or iterable) of elements of ``self`` + - ``index_set`` -- (optional) an indexing set for the product or + or a boolean + + If ``index_set`` is an iterable, then ``args`` should be a function + that takes in an index and returns an element `p_i` of ``self`` to + compute the product `\prod_{i \in I} (1 + p_i)`. The valuation of `p_i` + is weakly increasing as we iterate over `I` and there are only + finitely many terms with any fixed valuation. If ``index=True``, + then this will treat ``args`` as an infinite product indexed by + `0, 1, \ldots`. In particular, this assumes the product is nonzero. + + EXAMPLES:: + + sage: L. = LazyLaurentSeriesRing(QQ) + sage: euler = L.prod(lambda n: -t^n, PositiveIntegers()) + sage: euler + 1 - t - t^2 + t^5 + O(t^7) + sage: 1 / euler + 1 + t + 2*t^2 + 3*t^3 + 5*t^4 + 7*t^5 + 11*t^6 + O(t^7) + sage: euler - L.euler() + O(t^7) + """ + if index_set is None: + return super().prod(args) + if index_set is True: + it = args + else: + it = (args(i) for i in index_set) + from sage.data_structures.stream import Stream_infinite_product + coeff_stream = Stream_infinite_product(it, self) + return self.element_class(self, coeff_stream) + def _test_invert(self, **options): """ Test multiplicative inversion of elements of ``self``. @@ -1506,8 +1545,10 @@ def residue_field(self): raise TypeError("the base ring is not a field") return R + # === special functions === + def q_pochhammer(self, q=None): r""" Return the infinite ``q``-Pochhammer symbol `(a; q)_{\infty}`, @@ -1624,6 +1665,7 @@ def coeff(n): return (-1) ** ((m + 1) // 6) return self(coefficients=coeff, valuation=0) + ###################################################################### From eff5be50c0cca5595f99cd2e41a6003fd2f7d750 Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Mon, 3 Apr 2023 22:30:48 +0900 Subject: [PATCH 2/8] Addressing some of Martin's comments and fixing a bug. --- src/sage/data_structures/stream.py | 21 +++++++++++++-------- src/sage/rings/lazy_series_ring.py | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index caba146bd8a..8f5b1b538e1 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -3269,8 +3269,9 @@ class Stream_infinite_product(Stream): fixed valuation. In particular, this assumes the product is nonzero. For the ``_approximate_order``, this assumes the ring is a - lazy series. """ - def __init__(self, iterator, ring): + lazy series. + """ + def __init__(self, iterator, one): """ Initialize ``self``. @@ -3282,7 +3283,7 @@ def __init__(self, iterator, ring): self._prod_iter = iterator self._cur = None self._cur_order = -infinity - self._ring = ring + self._one = one super().__init__(False) @lazy_attribute @@ -3307,20 +3308,24 @@ def _advance(self): """ if self._cur is None: temp = next(self._prod_iter) - self._cur = self._ring.one() + temp + self._cur = self._one + temp self._cur_order = temp._coeff_stream._approximate_order order = self._cur_order while order == self._cur_order: - next_factor = next(self._prod_iter) + try: + next_factor = next(self._prod_iter) + except StopIteration: + self._cur_order = infinity + break coeff_stream = next_factor._coeff_stream while coeff_stream._approximate_order < order: - print(coeff_stream._approximate_order, coeff_stream[coeff_stream._approximate_order]) # This check also updates the next_factor._approximate_order if coeff_stream[coeff_stream._approximate_order]: order = coeff_stream._approximate_order raise ValueError(f"invalid product computation with invalid order {order} < {self._cur_order}") - self._cur *= self._ring.one() + next_factor - order = coeff_stream._approximate_order + self._cur *= self._one + next_factor + if not coeff_stream[coeff_stream._approximate_order]: + order += 1 self._cur_order = order def __getitem__(self, n): diff --git a/src/sage/rings/lazy_series_ring.py b/src/sage/rings/lazy_series_ring.py index c5ff2537a17..9c2d66216ce 100644 --- a/src/sage/rings/lazy_series_ring.py +++ b/src/sage/rings/lazy_series_ring.py @@ -926,7 +926,7 @@ def prod(self, args, index_set=None): else: it = (args(i) for i in index_set) from sage.data_structures.stream import Stream_infinite_product - coeff_stream = Stream_infinite_product(it, self) + coeff_stream = Stream_infinite_product(it, self.one()) return self.element_class(self, coeff_stream) def _test_invert(self, **options): From a118645fd8c36c919d4e44b316677ca143156f53 Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Tue, 4 Apr 2023 00:23:49 +0900 Subject: [PATCH 3/8] Change so products so the iterator produces 1 + p(x). --- src/sage/data_structures/stream.py | 47 ++++++++++++++++++------------ src/sage/rings/lazy_series_ring.py | 36 ++++++++++++++++++----- 2 files changed, 56 insertions(+), 27 deletions(-) diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index 8f5b1b538e1..b2dc1ce2885 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -3263,27 +3263,33 @@ class Stream_infinite_product(Stream): r""" Stream defined by an infinite product. - The ``iterator`` returns elements `p_i` to compute the product - `\prod_{i \in I} (1 + p_i)`. The valuation of `p_i` is weakly increasing - as we iterate over `I` and there are only finitely many terms with any - fixed valuation. In particular, this assumes the product is nonzero. + The ``iterator`` returns elements either `1 + p_i` to compute the + product `\prod_{i \in I} (1 + p_i)`. The valuation of `p_i` is weakly + increasing as we iterate over `I` and there are only finitely many + terms with any fixed valuation. In particular, this *assumes* the + product is nonzero. - For the ``_approximate_order``, this assumes the ring is a - lazy series. + .. WARNING:: + + This does not check that the input is valid. + + INPUT: + + - ``iterator`` -- the iterator for the factors + - ``constant_order`` -- the order corresponding to the constant term """ - def __init__(self, iterator, one): + def __init__(self, iterator, constant_order): """ Initialize ``self``. EXAMPLES:: sage: from sage.data_structures.stream import Stream_infinite_product - sage: TestSuite(f2).run() """ self._prod_iter = iterator self._cur = None self._cur_order = -infinity - self._one = one + self._constant_order = constant_order super().__init__(False) @lazy_attribute @@ -3304,11 +3310,16 @@ def _approximate_order(self): def _advance(self): """ - Advance the iterator so that the approximate order increases by one. + Advance the iterator so that the approximate order increases + by at least one. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_infinite_product """ if self._cur is None: temp = next(self._prod_iter) - self._cur = self._one + temp + self._cur = temp self._cur_order = temp._coeff_stream._approximate_order order = self._cur_order while order == self._cur_order: @@ -3316,15 +3327,13 @@ def _advance(self): next_factor = next(self._prod_iter) except StopIteration: self._cur_order = infinity + return + self._cur *= next_factor + # nonzero checks are safer than equality checks (i.e. in lazy series) + if order == self._constant_order and not (next_factor._coeff_stream[order] - 1): + order += 1 break - coeff_stream = next_factor._coeff_stream - while coeff_stream._approximate_order < order: - # This check also updates the next_factor._approximate_order - if coeff_stream[coeff_stream._approximate_order]: - order = coeff_stream._approximate_order - raise ValueError(f"invalid product computation with invalid order {order} < {self._cur_order}") - self._cur *= self._one + next_factor - if not coeff_stream[coeff_stream._approximate_order]: + while not next_factor._coeff_stream[order]: order += 1 self._cur_order = order diff --git a/src/sage/rings/lazy_series_ring.py b/src/sage/rings/lazy_series_ring.py index 9c2d66216ce..f8edf7dae82 100644 --- a/src/sage/rings/lazy_series_ring.py +++ b/src/sage/rings/lazy_series_ring.py @@ -896,28 +896,44 @@ def prod(self, args, index_set=None): INPUT: - - ``args`` -- a list (or iterable) of elements of ``self`` + - ``args`` -- a list (or iterable) of elements of ``self`` or when + ``index_set`` is given, a function that returns elements of ``self`` - ``index_set`` -- (optional) an indexing set for the product or - or a boolean + or ``True`` If ``index_set`` is an iterable, then ``args`` should be a function - that takes in an index and returns an element `p_i` of ``self`` to + that takes in an index and returns an element `1 + p_i` of ``self`` to compute the product `\prod_{i \in I} (1 + p_i)`. The valuation of `p_i` is weakly increasing as we iterate over `I` and there are only - finitely many terms with any fixed valuation. If ``index=True``, - then this will treat ``args`` as an infinite product indexed by - `0, 1, \ldots`. In particular, this assumes the product is nonzero. + finitely many terms with any fixed valuation. If ``index_set=True``, + then this will treat ``args`` as an iterator for a potentially + infinite product. In particular, this assumes the product is nonzero. + + .. WARNING:: + + If invalid input is given, this may loop forever. EXAMPLES:: sage: L. = LazyLaurentSeriesRing(QQ) - sage: euler = L.prod(lambda n: -t^n, PositiveIntegers()) + sage: euler = L.prod(lambda n: 1 - t^n, PositiveIntegers()) sage: euler 1 - t - t^2 + t^5 + O(t^7) sage: 1 / euler 1 + t + 2*t^2 + 3*t^3 + 5*t^4 + 7*t^5 + 11*t^6 + O(t^7) sage: euler - L.euler() O(t^7) + + sage: L.prod((1 - t^n for n in PositiveIntegers()), index_set=True) + 1 - t - t^2 + t^5 + O(t^7) + + sage: L.prod((1 + t^(n-3) for n in PositiveIntegers()), index_set=True) + 2*t^-3 + 4*t^-2 + 4*t^-1 + 4 + 6*t + 10*t^2 + 16*t^3 + O(t^4) + + sage: D = LazyDirichletSeriesRing(QQ, "s") + sage: ret = D.prod(lambda p: (1+D(1, valuation=p)).inverse(), index_set=Primes()) + sage: ret + 1 - 1/(2^s) - 1/(3^s) + 1/(4^s) - 1/(5^s) + 1/(6^s) - 1/(7^s) + O(1/(8^s)) """ if index_set is None: return super().prod(args) @@ -926,7 +942,11 @@ def prod(self, args, index_set=None): else: it = (args(i) for i in index_set) from sage.data_structures.stream import Stream_infinite_product - coeff_stream = Stream_infinite_product(it, self.one()) + if self._minimal_valuation is not None and self._minimal_valuation > 0: + # Only for Dirichlet series (currently) + coeff_stream = Stream_infinite_product(it, self._minimal_valuation) + else: + coeff_stream = Stream_infinite_product(it, 0) return self.element_class(self, coeff_stream) def _test_invert(self, **options): From b36b2ca99fbc5261192e8d7de894a0e9af1757f4 Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Wed, 30 Aug 2023 18:03:14 +0900 Subject: [PATCH 4/8] Changes to prod() from discussion and initial implementation of infinite sums. --- src/sage/data_structures/stream.py | 101 ++++++++++++----- src/sage/rings/lazy_series_ring.py | 172 ++++++++++++++++++++++++----- 2 files changed, 220 insertions(+), 53 deletions(-) diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index b2dc1ce2885..f8cf6a085aa 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -3259,15 +3259,15 @@ def is_nonzero(self): """ return self._series.is_nonzero() -class Stream_infinite_product(Stream): + +class Stream_infinite_operator(Stream): r""" - Stream defined by an infinite product. + Stream defined by applying an operator an infinite number of times. - The ``iterator`` returns elements either `1 + p_i` to compute the - product `\prod_{i \in I} (1 + p_i)`. The valuation of `p_i` is weakly - increasing as we iterate over `I` and there are only finitely many - terms with any fixed valuation. In particular, this *assumes* the - product is nonzero. + The ``iterator`` returns elements `s_i` to compute an infinite operator. + The valuation of `s_i` is weakly increasing as we iterate over `I` and + there are only finitely many terms with any fixed valuation with + `s_i \neq 0`. In particular, this *assumes* the result is nonzero. .. WARNING:: @@ -3276,20 +3276,18 @@ class Stream_infinite_product(Stream): INPUT: - ``iterator`` -- the iterator for the factors - - ``constant_order`` -- the order corresponding to the constant term """ - def __init__(self, iterator, constant_order): + def __init__(self, iterator): """ Initialize ``self``. EXAMPLES:: - sage: from sage.data_structures.stream import Stream_infinite_product + sage: from sage.data_structures.stream import Stream_infinite_sum """ - self._prod_iter = iterator + self._op_iter = iterator self._cur = None self._cur_order = -infinity - self._constant_order = constant_order super().__init__(False) @lazy_attribute @@ -3315,24 +3313,27 @@ def _advance(self): EXAMPLES:: - sage: from sage.data_structures.stream import Stream_infinite_product + sage: from sage.data_structures.stream import Stream_infinite_sum """ if self._cur is None: - temp = next(self._prod_iter) - self._cur = temp + temp = next(self._op_iter) + self.initial(temp) self._cur_order = temp._coeff_stream._approximate_order order = self._cur_order while order == self._cur_order: try: - next_factor = next(self._prod_iter) + next_factor = next(self._op_iter) except StopIteration: self._cur_order = infinity return - self._cur *= next_factor + coeff_stream = next_factor._coeff_stream + while coeff_stream._approximate_order < order: + # This check also updates the next_factor._approximate_order + if coeff_stream[coeff_stream._approximate_order]: + order = coeff_stream._approximate_order + raise ValueError(f"invalid product computation with invalid order {order} < {self._cur_order}") + self.apply_operator(next_factor) # nonzero checks are safer than equality checks (i.e. in lazy series) - if order == self._constant_order and not (next_factor._coeff_stream[order] - 1): - order += 1 - break while not next_factor._coeff_stream[order]: order += 1 self._cur_order = order @@ -3343,7 +3344,7 @@ def __getitem__(self, n): EXAMPLES:: - sage: from sage.data_structures.stream import Stream_infinite_product + sage: from sage.data_structures.stream import Stream_infinite_sum """ while n >= self._cur_order: self._advance() @@ -3356,7 +3357,7 @@ def order(self): EXAMPLES:: - sage: from sage.data_structures.stream import Stream_infinite_product + sage: from sage.data_structures.stream import Stream_infinite_sum """ return self._approximate_order @@ -3366,9 +3367,9 @@ def __hash__(self): EXAMPLES:: - sage: from sage.data_structures.stream import Stream_infinite_product + sage: from sage.data_structures.stream import Stream_infinite_sum """ - return hash((type(self), self._ring, self._prod_iter)) + return hash((type(self), self._ring, self._op_iter)) def __ne__(self, other): """ @@ -3380,7 +3381,7 @@ def __ne__(self, other): EXAMPLES:: - sage: from sage.data_structures.stream import Stream_infinite_product + sage: from sage.data_structures.stream import Stream_infinite_sum """ if not (isinstance(other, type(self)) and other._ring == self._ring): return True @@ -3396,6 +3397,54 @@ def is_nonzero(self): EXAMPLES:: - sage: from sage.data_structures.stream import Stream_infinite_product + sage: from sage.data_structures.stream import Stream_infinite_sum """ return True + +class Stream_infinite_sum(Stream_infinite_operator): + r""" + Stream defined by an infinite sum. + + The ``iterator`` returns elements `s_i` to compute the product + `\sum_{i \in I} s_i`. See :class:`Stream_infinite_operator` + for restrictions on the `s_i`. + + INPUT: + + - ``iterator`` -- the iterator for the factors + """ + def initial(self, obj): + """ + Set the initial data. + """ + self._cur = obj + + def apply_operator(self, next_obj): + """ + Apply the operator to ``next_obj``. + """ + self._cur += next_obj + +class Stream_infinite_product(Stream_infinite_operator): + r""" + Stream defined by an infinite product. + + The ``iterator`` returns elements `p_i` to compute the product + `\prod_{i \in I} (1 + p_i)`. See :class:`Stream_infinite_operator` + for restrictions on the `p_i`. + + INPUT: + + - ``iterator`` -- the iterator for the factors + """ + def initial(self, obj): + """ + Set the initial data. + """ + self._cur = obj + 1 + + def apply_operator(self, next_obj): + """ + Apply the operator to ``next_obj``. + """ + self._cur = self._cur + self._cur * next_obj diff --git a/src/sage/rings/lazy_series_ring.py b/src/sage/rings/lazy_series_ring.py index f8edf7dae82..24e147a6620 100644 --- a/src/sage/rings/lazy_series_ring.py +++ b/src/sage/rings/lazy_series_ring.py @@ -55,6 +55,7 @@ from sage.misc.lazy_attribute import lazy_attribute from sage.rings.integer_ring import ZZ +from sage.rings.infinity import infinity from sage.rings.polynomial.laurent_polynomial_ring import LaurentPolynomialRing from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing from sage.rings.lazy_series import (LazyModuleElement, @@ -890,28 +891,40 @@ def is_exact(self): """ return self.base_ring().is_exact() - def prod(self, args, index_set=None): + def prod(self, f, a=None, b=infinity, add_one=False): r""" The product of elements of ``self``. INPUT: - - ``args`` -- a list (or iterable) of elements of ``self`` or when - ``index_set`` is given, a function that returns elements of ``self`` - - ``index_set`` -- (optional) an indexing set for the product or - or ``True`` + - ``f`` -- a list (or iterable) of elements of ``self`` + - ``a``, ``b`` -- optional arguments + - ``add_one`` -- (default: ``False``); if ``True``, then converts a + lazy series `p_i` from ``args`` into `1 + p_i` for the product - If ``index_set`` is an iterable, then ``args`` should be a function - that takes in an index and returns an element `1 + p_i` of ``self`` to - compute the product `\prod_{i \in I} (1 + p_i)`. The valuation of `p_i` - is weakly increasing as we iterate over `I` and there are only - finitely many terms with any fixed valuation. If ``index_set=True``, - then this will treat ``args`` as an iterator for a potentially - infinite product. In particular, this assumes the product is nonzero. + If ``a`` and ``b`` are both integers, then this returns the product + `\prod_{i=a}^b f(i)`, where `f(i) = p_i` if ``add_one=False`` or + `f(i) = 1 + p_i` otherwise. If ``b`` is not specified, then we consider + `b = \infty`. Note this corresponds to the Python `range(a, b+1)`. + + If `a` is any other iterable, then this returns the product + `\prod_{i \in a} f(i)`, where `f(i) = p_i` if ``add_one=False`` or + `f(i) = 1 + p_i`. + + .. NOTE:: + + For infinite products, it is faster to use ``add_one=True`` since + the implementation is based on `p_i` in `\prod_i (1 + p_i)`. + + .. WARNING:: + + When ``f`` is an infinite generator, then the first argument + ``a`` must be ``True``. Otherwise this will loop forever. .. WARNING:: - If invalid input is given, this may loop forever. + For an *infinite* product of the form `\prod_i (1 + p_i)`, + if `p_i = 0`, then this will loop forever. EXAMPLES:: @@ -923,30 +936,135 @@ def prod(self, args, index_set=None): 1 + t + 2*t^2 + 3*t^3 + 5*t^4 + 7*t^5 + 11*t^6 + O(t^7) sage: euler - L.euler() O(t^7) + sage: L.prod(lambda n: -t^n, 1, add_one=True) + 1 - t - t^2 + t^5 + O(t^7) - sage: L.prod((1 - t^n for n in PositiveIntegers()), index_set=True) + sage: L.prod((1 - t^n for n in PositiveIntegers()), True) + 1 - t - t^2 + t^5 + O(t^7) + sage: L.prod((-t^n for n in PositiveIntegers()), True, add_one=True) 1 - t - t^2 + t^5 + O(t^7) - sage: L.prod((1 + t^(n-3) for n in PositiveIntegers()), index_set=True) + sage: L.prod((1 + t^(n-3) for n in PositiveIntegers()), True) 2*t^-3 + 4*t^-2 + 4*t^-1 + 4 + 6*t + 10*t^2 + 16*t^3 + O(t^4) + sage: L.prod(lambda n: 2 + t^n, -3, 5) + 96*t^-6 + 240*t^-5 + 336*t^-4 + 840*t^-3 + 984*t^-2 + 1248*t^-1 + + 1980 + 1668*t + 1824*t^2 + 1872*t^3 + 1782*t^4 + 1710*t^5 + + 1314*t^6 + 1122*t^7 + 858*t^8 + 711*t^9 + 438*t^10 + 282*t^11 + + 210*t^12 + 84*t^13 + 60*t^14 + 24*t^15 + sage: L.prod(lambda n: t^n / (1 + abs(n)), -2, 2, add_one=True) + 1/3*t^-3 + 5/6*t^-2 + 13/9*t^-1 + 25/9 + 13/9*t + 5/6*t^2 + 1/3*t^3 + sage: L.prod(lambda n: t^-2 + t^n / n, -4, -2) + 1/24*t^-9 - 1/8*t^-8 - 1/6*t^-7 + 1/2*t^-6 + sage: D = LazyDirichletSeriesRing(QQ, "s") - sage: ret = D.prod(lambda p: (1+D(1, valuation=p)).inverse(), index_set=Primes()) - sage: ret + sage: D.prod(lambda p: (1+D(1, valuation=p)).inverse(), Primes()) 1 - 1/(2^s) - 1/(3^s) + 1/(4^s) - 1/(5^s) + 1/(6^s) - 1/(7^s) + O(1/(8^s)) - """ - if index_set is None: - return super().prod(args) - if index_set is True: - it = args + + sage: D.prod(lambda p: D(1, valuation=p), Primes(), add_one=True) + 1 + 1/(2^s) + 1/(3^s) + 1/(5^s) + 1/(6^s) + 1/(7^s) + O(1/(8^s)) + """ + if a is None: + if add_one: + return super().prod(self.one() + g for g in f) + return super().prod(f) + + if a is True: + it = f + elif a in ZZ: + if b != infinity: + if add_one: + return super().prod(1 + f(i) for i in range(a, b+1)) + return super().prod(f(i) for i in range(a, b+1)) + from sage.sets.non_negative_integers import NonNegativeIntegers + it = (f(i+a) for i in NonNegativeIntegers()) else: - it = (args(i) for i in index_set) + it = (f(i) for i in a) + + # NOTE: We must have a new variable name for each new iterator + if not add_one: + data = (g - 1 for g in it) + else: + data = it + from sage.data_structures.stream import Stream_infinite_product - if self._minimal_valuation is not None and self._minimal_valuation > 0: - # Only for Dirichlet series (currently) - coeff_stream = Stream_infinite_product(it, self._minimal_valuation) + coeff_stream = Stream_infinite_product(data) + return self.element_class(self, coeff_stream) + + def sum(self, f, a=None, b=infinity): + r""" + The sum of elements of ``self``. + + INPUT: + + - ``f`` -- a list (or iterable or function) of elements of ``self`` + - ``a``, ``b`` -- optional arguments + + If ``a`` and ``b`` are both integers, then this returns the sum + `\sum_{i=a}^b f(i)`. If ``b`` is not specified, then we consider + `b = \infty`. Note this corresponds to the Python `range(a, b+1)`. + + If `a` is any other iterable, then this returns the sum + `\sum{i \in a} f(i)`. + + .. WARNING:: + + When ``f`` is an infinite generator, then the first argument + ``a`` must be ``True``. Otherwise this will loop forever. + + .. WARNING:: + + For an *infinite* sum of the form `\sum_i s_i`, + if `s_i = 0`, then this will loop forever. + + EXAMPLES:: + + sage: L. = LazyLaurentSeriesRing(QQ) + sage: L.sum(lambda n: t^n / (n+1), PositiveIntegers()) + 1/2*t + 1/3*t^2 + 1/4*t^3 + 1/5*t^4 + 1/6*t^5 + 1/7*t^6 + 1/8*t^7 + O(t^8) + + We verify the Rogers-Ramanujan identities up to degree 100:: + + sage: L. = LazyPowerSeriesRing(QQ) + sage: Gpi = L.prod(lambda k: -q^(1+5*k), 0, oo, add_one=True) + sage: Gpi *= L.prod(lambda k: -q^(4+5*k), 0, oo, add_one=True) + sage: Gp = 1 / Gpi + sage: G = L.sum(lambda n: q^(n^2) / prod(1 - q^(k+1) for k in range(n)), 0, oo) + sage: G - Gp + O(q^7) + sage: all(G[k] == Gp[k] for k in range(100)) + True + + sage: Hpi = L.prod(lambda k: -q^(2+5*k), 0, oo, add_one=True) + sage: Hpi *= L.prod(lambda k: -q^(3+5*k), 0, oo, add_one=True) + sage: Hp = 1 / Hpi + sage: H = L.sum(lambda n: q^(n^2+n) / prod(1 - q^(k+1) for k in range(n)), 0, oo) + sage: H - Hp + O(q^7) + sage: all(H[k] == Hp[k] for k in range(100)) + True + + :: + + sage: D = LazyDirichletSeriesRing(QQ, "s") + sage: D.sum(lambda p: D(1, valuation=p), Primes()) + 1/(2^s) + 1/(3^s) + 1/(5^s) + 1/(7^s) + O(1/(9^s)) + """ + if a is None: + return super().sum(f) + + if a is True: + it = f + elif a in ZZ: + if b != infinity: + return super().sum(f(i) for i in range(a, b+1)) + from sage.sets.non_negative_integers import NonNegativeIntegers + it = (f(i+a) for i in NonNegativeIntegers()) else: - coeff_stream = Stream_infinite_product(it, 0) + it = (f(i) for i in a) + + from sage.data_structures.stream import Stream_infinite_sum + coeff_stream = Stream_infinite_sum(it) return self.element_class(self, coeff_stream) def _test_invert(self, **options): From 5514c899b0c027be25b74c6533b752b0ede1916e Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Tue, 5 Sep 2023 16:38:59 +0900 Subject: [PATCH 5/8] Adding doctests and last polishing. --- src/sage/data_structures/stream.py | 159 ++++++++++++++++++++++++++--- src/sage/rings/lazy_series_ring.py | 3 - 2 files changed, 144 insertions(+), 18 deletions(-) diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index f8cf6a085aa..c459071eefb 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -3266,8 +3266,8 @@ class Stream_infinite_operator(Stream): The ``iterator`` returns elements `s_i` to compute an infinite operator. The valuation of `s_i` is weakly increasing as we iterate over `I` and - there are only finitely many terms with any fixed valuation with - `s_i \neq 0`. In particular, this *assumes* the result is nonzero. + there are only finitely many terms with any fixed valuation. + In particular, this *assumes* the result is nonzero. .. WARNING:: @@ -3278,12 +3278,15 @@ class Stream_infinite_operator(Stream): - ``iterator`` -- the iterator for the factors """ def __init__(self, iterator): - """ + r""" Initialize ``self``. EXAMPLES:: sage: from sage.data_structures.stream import Stream_infinite_sum + sage: L. = LazyLaurentSeriesRing(QQ) + sage: it = (t^n / (1 - t) for n in PositiveIntegers()) + sage: f = Stream_infinite_sum(it) """ self._op_iter = iterator self._cur = None @@ -3292,12 +3295,25 @@ def __init__(self, iterator): @lazy_attribute def _approximate_order(self): - """ + r""" Compute and return the approximate order of ``self``. EXAMPLES:: - sage: from sage.data_structures.stream import Stream_infinite_product + sage: from sage.data_structures.stream import Stream_infinite_sum, Stream_infinite_product + sage: L. = LazyLaurentSeriesRing(QQ) + sage: it = (t^n / (1 - t) for n in PositiveIntegers()) + sage: f = Stream_infinite_sum(it) + sage: f._approximate_order + 1 + sage: it = (t^n for n in PositiveIntegers()) + sage: f = Stream_infinite_product(it) + sage: f._approximate_order + 0 + sage: it = (t^(n-10) for n in PositiveIntegers()) + sage: f = Stream_infinite_product(it) + sage: f._approximate_order + -45 """ if self._cur is None: self._advance() @@ -3307,16 +3323,44 @@ def _approximate_order(self): return self._cur._coeff_stream.order() def _advance(self): - """ + r""" Advance the iterator so that the approximate order increases by at least one. EXAMPLES:: sage: from sage.data_structures.stream import Stream_infinite_sum + sage: L. = LazyLaurentSeriesRing(QQ) + sage: it = (n * t^n for n in range(10)) + sage: f = Stream_infinite_sum(it) + sage: f._cur is None + True + sage: f._advance() + sage: f._cur + t + 2*t^2 + sage: f._cur_order + 2 + sage: for _ in range(20): + ....: f._advance() + sage: f._cur + t + 2*t^2 + 3*t^3 + 4*t^4 + 5*t^5 + 6*t^6 + 7*t^7 + 8*t^8 + 9*t^9 + sage: f._cur_order + +Infinity + + sage: it = (t^(n//3) / (1 - t) for n in PositiveIntegers()) + sage: f = Stream_infinite_sum(it) + sage: f._advance() + sage: f._cur + 2 + 3*t + 3*t^2 + 3*t^3 + O(t^4) + sage: f._advance() + sage: f._cur + 2 + 5*t + 6*t^2 + 6*t^3 + 6*t^4 + O(t^5) """ if self._cur is None: temp = next(self._op_iter) + if not temp: + self._advance() + return self.initial(temp) self._cur_order = temp._coeff_stream._approximate_order order = self._cur_order @@ -3326,6 +3370,8 @@ def _advance(self): except StopIteration: self._cur_order = infinity return + if not next_factor: + continue coeff_stream = next_factor._coeff_stream while coeff_stream._approximate_order < order: # This check also updates the next_factor._approximate_order @@ -3339,12 +3385,19 @@ def _advance(self): self._cur_order = order def __getitem__(self, n): - """ + r""" Return the ``n``-th coefficient of ``self``. EXAMPLES:: sage: from sage.data_structures.stream import Stream_infinite_sum + sage: L. = LazyLaurentSeriesRing(QQ) + sage: it = (t^n / (1 - t) for n in PositiveIntegers()) + sage: f = Stream_infinite_sum(it) + sage: f[2] + 2 + sage: f[5] + 5 """ while n >= self._cur_order: self._advance() @@ -3358,21 +3411,31 @@ def order(self): EXAMPLES:: sage: from sage.data_structures.stream import Stream_infinite_sum + sage: L. = LazyLaurentSeriesRing(QQ) + sage: it = (t^(5+n) / (1 - t) for n in PositiveIntegers()) + sage: f = Stream_infinite_sum(it) + sage: f.order() + 6 """ return self._approximate_order def __hash__(self): - """ + r""" Return the hash of ``self``. EXAMPLES:: sage: from sage.data_structures.stream import Stream_infinite_sum + sage: L. = LazyLaurentSeriesRing(QQ) + sage: it = (t^n / (1 - t) for n in PositiveIntegers()) + sage: f = Stream_infinite_sum(it) + sage: hash(f) == hash((type(f), f._op_iter)) + True """ - return hash((type(self), self._ring, self._op_iter)) + return hash((type(self), self._op_iter)) def __ne__(self, other): - """ + r""" Return whether ``self`` and ``other`` are known to be equal. INPUT: @@ -3382,8 +3445,21 @@ def __ne__(self, other): EXAMPLES:: sage: from sage.data_structures.stream import Stream_infinite_sum + sage: L. = LazyLaurentSeriesRing(QQ) + sage: itf = (t^n / (1 - t) for n in PositiveIntegers()) + sage: f = Stream_infinite_sum(itf) + sage: itg = (t^(2*n-1) / (1 - t) for n in PositiveIntegers()) + sage: g = Stream_infinite_sum(itg) + sage: f != g + False + sage: f[10] + 10 + sage: g[10] + 5 + sage: f != g + True """ - if not (isinstance(other, type(self)) and other._ring == self._ring): + if not isinstance(other, type(self)): return True ao = min(self._approximate_order, other._approximate_order) if any(self[i] != other[i] for i in range(ao, min(self._cur_order, other._cur_order))): @@ -3398,9 +3474,15 @@ def is_nonzero(self): EXAMPLES:: sage: from sage.data_structures.stream import Stream_infinite_sum + sage: L. = LazyLaurentSeriesRing(QQ) + sage: it = (t^n / (1 - t) for n in PositiveIntegers()) + sage: f = Stream_infinite_sum(it) + sage: f.is_nonzero() + True """ return True + class Stream_infinite_sum(Stream_infinite_operator): r""" Stream defined by an infinite sum. @@ -3414,17 +3496,41 @@ class Stream_infinite_sum(Stream_infinite_operator): - ``iterator`` -- the iterator for the factors """ def initial(self, obj): - """ + r""" Set the initial data. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_infinite_sum + sage: L. = LazyLaurentSeriesRing(QQ) + sage: it = (t^n / (1 - t) for n in PositiveIntegers()) + sage: f = Stream_infinite_sum(it) + sage: f._cur is None + True + sage: f._advance() # indirect doctest + sage: f._cur + t + 2*t^2 + 2*t^3 + 2*t^4 + O(t^5) """ self._cur = obj def apply_operator(self, next_obj): - """ + r""" Apply the operator to ``next_obj``. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_infinite_sum + sage: L. = LazyLaurentSeriesRing(QQ) + sage: it = (t^(n//2) / (1 - t) for n in PositiveIntegers()) + sage: f = Stream_infinite_sum(it) + sage: f._advance() + sage: f._advance() # indirect doctest + sage: f._cur + 1 + 3*t + 4*t^2 + 4*t^3 + 4*t^4 + O(t^5) """ self._cur += next_obj + class Stream_infinite_product(Stream_infinite_operator): r""" Stream defined by an infinite product. @@ -3438,13 +3544,36 @@ class Stream_infinite_product(Stream_infinite_operator): - ``iterator`` -- the iterator for the factors """ def initial(self, obj): - """ + r""" Set the initial data. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_infinite_product + sage: L. = LazyLaurentSeriesRing(QQ) + sage: it = (t^n / (1 - t) for n in PositiveIntegers()) + sage: f = Stream_infinite_product(it) + sage: f._cur is None + True + sage: f._advance() # indirect doctest + sage: f._cur + 1 + t + 2*t^2 + 3*t^3 + 4*t^4 + 5*t^5 + 6*t^6 + O(t^7) """ self._cur = obj + 1 def apply_operator(self, next_obj): - """ + r""" Apply the operator to ``next_obj``. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_infinite_product + sage: L. = LazyLaurentSeriesRing(QQ) + sage: it = (t^n / (1 - t) for n in PositiveIntegers()) + sage: f = Stream_infinite_product(it) + sage: f._advance() + sage: f._advance() # indirect doctest + sage: f._cur + 1 + t + 2*t^2 + 4*t^3 + 6*t^4 + 9*t^5 + 13*t^6 + O(t^7) """ self._cur = self._cur + self._cur * next_obj diff --git a/src/sage/rings/lazy_series_ring.py b/src/sage/rings/lazy_series_ring.py index 24e147a6620..7da4be96e20 100644 --- a/src/sage/rings/lazy_series_ring.py +++ b/src/sage/rings/lazy_series_ring.py @@ -1683,10 +1683,8 @@ def residue_field(self): raise TypeError("the base ring is not a field") return R - # === special functions === - def q_pochhammer(self, q=None): r""" Return the infinite ``q``-Pochhammer symbol `(a; q)_{\infty}`, @@ -1803,7 +1801,6 @@ def coeff(n): return (-1) ** ((m + 1) // 6) return self(coefficients=coeff, valuation=0) - ###################################################################### From 4b881809b7aa791434da2966cde36a16ad4a7247 Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Thu, 7 Sep 2023 18:25:06 +0900 Subject: [PATCH 6/8] Fixing a broken doctest by better testing for 0; using self.one(). --- src/sage/data_structures/stream.py | 4 ++-- src/sage/rings/lazy_series_ring.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index c459071eefb..6268e304331 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -3358,7 +3358,7 @@ def _advance(self): """ if self._cur is None: temp = next(self._op_iter) - if not temp: + if isinstance(temp._coeff_stream, Stream_zero): self._advance() return self.initial(temp) @@ -3370,7 +3370,7 @@ def _advance(self): except StopIteration: self._cur_order = infinity return - if not next_factor: + if isinstance(next_factor._coeff_stream, Stream_zero): continue coeff_stream = next_factor._coeff_stream while coeff_stream._approximate_order < order: diff --git a/src/sage/rings/lazy_series_ring.py b/src/sage/rings/lazy_series_ring.py index 7da4be96e20..e9308946514 100644 --- a/src/sage/rings/lazy_series_ring.py +++ b/src/sage/rings/lazy_series_ring.py @@ -974,7 +974,7 @@ def prod(self, f, a=None, b=infinity, add_one=False): elif a in ZZ: if b != infinity: if add_one: - return super().prod(1 + f(i) for i in range(a, b+1)) + return super().prod(self.one() + f(i) for i in range(a, b+1)) return super().prod(f(i) for i in range(a, b+1)) from sage.sets.non_negative_integers import NonNegativeIntegers it = (f(i+a) for i in NonNegativeIntegers()) @@ -983,7 +983,7 @@ def prod(self, f, a=None, b=infinity, add_one=False): # NOTE: We must have a new variable name for each new iterator if not add_one: - data = (g - 1 for g in it) + data = (g - self.one() for g in it) else: data = it From 666dd0c6981b8dd6ff103565fa990539ca9ceafd Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Fri, 8 Sep 2023 14:31:11 +0900 Subject: [PATCH 7/8] Small doc formatting tweak. --- src/sage/rings/lazy_series_ring.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/rings/lazy_series_ring.py b/src/sage/rings/lazy_series_ring.py index e9308946514..4f3e85d02bc 100644 --- a/src/sage/rings/lazy_series_ring.py +++ b/src/sage/rings/lazy_series_ring.py @@ -905,7 +905,7 @@ def prod(self, f, a=None, b=infinity, add_one=False): If ``a`` and ``b`` are both integers, then this returns the product `\prod_{i=a}^b f(i)`, where `f(i) = p_i` if ``add_one=False`` or `f(i) = 1 + p_i` otherwise. If ``b`` is not specified, then we consider - `b = \infty`. Note this corresponds to the Python `range(a, b+1)`. + `b = \infty`. Note this corresponds to the Python ``range(a, b+1)``. If `a` is any other iterable, then this returns the product `\prod_{i \in a} f(i)`, where `f(i) = p_i` if ``add_one=False`` or @@ -1002,7 +1002,7 @@ def sum(self, f, a=None, b=infinity): If ``a`` and ``b`` are both integers, then this returns the sum `\sum_{i=a}^b f(i)`. If ``b`` is not specified, then we consider - `b = \infty`. Note this corresponds to the Python `range(a, b+1)`. + `b = \infty`. Note this corresponds to the Python ``range(a, b+1)``. If `a` is any other iterable, then this returns the sum `\sum{i \in a} f(i)`. From 2afd7f37aae6e164bbd3f25bfa64ee8b5054c1f1 Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Sun, 10 Sep 2023 12:28:09 +0900 Subject: [PATCH 8/8] Better order handling within infinite operator streams. --- src/sage/data_structures/stream.py | 9 +++++---- src/sage/rings/lazy_series_ring.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index 6268e304331..673c78e1476 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -3319,8 +3319,7 @@ def _approximate_order(self): self._advance() while self._cur_order <= 0: self._advance() - self._true_order = True - return self._cur._coeff_stream.order() + return self._cur._coeff_stream._approximate_order def _advance(self): r""" @@ -3363,6 +3362,7 @@ def _advance(self): return self.initial(temp) self._cur_order = temp._coeff_stream._approximate_order + order = self._cur_order while order == self._cur_order: try: @@ -3379,8 +3379,9 @@ def _advance(self): order = coeff_stream._approximate_order raise ValueError(f"invalid product computation with invalid order {order} < {self._cur_order}") self.apply_operator(next_factor) - # nonzero checks are safer than equality checks (i.e. in lazy series) - while not next_factor._coeff_stream[order]: + order = coeff_stream._approximate_order + # We check to see if we need to increment the order + if order == self._cur_order and not coeff_stream[order]: order += 1 self._cur_order = order diff --git a/src/sage/rings/lazy_series_ring.py b/src/sage/rings/lazy_series_ring.py index 4f3e85d02bc..a4fb39436f3 100644 --- a/src/sage/rings/lazy_series_ring.py +++ b/src/sage/rings/lazy_series_ring.py @@ -1023,6 +1023,17 @@ def sum(self, f, a=None, b=infinity): sage: L.sum(lambda n: t^n / (n+1), PositiveIntegers()) 1/2*t + 1/3*t^2 + 1/4*t^3 + 1/5*t^4 + 1/6*t^5 + 1/7*t^6 + 1/8*t^7 + O(t^8) + sage: L. = LazyPowerSeriesRing(QQ) + sage: T = L.undefined(1) + sage: D = L.undefined(0) + sage: H = L.sum(lambda k: T(z^k)/k, 2) + sage: T.define(z*exp(T)*D) + sage: D.define(exp(H)) + sage: T + z + z^2 + 2*z^3 + 4*z^4 + 9*z^5 + 20*z^6 + 48*z^7 + O(z^8) + sage: D + 1 + 1/2*z^2 + 1/3*z^3 + 7/8*z^4 + 11/30*z^5 + 281/144*z^6 + O(z^7) + We verify the Rogers-Ramanujan identities up to degree 100:: sage: L. = LazyPowerSeriesRing(QQ)