From ba8f1c357eca7cb585942f6b047e9718786e7cdf Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Wed, 12 Apr 2023 17:23:13 +0900 Subject: [PATCH 1/8] Adding a new comparison mode to lazy series and added a check for undefined series. --- .../combinat/species/recursive_species.py | 9 + src/sage/data_structures/stream.py | 53 ++++ src/sage/rings/lazy_series.py | 229 +++++++++++++++--- src/sage/rings/lazy_series_ring.py | 117 +++++++-- 4 files changed, 353 insertions(+), 55 deletions(-) diff --git a/src/sage/combinat/species/recursive_species.py b/src/sage/combinat/species/recursive_species.py index a361bdfad20..8fd01494c1a 100644 --- a/src/sage/combinat/species/recursive_species.py +++ b/src/sage/combinat/species/recursive_species.py @@ -401,6 +401,15 @@ def define(self, x): [1, 2, 3, 5, 8, 13, 21, 34, 55, 89] sage: F.isotype_generating_series()[0:10] [1, 2, 3, 5, 8, 13, 21, 34, 55, 89] + + Check that :issue:`35071` is fixed:: + + sage: X = species.SingletonSpecies() + sage: E = species.SetSpecies(max=3) + sage: B = species.CombinatorialSpecies(min=1) + sage: B.define(X*E(B)) + sage: B.generating_series() + z + z^2 + 3/2*z^3 + 5/2*z^4 + 9/2*z^5 + 17/2*z^6 + 133/8*z^7 + O(z^8) """ if not isinstance(x, GenericCombinatorialSpecies): raise TypeError("x must be a combinatorial species") diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index 87b5b1e7ecb..735398cbcca 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -199,6 +199,12 @@ def is_nonzero(self): """ return False + def is_undefined(self): + """ + Return ``True`` if ``self`` is an undefined stream. + """ + return False + class Stream_inexact(Stream): """ @@ -1042,6 +1048,12 @@ def iterate_coefficients(self): yield self._target[n] n += 1 + def is_undefined(self): + """ + Return ``True`` if ``self`` is an undefined stream. + """ + return self._target is None + class Stream_unary(Stream_inexact): r""" @@ -1118,6 +1130,12 @@ def __eq__(self, other): """ return isinstance(other, type(self)) and self._series == other._series + def is_undefined(self): + """ + Return ``True`` if ``self`` is an undefined stream. + """ + return self._series.is_undefined() + class Stream_binary(Stream_inexact): """ @@ -1206,6 +1224,12 @@ def __eq__(self, other): return False return self._left == other._left and self._right == other._right + def is_undefined(self): + """ + Return ``True`` if ``self`` is an undefined stream. + """ + return self._left.is_undefined() or self._right.is_undefined() + class Stream_binaryCommutative(Stream_binary): r""" @@ -2316,6 +2340,12 @@ def is_nonzero(self): """ return self._series.is_nonzero() + def is_undefined(self): + """ + Return ``True`` if ``self`` is an undefined stream. + """ + return self._series.is_undefined() + class Stream_rmul(Stream_scalar): """ @@ -2748,6 +2778,12 @@ def __eq__(self, other): return (isinstance(other, type(self)) and self._series == other._series and self._function == other._function) + def is_undefined(self): + """ + Return ``True`` if ``self`` is an undefined stream. + """ + return self._series.is_undefined() + class Stream_shift(Stream): """ @@ -2881,6 +2917,11 @@ def is_nonzero(self): """ return self._series.is_nonzero() + def is_undefined(self): + """ + Return ``True`` if ``self`` is an undefined stream. + """ + return self._series.is_undefined() class Stream_truncated(Stream_inexact): """ @@ -3130,6 +3171,12 @@ def is_nonzero(self): start = self._approximate_order - offset return any(self._cache[start:]) + def is_undefined(self): + """ + Return ``True`` if ``self`` is an undefined stream. + """ + return self._series.is_undefined() + class Stream_derivative(Stream_inexact): """ @@ -3258,3 +3305,9 @@ def is_nonzero(self): True """ return self._series.is_nonzero() + + def is_undefined(self): + """ + Return ``True`` if ``self`` is an undefined stream. + """ + return self._series.is_undefined() diff --git a/src/sage/rings/lazy_series.py b/src/sage/rings/lazy_series.py index e9a035c445d..a8dd02146e0 100644 --- a/src/sage/rings/lazy_series.py +++ b/src/sage/rings/lazy_series.py @@ -246,6 +246,7 @@ Stream_dirichlet_invert, Stream_plethysm ) +from sage.misc.unknown import Unknown, UnknownError class LazyModuleElement(Element): @@ -939,13 +940,31 @@ def _richcmp_(self, other, op): sage: fz = L(lambda n: 0, valuation=0) sage: L.zero() == fz - Traceback (most recent call last): - ... - ValueError: undecidable + False sage: fz == L.zero() - Traceback (most recent call last): - ... - ValueError: undecidable + False + + With using :class:`Unknown`:: + + sage: L.options.use_unknown = True + sage: fz = L(lambda n: 0, valuation=0) + sage: L.zero() == fz + Unknown + sage: fz == L.zero() + Unknown + sage: fz != L.zero() + Unknown + + With using finite halting precision:: + + sage: L.options.halting_precision = 40 + sage: fz = L(lambda n: 0, valuation=0) + sage: L.zero() == fz + True + sage: fz == L.zero() + True + + sage: L.options._reset() TESTS:: @@ -954,24 +973,27 @@ def _richcmp_(self, other, op): sage: g = L([0,0,1,0,1,0,0], degree=7, constant=1) sage: f == g True - """ if op is op_EQ: + if (not self.parent().options['use_unknown'] + and self.parent().options['halting_precision'] is None): + return self._coeff_stream == other._coeff_stream + if isinstance(self._coeff_stream, Stream_zero): if isinstance(other._coeff_stream, Stream_zero): return True - if other._coeff_stream.is_nonzero(): + if other._coeff_stream.is_undefined() or other._coeff_stream.is_nonzero(): return False elif isinstance(other._coeff_stream, Stream_zero): - if self._coeff_stream.is_nonzero(): + if self._coeff_stream.is_undefined() or self._coeff_stream.is_nonzero(): return False elif isinstance(self._coeff_stream, Stream_exact): if isinstance(other._coeff_stream, Stream_exact): return self._coeff_stream == other._coeff_stream - if self._coeff_stream != other._coeff_stream: + if self._coeff_stream != other._coeff_stream or other._coeff_stream.is_undefined(): return False elif isinstance(other._coeff_stream, Stream_exact): - if other._coeff_stream != self._coeff_stream: + if other._coeff_stream != self._coeff_stream or self._coeff_stream.is_undefined(): return False else: # both streams are inexact, perhaps they are equal by @@ -981,19 +1003,26 @@ def _richcmp_(self, other, op): # perhaps their caches are different if self._coeff_stream != other._coeff_stream: return False + if self._coeff_stream.is_undefined() or other._coeff_stream.is_undefined(): + return False # undecidable otherwise prec = self.parent().options['halting_precision'] if prec is None: - raise ValueError("undecidable") + return Unknown + # raise UnknownError("undecidable") # at least one of the approximate orders is not infinity m = min(self._coeff_stream._approximate_order, other._coeff_stream._approximate_order) return all(self[i] == other[i] for i in range(m, m + prec)) if op is op_NE: - return not (self == other) + ret = (self == other) + if ret is Unknown: + return ret + return not ret + # FIXME: This should check for equality in <= and >= and other return NotImplemented return False def __hash__(self): @@ -1016,9 +1045,8 @@ def __bool__(self): """ Test whether ``self`` is not zero. - An uninitialized series returns ``True`` as it is considered - as a formal variable, such as a generator of a polynomial - ring. + When the halting precision is infinite, then any series that is + not known to be zero will be ``True``. TESTS:: @@ -1082,18 +1110,52 @@ def __bool__(self): sage: g.define(1 + z*g) sage: bool(g) True + + Comparison with finite halting precision:: + + sage: M = L(lambda n: 2*n if n < 10 else 0, valuation=0) + sage: bool(M) + True + sage: M.is_zero() + False + + sage: L.options.halting_precision = 20 + sage: bool(M) + False + sage: M.is_zero() + True + + + With finite halting precision, it can be considered to + be indistinguishable from zero until possibly enough + coefficients are computed:: + + sage: L. = LazyLaurentSeriesRing(GF(2)) + sage: L.options.halting_precision = 20 + sage: f = L(lambda n: 0, valuation=0) + sage: f.is_zero() + True + + sage: g = L(lambda n: 0 if n < 50 else 1, valuation=2) + sage: bool(g) # checks up to degree 22 = 2 + 20 + False + sage: bool(g) # checks up to degree 42 = 22 + 20 + False + sage: bool(g) # checks up to degree 62 = 42 + 20 + True + sage: L.options._reset() """ if isinstance(self._coeff_stream, Stream_zero): return False + + prec = self.parent().options['halting_precision'] + if prec is None and not self.parent().options['use_unknown']: + return True + if isinstance(self._coeff_stream, Stream_exact): return True - if isinstance(self._coeff_stream, Stream_uninitialized): - if self._coeff_stream._target is None: - return True - if isinstance(self._coeff_stream._target, Stream_zero): - return False - if isinstance(self._coeff_stream._target, Stream_exact): - return True + if self._coeff_stream.is_undefined(): + return True if self._coeff_stream._is_sparse: cache = self._coeff_stream._cache if any(cache[a] for a in cache): @@ -1102,15 +1164,57 @@ def __bool__(self): if any(self._coeff_stream._cache): return True - v = self._coeff_stream._approximate_order - if self[v]: - return True - - prec = self.parent().options['halting_precision'] if prec is None: - raise ValueError("undecidable as lazy Laurent series") + raise UnknownError("undecidable") + v = self._coeff_stream._approximate_order return any(self[i] for i in range(v, v + prec)) + def is_nonzero(self): + """ + Return ``True`` if ``self`` is known to be nonzero. + + EXAMPLES: + + A series that it not known to be nonzero with no halting precision:: + + sage: L. = LazyLaurentSeriesRing(GF(2)) + sage: f = L(lambda n: 0, valuation=0) + sage: f.is_nonzero() + False + sage: bool(f) + True + sage: g = L(lambda n: 0 if n < 50 else 1, valuation=2) + sage: g.is_nonzero() + False + sage: g[60] + 1 + sage: g.is_nonzero() + True + + With finite halting precision, it can be considered to + be indistinguishable from zero until possibly enough + coefficients are computed:: + + sage: L.options.halting_precision = 20 + sage: f = L(lambda n: 0, valuation=0) + sage: f.is_zero() + True + + sage: g = L(lambda n: 0 if n < 50 else 1, valuation=2) + sage: g.is_nonzero() # checks up to degree 22 = 2 + 20 + False + sage: g.is_nonzero() # checks up to degree 42 = 22 + 20 + False + sage: g.is_nonzero() # checks up to degree 62 = 42 + 20 + True + sage: L.options._reset() + """ + if self._coeff_stream.is_nonzero(): + return True + if self.parent().options['halting_precision'] is not None: + return bool(self) + return False + def define(self, s): r""" Define an equation by ``self = s``. @@ -1358,6 +1462,24 @@ def define(self, s): sage: (f*s[1]).revert() + 1 - f # needs lrcalc_python sage.combinat O^7 + Undefined series inside of another series (see :issue:`35071`):: + + sage: L. = LazyPowerSeriesRing(QQ) + sage: f = z^2 + sage: b = L.undefined(valuation=1) + sage: b.define(z*f(f(b))) + sage: b + O(z^8) + + sage: L. = LazyPowerSeriesRing(ZZ) + sage: f = L.undefined() + sage: f.define(L(lambda n: 0 if not n else sigma(f[n-1]+1))) + sage: f + x + 3*x^2 + 7*x^3 + 15*x^4 + 31*x^5 + 63*x^6 + O(x^7) + sage: f = L.undefined() + sage: f.define((1/(1-L(lambda n: 0 if not n else sigma(f[n-1]+1))))) + sage: f + 1 + 3*x + 16*x^2 + 87*x^3 + 607*x^4 + 4518*x^5 + 30549*x^6 + O(x^7) """ if not isinstance(self._coeff_stream, Stream_uninitialized) or self._coeff_stream._target is not None: raise ValueError("series already defined") @@ -1404,7 +1526,9 @@ def _repr_(self): sage: L(lambda x: x if x > 0 else 0, valuation=-10) O(z^-3) - sage: L.undefined(valuation=0) + sage: s = L.undefined(valuation=0); s + Uninitialized Lazy Laurent Series + sage: (s + s^2).map_coefficients(lambda f: f % 3) Uninitialized Lazy Laurent Series sage: L(0) 0 @@ -1418,7 +1542,7 @@ def _repr_(self): """ if isinstance(self._coeff_stream, Stream_zero): return '0' - if isinstance(self._coeff_stream, Stream_uninitialized) and self._coeff_stream._target is None: + if self._coeff_stream.is_undefined(): return 'Uninitialized Lazy Laurent Series' return self._format_series(repr) @@ -1451,7 +1575,10 @@ def _latex_(self): sage: latex(L(lambda x: x if x > 0 else 0, valuation=-10)) O(\frac{1}{z^{3}}) - sage: latex(L.undefined(valuation=0)) + sage: s = L.undefined(valuation=0) + sage: latex(s) + \text{\texttt{Undef}} + sage: latex((s + s^2).map_coefficients(lambda f: f % 3)) \text{\texttt{Undef}} sage: latex(L(0)) 0 @@ -1468,7 +1595,7 @@ def _latex_(self): from sage.misc.latex import latex if isinstance(self._coeff_stream, Stream_zero): return latex('0') - if isinstance(self._coeff_stream, Stream_uninitialized) and self._coeff_stream._target is None: + if self._coeff_stream.is_undefined(): return latex("Undef") return self._format_series(latex) @@ -1792,9 +1919,17 @@ def _acted_upon_(self, scalar, self_on_left): Different scalars potentially give different series:: sage: 2 * M == 3 * M - Traceback (most recent call last): - ... - ValueError: undecidable + False + + sage: L.options.use_unknown = True + sage: 2 * M == 3 * M + Unknown + + sage: L.options.halting_precision = 30 + sage: 2 * M == 3 * M + False + + sage: L.options._reset() Sparse series can be multiplied with a scalar:: @@ -3207,8 +3342,28 @@ def _div_(self, other): sage: f / f s[] + Dividing when the coefficient ring is a lazy Dirichlet ring:: + + sage: D = LazyDirichletSeriesRing(QQ, "s") + sage: zeta = D(constant=1) + sage: L. = LazyLaurentSeriesRing(D) + sage: 1 / (1 - t*zeta) + (1 + O(1/(8^s))) + + (1 + 1/(2^s) + 1/(3^s) + 1/(4^s) + 1/(5^s) + 1/(6^s) + 1/(7^s) + O(1/(8^s)))*t + + ... + O(t^7) + + Check for dividing by other type of `0` series:: + + sage: L. = LazyPowerSeriesRing(QQ) + sage: f = L(lambda n: 0, valuation=0) + sage: L.options.halting_precision = 20 + sage: 1 / f + Traceback (most recent call last): + ... + ZeroDivisionError: cannot divide by 0 + sage: L.options._reset() """ - if isinstance(other._coeff_stream, Stream_zero): + if not other: raise ZeroDivisionError("cannot divide by 0") P = self.parent() diff --git a/src/sage/rings/lazy_series_ring.py b/src/sage/rings/lazy_series_ring.py index c36328731df..6e1e402e38d 100644 --- a/src/sage/rings/lazy_series_ring.py +++ b/src/sage/rings/lazy_series_ring.py @@ -19,6 +19,21 @@ :class:`sage.rings.padics.generic_nodes.pAdicRelaxedGeneric`, :func:`sage.rings.padics.factory.ZpER` +.. WARNING:: + + When the halting precision is infinite, the default for ``bool(f)`` + is ``True`` for any lazy series ``f`` that is not known to be zero. + This could end up resulting in infinite loops:: + + sage: L. = LazyPowerSeriesRing(ZZ) + sage: f = L(lambda n: 0, valuation=0) + sage: 1 / f # not tested - infinite loop + +.. SEEALSO:: + + The examples of :class:`LazyLaurentSeriesRing` contain a discussion + about the different methods of comparisons the lazy series can use. + AUTHORS: - Kwankyu Lee (2019-02-24): initial version @@ -489,7 +504,7 @@ def _element_constructor_(self, x=None, valuation=None, degree=None, constant=No if valuation is None: raise ValueError("you must specify the degree for the polynomial 0") degree = valuation - if x == R.zero(): + if not x: coeff_stream = Stream_exact([], order=degree, constant=constant) return self.element_class(self, coeff_stream) initial_coefficients = [x[i] for i in range(x.valuation(), x.degree() + 1)] @@ -662,6 +677,7 @@ class options(GlobalOptions): - constant_length: 3 - display_length: 7 - halting_precision: None + - use_unknown: False sage: LLS.options.display_length 7 @@ -695,8 +711,11 @@ class options(GlobalOptions): description='the number of coefficients to display for nonzero constant series', checker=lambda x: x in ZZ and x > 0) halting_precision = dict(default=None, - description='the number of coefficients, beginning with the approximate valuation, to check in equality tests', - checker=lambda x: x is None or x in ZZ and x > 0) + description='the number of coefficients, beginning with the approximate valuation, to check in equality tests', + checker=lambda x: x is None or x in ZZ and x > 0) + use_unknown = dict(default=False, + description='whether to raise an error when a comparison is unknown', + checker=lambda x: x is True or x is False) @cached_method def one(self): @@ -1142,36 +1161,98 @@ class LazyLaurentSeriesRing(LazySeriesRing): sage: s 1 + z + 2*z^2 + 5*z^3 + 14*z^4 + 42*z^5 + 132*z^6 + O(z^7) - If the series is not specified by a finite number of initial - coefficients and a constant for the remaining coefficients, then - equality checking will depend on the coefficients which have - already been computed. If this information is not enough to - check that two series are different we raise an error:: + By default, any two series ``f`` and ``g`` that are not known to + be equal are considered to be different:: + + sage: f = L(lambda n: 0, valuation=0) + sage: f == 0 + False + + .. WARNING:: + + We have imposed that ``(f == g) == not (f != g)``, and so + ``f != g`` returning ``True`` might not mean that the two + series are actually different:: + + sage: g = L.zero() + sage: f != g + True + + This can be verified by :meth:`~sage.rings.lazy_series.is_nonzero()`, + which only returns ``True`` if the series is known to be nonzero:: + + sage: (f - g).is_nonzero() + False + + The implementation of the ring can be either be a sparse or a dense one. + The default is a sparse implementation:: + sage: L. = LazyLaurentSeriesRing(ZZ) + sage: L.is_sparse() + True + sage: L. = LazyLaurentSeriesRing(ZZ, sparse=False) + sage: L.is_sparse() + False + + We additionally provide two other methods of performing comparisons. + The first is returning an :class:`Unknown` and the second uses a check + up to a (user set) finite precision. These behaviors are set using the + options ``use_unknown`` and ``halting_precision``. In particular, + this applies to series that are not specified by a finite number + of initial coefficients and a constant for the remaining coefficients. + Equality checking will depend on the coefficients which have + already been computed. If this information is not enough to + check that two series are different, then if ``L.options.use_unknown`` + is set to ``True``, then we return an :class:`Unknown`:: + + sage: L.options.use_unknown = True sage: f = 1 / (z + z^2); f z^-1 - 1 + z - z^2 + z^3 - z^4 + z^5 + O(z^6) sage: f2 = f * 2 # currently no coefficients computed sage: f3 = f * 3 # currently no coefficients computed sage: f2 == f3 - Traceback (most recent call last): - ... - ValueError: undecidable + Unknown sage: f2 # computes some of the coefficients of f2 2*z^-1 - 2 + 2*z - 2*z^2 + 2*z^3 - 2*z^4 + 2*z^5 + O(z^6) sage: f3 # computes some of the coefficients of f3 3*z^-1 - 3 + 3*z - 3*z^2 + 3*z^3 - 3*z^4 + 3*z^5 + O(z^6) sage: f2 == f3 False + sage: f2a = f + f + sage: f2 == f2a + Unknown + sage: zf = L(lambda n: 0, valuation=0) + sage: zf == 0 + Unknown - The implementation of the ring can be either be a sparse or a dense one. - The default is a sparse implementation:: + For boolean checks, an error is raised when it is not known to be nonzero:: - sage: L. = LazyLaurentSeriesRing(ZZ) - sage: L.is_sparse() - True - sage: L. = LazyLaurentSeriesRing(ZZ, sparse=False) - sage: L.is_sparse() + sage: bool(zf) + Traceback (most recent call last): + ... + UnknownError: undecidable + + If the halting precision is set to a finite number `p` (for unlimited + precision, it is set to ``None``), then it will check up to `p` values + from the current position:: + + sage: L.options.halting_precision = 20 + sage: f2 = f * 2 # currently no coefficients computed + sage: f3 = f * 3 # currently no coefficients computed + sage: f2 == f3 False + sage: f2a = f + f + sage: f2 == f2a + True + sage: zf = L(lambda n: 0, valuation=0) + sage: zf == 0 + True + + TESTS: + + We reset the options:: + + sage: L.options._reset() """ Element = LazyLaurentSeries From ea00ee70af2bcb303fcf92e03e6ec02512545459 Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Wed, 12 Apr 2023 17:48:42 +0900 Subject: [PATCH 2/8] Alternative proposal returning None for unknown comparisons. --- src/sage/data_structures/stream.py | 2 +- src/sage/rings/lazy_series.py | 36 ++++++++++++++++-------------- src/sage/rings/lazy_series_ring.py | 26 ++++++++++----------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index 735398cbcca..b35c865d060 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -665,7 +665,7 @@ def __init__(self, initial_coefficients, constant=None, degree=None, order=None) if order + len(initial_coefficients) == self._degree: # Strip off the constant values at the end for w in reversed(initial_coefficients): - if w != self._constant: + if not (w == self._constant): break initial_coefficients.pop() self._degree -= 1 diff --git a/src/sage/rings/lazy_series.py b/src/sage/rings/lazy_series.py index a8dd02146e0..9220b768da5 100644 --- a/src/sage/rings/lazy_series.py +++ b/src/sage/rings/lazy_series.py @@ -939,21 +939,21 @@ def _richcmp_(self, other, op): False sage: fz = L(lambda n: 0, valuation=0) - sage: L.zero() == fz - False - sage: fz == L.zero() - False + sage: (L.zero() == fz) is None + True + sage: (fz == L.zero()) is None + True With using :class:`Unknown`:: sage: L.options.use_unknown = True sage: fz = L(lambda n: 0, valuation=0) - sage: L.zero() == fz - Unknown - sage: fz == L.zero() - Unknown - sage: fz != L.zero() - Unknown + sage: (L.zero() == fz) is None + True + sage: (fz == L.zero()) is None + True + sage: (fz != L.zero()) is None + True With using finite halting precision:: @@ -1009,7 +1009,8 @@ def _richcmp_(self, other, op): # undecidable otherwise prec = self.parent().options['halting_precision'] if prec is None: - return Unknown + return None + #return Unknown # raise UnknownError("undecidable") # at least one of the approximate orders is not infinity m = min(self._coeff_stream._approximate_order, @@ -1018,7 +1019,7 @@ def _richcmp_(self, other, op): if op is op_NE: ret = (self == other) - if ret is Unknown: + if ret is None: return ret return not ret @@ -1165,7 +1166,8 @@ def __bool__(self): return True if prec is None: - raise UnknownError("undecidable") + return True + #raise UnknownError("undecidable") v = self._coeff_stream._approximate_order return any(self[i] for i in range(v, v + prec)) @@ -1918,12 +1920,12 @@ def _acted_upon_(self, scalar, self_on_left): Different scalars potentially give different series:: - sage: 2 * M == 3 * M - False + sage: (2 * M == 3 * M) is None + True sage: L.options.use_unknown = True - sage: 2 * M == 3 * M - Unknown + sage: (2 * M == 3 * M) is None + True sage: L.options.halting_precision = 30 sage: 2 * M == 3 * M diff --git a/src/sage/rings/lazy_series_ring.py b/src/sage/rings/lazy_series_ring.py index 6e1e402e38d..25379e67147 100644 --- a/src/sage/rings/lazy_series_ring.py +++ b/src/sage/rings/lazy_series_ring.py @@ -677,7 +677,7 @@ class options(GlobalOptions): - constant_length: 3 - display_length: 7 - halting_precision: None - - use_unknown: False + - use_unknown: True sage: LLS.options.display_length 7 @@ -713,7 +713,7 @@ class options(GlobalOptions): halting_precision = dict(default=None, description='the number of coefficients, beginning with the approximate valuation, to check in equality tests', checker=lambda x: x is None or x in ZZ and x > 0) - use_unknown = dict(default=False, + use_unknown = dict(default=True, description='whether to raise an error when a comparison is unknown', checker=lambda x: x is True or x is False) @@ -1165,8 +1165,8 @@ class LazyLaurentSeriesRing(LazySeriesRing): be equal are considered to be different:: sage: f = L(lambda n: 0, valuation=0) - sage: f == 0 - False + sage: (f == 0) is None + True .. WARNING:: @@ -1175,7 +1175,7 @@ class LazyLaurentSeriesRing(LazySeriesRing): series are actually different:: sage: g = L.zero() - sage: f != g + sage: (f != g) is None True This can be verified by :meth:`~sage.rings.lazy_series.is_nonzero()`, @@ -1210,8 +1210,8 @@ class LazyLaurentSeriesRing(LazySeriesRing): z^-1 - 1 + z - z^2 + z^3 - z^4 + z^5 + O(z^6) sage: f2 = f * 2 # currently no coefficients computed sage: f3 = f * 3 # currently no coefficients computed - sage: f2 == f3 - Unknown + sage: (f2 == f3) is None + True sage: f2 # computes some of the coefficients of f2 2*z^-1 - 2 + 2*z - 2*z^2 + 2*z^3 - 2*z^4 + 2*z^5 + O(z^6) sage: f3 # computes some of the coefficients of f3 @@ -1219,18 +1219,16 @@ class LazyLaurentSeriesRing(LazySeriesRing): sage: f2 == f3 False sage: f2a = f + f - sage: f2 == f2a - Unknown + sage: (f2 == f2a) is None + True sage: zf = L(lambda n: 0, valuation=0) - sage: zf == 0 - Unknown + sage: (zf == 0) is None + True For boolean checks, an error is raised when it is not known to be nonzero:: sage: bool(zf) - Traceback (most recent call last): - ... - UnknownError: undecidable + True If the halting precision is set to a finite number `p` (for unlimited precision, it is set to ``None``), then it will check up to `p` values From 2095704c51c5440f637ad1b1433ff9803868e3a4 Mon Sep 17 00:00:00 2001 From: Martin Rubey Date: Sun, 20 Aug 2023 17:50:22 +0200 Subject: [PATCH 3/8] unify docstrings and uncontroversial doctests from #35480 and #35485 --- src/sage/data_structures/stream.py | 65 ++++++++++++++---------- src/sage/rings/lazy_series.py | 79 ++++++++++++++++++++++-------- src/sage/rings/lazy_series_ring.py | 6 +-- 3 files changed, 101 insertions(+), 49 deletions(-) diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index b35c865d060..641f330a5ba 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -186,7 +186,7 @@ def __ne__(self, other): def is_nonzero(self): r""" Return ``True`` if and only if this stream is known - to be nonzero. + to be non-zero. The default implementation is ``False``. @@ -243,7 +243,7 @@ def __init__(self, is_sparse, true_order): def is_nonzero(self): r""" - Return ``True`` if and only if the cache contains a nonzero element. + Return ``True`` if and only if the cache contains a non-zero element. EXAMPLES:: @@ -332,7 +332,7 @@ def __setstate__(self, d): def __getitem__(self, n): """ - Return the `n`-th coefficient of ``self``. + Return the ``n``-th coefficient of ``self``. INPUT: @@ -433,7 +433,7 @@ def iterate_coefficients(self): def order(self): r""" Return the order of ``self``, which is the minimum index ``n`` such - that ``self[n]`` is nonzero. + that ``self[n]`` is non-zero. EXAMPLES:: @@ -659,7 +659,7 @@ def __init__(self, initial_coefficients, constant=None, degree=None, order=None) # complicated otherwise for i, v in enumerate(initial_coefficients): if v: - # We have found the first nonzero coefficient + # We have found the first non-zero coefficient order += i initial_coefficients = initial_coefficients[i:] if order + len(initial_coefficients) == self._degree: @@ -738,7 +738,7 @@ def __getitem__(self, n): def order(self): r""" Return the order of ``self``, which is the minimum index - ``n`` such that ``self[n]`` is nonzero. + ``n`` such that ``self[n]`` is non-zero. EXAMPLES:: @@ -869,9 +869,9 @@ def __ne__(self, other): def is_nonzero(self): r""" Return ``True`` if and only if this stream is known - to be nonzero. + to be non-zero. - An assumption of this class is that it is nonzero. + An assumption of this class is that it is non-zero. EXAMPLES:: @@ -1578,7 +1578,7 @@ def get_coefficient(self, n): def is_nonzero(self): r""" Return ``True`` if and only if this stream is known - to be nonzero. + to be non-zero. EXAMPLES:: @@ -1596,6 +1596,14 @@ def is_nonzero(self): return self._left.is_nonzero() and self._right.is_nonzero() +class Stream_cauchy_mul_commutative(Stream_cauchy_mul, Stream_binaryCommutative): + """ + Operator for multiplication of two coefficient streams using the + Cauchy product for commutative multiplication of coefficients. + """ + pass + + class Stream_dirichlet_convolve(Stream_binary): r""" Operator for the Dirichlet convolution of two streams. @@ -1644,7 +1652,7 @@ def _approximate_order(self): or self._right._approximate_order <= 0): raise ValueError("Dirichlet convolution is only defined for " "coefficient streams with minimal index of " - "nonzero coefficient at least 1") + "non-zero coefficient at least 1") return self._left._approximate_order * self._right._approximate_order def get_coefficient(self, n): @@ -2236,6 +2244,11 @@ class Stream_scalar(Stream_inexact): Base class for operators multiplying a coefficient stream by a scalar. + INPUT: + + - ``series`` -- a :class:`Stream` + - ``scalar`` -- a non-zero, non-one scalar + .. TODO:: This does not inherit from :class:`Stream_unary`, because of @@ -2322,7 +2335,7 @@ def __eq__(self, other): def is_nonzero(self): r""" Return ``True`` if and only if this stream is known - to be nonzero. + to be non-zero. EXAMPLES:: @@ -2355,7 +2368,7 @@ class Stream_rmul(Stream_scalar): INPUT: - ``series`` -- a :class:`Stream` - - ``scalar`` -- a non-zero scalar + - ``scalar`` -- a non-zero, non-one scalar EXAMPLES:: @@ -2397,7 +2410,7 @@ class Stream_lmul(Stream_scalar): INPUT: - ``series`` -- a :class:`Stream` - - ``scalar`` -- a non-zero scalar + - ``scalar`` -- a non-zero, non-one scalar EXAMPLES:: @@ -2504,7 +2517,7 @@ def get_coefficient(self, n): def is_nonzero(self): r""" Return ``True`` if and only if this stream is known - to be nonzero. + to be non-zero. EXAMPLES:: @@ -2643,9 +2656,9 @@ def iterate_coefficients(self): def is_nonzero(self): r""" Return ``True`` if and only if this stream is known - to be nonzero. + to be non-zero. - An assumption of this class is that it is nonzero. + An assumption of this class is that it is non-zero. EXAMPLES:: @@ -2660,7 +2673,7 @@ def is_nonzero(self): class Stream_map_coefficients(Stream_inexact): r""" - The stream with ``function`` applied to each nonzero coefficient + The stream with ``function`` applied to each non-zero coefficient of ``series``. INPUT: @@ -2787,7 +2800,7 @@ def is_undefined(self): class Stream_shift(Stream): """ - Operator for shifting a nonzero, nonexact stream. + Operator for shifting a non-zero, non-exact stream. Instances of this class share the cache with its input stream. @@ -2833,7 +2846,7 @@ def _approximate_order(self): def order(self): r""" Return the order of ``self``, which is the minimum index - ``n`` such that ``self[n]`` is nonzero. + ``n`` such that ``self[n]`` is non-zero. EXAMPLES:: @@ -2903,9 +2916,9 @@ def __eq__(self, other): def is_nonzero(self): r""" Return ``True`` if and only if this stream is known - to be nonzero. + to be non-zero. - An assumption of this class is that it is nonzero. + An assumption of this class is that it is non-zero. EXAMPLES:: @@ -2925,7 +2938,7 @@ def is_undefined(self): class Stream_truncated(Stream_inexact): """ - Operator for shifting a nonzero, nonexact stream that has + Operator for shifting a non-zero, non-exact stream that has been shifted below its minimal valuation. Instances of this class share the cache with its input stream. @@ -3086,7 +3099,7 @@ def __eq__(self, other): def order(self): """ Return the order of ``self``, which is the minimum index ``n`` such - that ``self[n]`` is nonzero. + that ``self[n]`` is non-zero. EXAMPLES:: @@ -3132,7 +3145,7 @@ def order(self): def is_nonzero(self): r""" Return ``True`` if and only if this stream is known - to be nonzero. + to be non-zero. EXAMPLES:: @@ -3180,7 +3193,7 @@ def is_undefined(self): class Stream_derivative(Stream_inexact): """ - Operator for taking derivatives of a stream. + Operator for taking derivatives of a non-exact stream. INPUT: @@ -3293,7 +3306,7 @@ def __eq__(self, other): def is_nonzero(self): r""" Return ``True`` if and only if this stream is known - to be nonzero. + to be non-zero. EXAMPLES:: diff --git a/src/sage/rings/lazy_series.py b/src/sage/rings/lazy_series.py index 9220b768da5..8a7f98abfd0 100644 --- a/src/sage/rings/lazy_series.py +++ b/src/sage/rings/lazy_series.py @@ -196,6 +196,19 @@ sage: L = LazySymmetricFunctions(s) # needs sage.combinat sage: check(L, lambda n: sum(k*s(la) for k, la in enumerate(Partitions(n))), # needs sage.combinat ....: valuation=0) + +Check that we can invert matrices:: + + sage: L. = LazyLaurentSeriesRing(QQ) + sage: a11 = 1 + L(lambda n: 1 if not n else 0, valuation=0) + sage: a12 = 1 + L(lambda n: 1 if n == 1 else 0, valuation=0) + sage: a21 = 1 + L(lambda n: 1 if n == 2 else 0, valuation=0) + sage: a22 = 1 + L(lambda n: 1 if n == 3 else 0, valuation=0) + sage: m = matrix([[a11, a12], [a21, a22]]) + sage: m.inverse() + [ 1 + z + 2*z^2 + 3*z^3 + 4*z^4 + 5*z^5 + 6*z^6 + O(z^7) -1 - 2*z - 3*z^2 - 4*z^3 - 5*z^4 - 6*z^5 - 7*z^6 + O(z^7)] + [ -1 - z - 3*z^2 - 3*z^3 - 5*z^4 - 5*z^5 - 7*z^6 + O(z^7) 2 + 2*z + 4*z^2 + 4*z^3 + 6*z^4 + 6*z^5 + 8*z^6 + O(z^7)] + """ # **************************************************************************** @@ -219,6 +232,7 @@ from sage.combinat.partition import Partition, Partitions from sage.misc.derivative import derivative_parse from sage.categories.integral_domains import IntegralDomains +from sage.categories.rings import Rings from sage.rings.infinity import infinity from sage.rings.integer_ring import ZZ from sage.rings.rational_field import QQ @@ -228,6 +242,7 @@ from sage.data_structures.stream import ( Stream_add, Stream_cauchy_mul, + Stream_cauchy_mul_commutative, Stream_sub, Stream_cauchy_compose, Stream_lmul, @@ -1064,11 +1079,9 @@ def __bool__(self): False sage: M = L(lambda n: 2*n if n < 10 else 1, valuation=0); M O(z^7) - sage: bool(M) - Traceback (most recent call last): - ... - ValueError: undecidable as lazy Laurent series - sage: M[15] + sage: bool(M) # optional - sage.rings.finite_rings + True + sage: M[15] # optional - sage.rings.finite_rings 1 sage: bool(M) True @@ -1077,11 +1090,9 @@ def __bool__(self): sage: L. = LazyLaurentSeriesRing(GF(2), sparse=True) sage: M = L(lambda n: 2*n if n < 10 else 1, valuation=0); M O(z^7) - sage: bool(M) - Traceback (most recent call last): - ... - ValueError: undecidable as lazy Laurent series - sage: M[15] + sage: bool(M) # optional - sage.rings.finite_rings + True + sage: M[15] # optional - sage.rings.finite_rings 1 sage: bool(M) True @@ -1237,7 +1248,7 @@ def define(self, s): sage: binomial(2000, 1000) / C[1000] # needs sage.symbolic 1001 - The Catalan numbers but with a valuation 1:: + The Catalan numbers but with a valuation `1`:: sage: B = L.undefined(valuation=1) sage: B.define(z + B^2) @@ -2002,6 +2013,19 @@ def _acted_upon_(self, scalar, self_on_left): sage: f = L(constant=2) sage: 2*f 0 + + Check that non-commutativity is taken into account:: + + sage: M = MatrixSpace(ZZ, 2) + sage: L. = LazyPowerSeriesRing(M) + sage: f = L(lambda n: matrix([[1,n],[0,1]])) + sage: m = matrix([[1,0],[1,1]]) + sage: (m * f - f * m)[1] + [-1 0] + [ 0 1] + sage: m * f[1] - f[1] * m + [-1 0] + [ 0 1] """ # With the current design, the coercion model does not have # enough information to detect a priori that this method only @@ -2042,7 +2066,7 @@ def _acted_upon_(self, scalar, self_on_left): order=v, constant=c, degree=coeff_stream._degree)) - if self_on_left or R.is_commutative(): + if self_on_left or R in Rings().Commutative(): return P.element_class(P, Stream_lmul(coeff_stream, scalar, P.is_sparse())) return P.element_class(P, Stream_rmul(coeff_stream, scalar, @@ -3010,6 +3034,9 @@ def _mul_(self, other): and right.order() == 0 and not right._constant): return self # right == 1 + if ((isinstance(left, Stream_cauchy_invert) and left._series == right) + or (isinstance(right, Stream_cauchy_invert) and right._series == left)): + return P.one() # The product is exact if and only if both factors are exact # and one of the factors has eventually 0 coefficients: # (p + a x^d/(1-x))(q + b x^e/(1-x)) @@ -3057,7 +3084,11 @@ def _mul_(self, other): constant=c) return P.element_class(P, coeff_stream) - return P.element_class(P, Stream_cauchy_mul(left, right, P.is_sparse())) + if P in Rings().Commutative(): + coeff_stream = Stream_cauchy_mul_commutative(left, right, P.is_sparse()) + else: + coeff_stream = Stream_cauchy_mul(left, right, P.is_sparse()) + return P.element_class(P, coeff_stream) def __pow__(self, n): r""" @@ -3200,7 +3231,7 @@ def __invert__(self): sage: g = L([2], valuation=-1, constant=1); g 2*x^-1 + 1 + x + x^2 + O(x^3) sage: g * g^-1 - 1 + O(x^7) + 1 sage: L. = LazyPowerSeriesRing(QQ) sage: ~(x + x^2) @@ -3383,7 +3414,7 @@ def _div_(self, other): return self # self is right - if left is right: + if left == right: return P.one() if (P._minimal_valuation is not None @@ -3458,7 +3489,11 @@ def _div_(self, other): # P._minimal_valuation is zero, because we allow division by # series of positive valuation right_inverse = Stream_cauchy_invert(right) - return P.element_class(P, Stream_cauchy_mul(left, right_inverse, P.is_sparse())) + if P in Rings().Commutative(): + coeff_stream = Stream_cauchy_mul_commutative(left, right_inverse, P.is_sparse()) + else: + coeff_stream = Stream_cauchy_mul(left, right_inverse, P.is_sparse()) + return P.element_class(P, coeff_stream) def _floordiv_(self, other): r""" @@ -3549,7 +3584,9 @@ def exp(self): d_self = Stream_function(lambda n: (n + 1) * coeff_stream[n + 1], False, 0) f = P.undefined(valuation=0) - d_self_f = Stream_cauchy_mul(d_self, f._coeff_stream, False) + # d_self and f._coeff_stream always commute, the coefficients + # of the product are of the form sum_{k=1}^n a_k a_{n+1-k}. + d_self_f = Stream_cauchy_mul_commutative(d_self, f._coeff_stream, False) int_d_self_f = Stream_function(lambda n: d_self_f[n-1] / R(n) if n else R.one(), False, 0) f._coeff_stream._target = int_d_self_f @@ -3598,9 +3635,11 @@ def log(self): # multivariate power series d_self = Stream_function(lambda n: (n + 1) * coeff_stream[n + 1], P.is_sparse(), 0) - d_self_quo_self = Stream_cauchy_mul(d_self, - Stream_cauchy_invert(coeff_stream), - P.is_sparse()) + coeff_stream_inverse = Stream_cauchy_invert(coeff_stream) + # d_self and coeff_stream_inverse always commute + d_self_quo_self = Stream_cauchy_mul_commutative(d_self, + coeff_stream_inverse, + P.is_sparse()) int_d_self_quo_self = Stream_function(lambda n: d_self_quo_self[n-1] / R(n), P.is_sparse(), 1) return P.element_class(P, int_d_self_quo_self) diff --git a/src/sage/rings/lazy_series_ring.py b/src/sage/rings/lazy_series_ring.py index 25379e67147..b8b3095297e 100644 --- a/src/sage/rings/lazy_series_ring.py +++ b/src/sage/rings/lazy_series_ring.py @@ -2400,10 +2400,10 @@ def __init__(self, basis, sparse=True, category=None): if basis not in GradedAlgebrasWithBasis: raise ValueError("basis should be in GradedAlgebrasWithBasis") self._arity = 1 - category = Algebras(base_ring.category()) - if base_ring in IntegralDomains(): + category = Algebras(basis.category()) + if basis in IntegralDomains(): category &= IntegralDomains() - elif base_ring in Rings().Commutative(): + elif basis in Rings().Commutative(): category = category.Commutative() if base_ring.is_zero(): From a8eca6209bfc296ab8d6879bc989235acb48c2b2 Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Wed, 12 Apr 2023 17:23:13 +0900 Subject: [PATCH 4/8] Removing redundant comparisons, renaming error comparisons (following padics), adding is_unitialized() doctests. --- src/sage/data_structures/stream.py | 172 ++++++++++++++++++++++----- src/sage/rings/lazy_series.py | 180 +++++++++++++++++------------ src/sage/rings/lazy_series_ring.py | 54 ++++++--- 3 files changed, 281 insertions(+), 125 deletions(-) diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index 641f330a5ba..7038ce2149c 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -199,9 +199,21 @@ def is_nonzero(self): """ return False - def is_undefined(self): + def is_trivially_ne(self, other): """ - Return ``True`` if ``self`` is an undefined stream. + Return if ``self`` is trivially not equal to ``other``. + + The default implementation is ``False``. + + .. NOTE:: + + This does not check that the streams are equal. + """ + return False + + def is_uninitialized(self): + """ + Return ``True`` if ``self`` is an uninitialized stream. """ return False @@ -542,7 +554,10 @@ def __ne__(self, other): # TODO: more cases, in particular mixed implementations, # could be detected if not isinstance(other, Stream_inexact): - return False + return (other != self) + + if self.is_uninitialized() != other.is_uninitialized(): + return True if self._is_sparse and other._is_sparse: for i in self._cache: @@ -746,7 +761,6 @@ def order(self): sage: s = Stream_exact([1]) sage: s.order() 0 - """ return self._approximate_order @@ -843,13 +857,17 @@ def __ne__(self, other): [0, 0, 0, 2, 1, 1, 1, 1] sage: [f[i] for i in range(-3, 5)] [0, 0, 0, 2, 1, 1, 1, 1] - """ if isinstance(other, type(self)): return (self._degree != other._degree or self._approximate_order != other._approximate_order or self._initial_coefficients != other._initial_coefficients or self._constant != other._constant) + if other.is_uninitialized(): + return True + if isinstance(other, Stream_zero): + # We are assumed to be nonzero + return True # if other is not exact, we can at least compare with the # elements in its cache if other._is_sparse: @@ -1009,7 +1027,6 @@ class Stream_uninitialized(Stream_inexact): sage: C._target = one sage: C[4] 0 - """ def __init__(self, approximate_order, true_order=False): """ @@ -1048,9 +1065,16 @@ def iterate_coefficients(self): yield self._target[n] n += 1 - def is_undefined(self): + def is_uninitialized(self): """ - Return ``True`` if ``self`` is an undefined stream. + Return ``True`` if ``self`` is an uninitialized stream. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_uninitialized + sage: C = Stream_uninitialized(0) + sage: C.is_uninitialized() + True """ return self._target is None @@ -1074,7 +1098,6 @@ class Stream_unary(Stream_inexact): sage: [g[i] for i in range(10)] [0, 4, 8, 12, 16, 20, 24, 28, 32, 36] """ - def __init__(self, series, is_sparse): """ Initialize ``self``. @@ -1130,11 +1153,19 @@ def __eq__(self, other): """ return isinstance(other, type(self)) and self._series == other._series - def is_undefined(self): + def is_uninitialized(self): """ - Return ``True`` if ``self`` is an undefined stream. + Return ``True`` if ``self`` is an uninitialized stream. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_uninitialized, Stream_unary + sage: C = Stream_uninitialized(0) + sage: M = Stream_unary(C, True) + sage: M.is_uninitialized() + True """ - return self._series.is_undefined() + return self._series.is_uninitialized() class Stream_binary(Stream_inexact): @@ -1224,11 +1255,23 @@ def __eq__(self, other): return False return self._left == other._left and self._right == other._right - def is_undefined(self): + def is_uninitialized(self): """ - Return ``True`` if ``self`` is an undefined stream. + Return ``True`` if ``self`` is an uninitialized stream. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_uninitialized, Stream_sub, Stream_function + sage: C = Stream_uninitialized(0) + sage: F = Stream_function(lambda n: n, True, 0) + sage: B = Stream_sub(F, C, True) + sage: B.is_uninitialized() + True + sage: Bp = Stream_sub(F, F, True) + sage: Bp.is_uninitialized() + False """ - return self._left.is_undefined() or self._right.is_undefined() + return self._left.is_uninitialized() or self._right.is_uninitialized() class Stream_binaryCommutative(Stream_binary): @@ -1370,6 +1413,33 @@ def __eq__(self, other): """ return self is other or isinstance(other, Stream_zero) + def __ne__(self, other): + """ + Return whether ``self`` and ``other`` are known to be not equal. + + INPUT: + + - ``other`` -- a stream + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_zero, Stream_function + sage: Stream_zero() != Stream_zero() + False + sage: f = Stream_function(lambda n: 2*n, True, 0) + sage: Stream_zero() != f + False + sage: f[0] + 0 + sage: Stream_zero() != f + False + sage: f[1] + 2 + sage: Stream_zero() != f + True + """ + return self is not other and not isinstance(other, Stream_zero) and other.is_nonzero() + def __hash__(self): """ Return the hash of ``self``. @@ -2353,11 +2423,19 @@ def is_nonzero(self): """ return self._series.is_nonzero() - def is_undefined(self): + def is_uninitialized(self): """ - Return ``True`` if ``self`` is an undefined stream. + Return ``True`` if ``self`` is an uninitialized stream. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_uninitialized, Stream_lmul, Stream_function + sage: C = Stream_uninitialized(0) + sage: B = Stream_lmul(C, 2, True) + sage: B.is_uninitialized() + True """ - return self._series.is_undefined() + return self._series.is_uninitialized() class Stream_rmul(Stream_scalar): @@ -2791,11 +2869,19 @@ def __eq__(self, other): return (isinstance(other, type(self)) and self._series == other._series and self._function == other._function) - def is_undefined(self): + def is_uninitialized(self): """ - Return ``True`` if ``self`` is an undefined stream. + Return ``True`` if ``self`` is an uninitialized stream. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_uninitialized, Stream_map_coefficients + sage: C = Stream_uninitialized(0) + sage: M = Stream_map_coefficients(C, lambda n: -n, True) + sage: M.is_uninitialized() + True """ - return self._series.is_undefined() + return self._series.is_uninitialized() class Stream_shift(Stream): @@ -2930,11 +3016,20 @@ def is_nonzero(self): """ return self._series.is_nonzero() - def is_undefined(self): + def is_uninitialized(self): """ - Return ``True`` if ``self`` is an undefined stream. + Return ``True`` if ``self`` is an uninitialized stream. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_uninitialized, Stream_shift + sage: C = Stream_uninitialized(0) + sage: S = Stream_shift(C, 5) + sage: S.is_uninitialized() + True """ - return self._series.is_undefined() + return self._series.is_uninitialized() + class Stream_truncated(Stream_inexact): """ @@ -3184,11 +3279,19 @@ def is_nonzero(self): start = self._approximate_order - offset return any(self._cache[start:]) - def is_undefined(self): + def is_uninitialized(self): """ - Return ``True`` if ``self`` is an undefined stream. + Return ``True`` if ``self`` is an uninitialized stream. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_uninitialized, Stream_truncated + sage: C = Stream_uninitialized(0) + sage: S = Stream_truncated(C, -5, 3) + sage: S.is_uninitialized() + True """ - return self._series.is_undefined() + return self._series.is_uninitialized() class Stream_derivative(Stream_inexact): @@ -3275,7 +3378,6 @@ def __hash__(self): True sage: hash(f) == hash(g) False - """ return hash((type(self), self._series, self._shift)) @@ -3319,8 +3421,16 @@ def is_nonzero(self): """ return self._series.is_nonzero() - def is_undefined(self): + def is_uninitialized(self): """ - Return ``True`` if ``self`` is an undefined stream. + Return ``True`` if ``self`` is an uninitialized stream. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_uninitialized, Stream_derivative + sage: C = Stream_uninitialized(0) + sage: D = Stream_derivative(C, 1, True) + sage: D.is_uninitialized() + True """ - return self._series.is_undefined() + return self._series.is_uninitialized() diff --git a/src/sage/rings/lazy_series.py b/src/sage/rings/lazy_series.py index 8a7f98abfd0..2c982f6fc80 100644 --- a/src/sage/rings/lazy_series.py +++ b/src/sage/rings/lazy_series.py @@ -261,7 +261,6 @@ Stream_dirichlet_invert, Stream_plethysm ) -from sage.misc.unknown import Unknown, UnknownError class LazyModuleElement(Element): @@ -954,23 +953,30 @@ def _richcmp_(self, other, op): False sage: fz = L(lambda n: 0, valuation=0) - sage: (L.zero() == fz) is None - True - sage: (fz == L.zero()) is None - True + sage: L.zero() == fz + False + sage: fz == L.zero() + False - With using :class:`Unknown`:: + With using secure computations:: - sage: L.options.use_unknown = True + sage: L.options.secure = True sage: fz = L(lambda n: 0, valuation=0) - sage: (L.zero() == fz) is None - True - sage: (fz == L.zero()) is None - True - sage: (fz != L.zero()) is None - True + sage: L.zero() == fz + Traceback (most recent call last): + ... + ValueError: undecidable + sage: fz == L.zero() + Traceback (most recent call last): + ... + ValueError: undecidable + sage: fz != L.zero() + Traceback (most recent call last): + ... + ValueError: undecidable - With using finite halting precision:: + With using finite halting precision (which ignores + the ``secure`` option):: sage: L.options.halting_precision = 40 sage: fz = L(lambda n: 0, valuation=0) @@ -990,43 +996,20 @@ def _richcmp_(self, other, op): True """ if op is op_EQ: - if (not self.parent().options['use_unknown'] + if self._coeff_stream == other._coeff_stream: + return True + + if (not self.parent().options['secure'] and self.parent().options['halting_precision'] is None): - return self._coeff_stream == other._coeff_stream - - if isinstance(self._coeff_stream, Stream_zero): - if isinstance(other._coeff_stream, Stream_zero): - return True - if other._coeff_stream.is_undefined() or other._coeff_stream.is_nonzero(): - return False - elif isinstance(other._coeff_stream, Stream_zero): - if self._coeff_stream.is_undefined() or self._coeff_stream.is_nonzero(): - return False - elif isinstance(self._coeff_stream, Stream_exact): - if isinstance(other._coeff_stream, Stream_exact): - return self._coeff_stream == other._coeff_stream - if self._coeff_stream != other._coeff_stream or other._coeff_stream.is_undefined(): - return False - elif isinstance(other._coeff_stream, Stream_exact): - if other._coeff_stream != self._coeff_stream or self._coeff_stream.is_undefined(): - return False - else: - # both streams are inexact, perhaps they are equal by - # construction - if self._coeff_stream == other._coeff_stream: - return True - # perhaps their caches are different - if self._coeff_stream != other._coeff_stream: - return False - if self._coeff_stream.is_undefined() or other._coeff_stream.is_undefined(): - return False + return False + + if self._coeff_stream != other._coeff_stream: + return False # undecidable otherwise prec = self.parent().options['halting_precision'] if prec is None: - return None - #return Unknown - # raise UnknownError("undecidable") + raise ValueError("undecidable") # at least one of the approximate orders is not infinity m = min(self._coeff_stream._approximate_order, other._coeff_stream._approximate_order) @@ -1137,7 +1120,6 @@ def __bool__(self): sage: M.is_zero() True - With finite halting precision, it can be considered to be indistinguishable from zero until possibly enough coefficients are computed:: @@ -1161,30 +1143,24 @@ def __bool__(self): return False prec = self.parent().options['halting_precision'] - if prec is None and not self.parent().options['use_unknown']: + if prec is None and not self.parent().options['secure']: return True if isinstance(self._coeff_stream, Stream_exact): return True - if self._coeff_stream.is_undefined(): + if self._coeff_stream.is_uninitialized(): + return True + if self._coeff_stream.is_nonzero(): return True - if self._coeff_stream._is_sparse: - cache = self._coeff_stream._cache - if any(cache[a] for a in cache): - return True - else: - if any(self._coeff_stream._cache): - return True if prec is None: - return True - #raise UnknownError("undecidable") + raise ValueError("undecidable") v = self._coeff_stream._approximate_order return any(self[i] for i in range(v, v + prec)) def is_nonzero(self): - """ - Return ``True`` if ``self`` is known to be nonzero. + r""" + Return ``True`` if ``self`` is *known* to be nonzero. EXAMPLES: @@ -1228,6 +1204,52 @@ def is_nonzero(self): return bool(self) return False + def prove_nonzero(self): + r""" + Generate coefficients until the result is shown to be nonzero + and returns the degree with the first nonzero coefficient. + + .. WARNING:: + + If the stream is exactly zero, this will run forever. + + EXAMPLES:: + + sage: L. = LazyLaurentSeriesRing(GF(5)) + sage: g = L(lambda n: 0 if n < 50 else 1, valuation=2) + sage: g.prove_nonzero() + 50 + + sage: L.zero().prove_nonzero() + Traceback (most recent call last): + ... + ValueError: trivially zero + """ + if isinstance(self._coeff_stream, Stream_zero): + raise ValueError("trivially zero") + + i = self._coeff_stream._approximate_order + while True: + if self[i]: + return i + i += 1 + + def is_trivial_zero(self): + r""" + Return whether ``self`` is known to be trivially zero. + + EXAMPLES:: + + sage: L. = LazyLaurentSeriesRing(ZZ) + sage: f = L(lambda n: 0, valuation=2) + sage: f.is_trivial_zero() + False + + sage: L.zero().is_trivial_zero() + True + """ + return isinstance(self._coeff_stream, Stream_zero) + def define(self, s): r""" Define an equation by ``self = s``. @@ -1540,9 +1562,9 @@ def _repr_(self): O(z^-3) sage: s = L.undefined(valuation=0); s - Uninitialized Lazy Laurent Series + Uninitialized Lazy Series sage: (s + s^2).map_coefficients(lambda f: f % 3) - Uninitialized Lazy Laurent Series + Uninitialized Lazy Series sage: L(0) 0 @@ -1555,8 +1577,8 @@ def _repr_(self): """ if isinstance(self._coeff_stream, Stream_zero): return '0' - if self._coeff_stream.is_undefined(): - return 'Uninitialized Lazy Laurent Series' + if self._coeff_stream.is_uninitialized(): + return 'Uninitialized Lazy Series' return self._format_series(repr) def _latex_(self): @@ -1608,7 +1630,7 @@ def _latex_(self): from sage.misc.latex import latex if isinstance(self._coeff_stream, Stream_zero): return latex('0') - if self._coeff_stream.is_undefined(): + if self._coeff_stream.is_uninitialized(): return latex("Undef") return self._format_series(latex) @@ -1623,14 +1645,17 @@ def _ascii_art_(self): sage: L. = LazyLaurentSeriesRing(e) sage: L.options.display_length = 3 sage: ascii_art(1 / (1 - e[1]*z)) - e[] + e[1]*z + e[1, 1]*z^2 + O(e[]*z^3) + e[] + e[1]*z + e[1, 1]*z^2 + O(e[]*z^3 + sage: x = L.undefined(valuation=0) # optional - sage.combinat + sage: ascii_art(x + x^2 - 5) # optional - sage.combinat + Uninitialized Lazy Series sage: L.options._reset() """ from sage.typeset.ascii_art import ascii_art, AsciiArt if isinstance(self._coeff_stream, Stream_zero): return AsciiArt('0') - if isinstance(self._coeff_stream, Stream_uninitialized) and self._coeff_stream._target is None: - return AsciiArt('Uninitialized Lazy Laurent Series') + if self._coeff_stream.is_uninitialized(): + return AsciiArt('Uninitialized Lazy Series') return self._format_series(ascii_art, True) def _unicode_art_(self): @@ -1645,13 +1670,16 @@ def _unicode_art_(self): sage: L.options.display_length = 3 sage: unicode_art(1 / (1 - e[1]*z)) e[] + e[1]*z + e[1, 1]*z^2 + O(e[]*z^3) + sage: x = L.undefined(valuation=0) # optional - sage.combinat + sage: ascii_art(x + x^2 - 5) # optional - sage.combinat + Uninitialized Lazy Series sage: L.options._reset() """ from sage.typeset.unicode_art import unicode_art, UnicodeArt if isinstance(self._coeff_stream, Stream_zero): return UnicodeArt('0') - if isinstance(self._coeff_stream, Stream_uninitialized) and self._coeff_stream._target is None: - return UnicodeArt('Uninitialized Lazy Laurent Series') + if self._coeff_stream.is_uninitialized(): + return UnicodeArt('Uninitialized Lazy Series') return self._format_series(unicode_art, True) def change_ring(self, ring): @@ -1931,12 +1959,14 @@ def _acted_upon_(self, scalar, self_on_left): Different scalars potentially give different series:: - sage: (2 * M == 3 * M) is None - True + sage: 2 * M == 3 * M + False - sage: L.options.use_unknown = True - sage: (2 * M == 3 * M) is None - True + sage: L.options.secure = True + sage: 2 * M == 3 * M + Traceback (most recent call last): + ... + ValueError: undecidable sage: L.options.halting_precision = 30 sage: 2 * M == 3 * M diff --git a/src/sage/rings/lazy_series_ring.py b/src/sage/rings/lazy_series_ring.py index b8b3095297e..9673a4790af 100644 --- a/src/sage/rings/lazy_series_ring.py +++ b/src/sage/rings/lazy_series_ring.py @@ -677,7 +677,7 @@ class options(GlobalOptions): - constant_length: 3 - display_length: 7 - halting_precision: None - - use_unknown: True + - secure: False sage: LLS.options.display_length 7 @@ -713,9 +713,9 @@ class options(GlobalOptions): halting_precision = dict(default=None, description='the number of coefficients, beginning with the approximate valuation, to check in equality tests', checker=lambda x: x is None or x in ZZ and x > 0) - use_unknown = dict(default=True, - description='whether to raise an error when a comparison is unknown', - checker=lambda x: x is True or x is False) + secure = dict(default=False, + description='whether to raise an error when a comparison is unknown', + checker=lambda x: x is True or x is False) @cached_method def one(self): @@ -1165,8 +1165,15 @@ class LazyLaurentSeriesRing(LazySeriesRing): be equal are considered to be different:: sage: f = L(lambda n: 0, valuation=0) - sage: (f == 0) is None - True + sage: f == 0 + False + + sage: f = L(constant=1, valuation=0).derivative(); f + 1 + 2*z + 3*z^2 + 4*z^3 + 5*z^4 + 6*z^5 + 7*z^6 + O(z^7) + sage: g = L(lambda n: (n+1), valuation=0); g + 1 + 2*z + 3*z^2 + 4*z^3 + 5*z^4 + 6*z^5 + 7*z^6 + O(z^7) + sage: f == g + False .. WARNING:: @@ -1174,8 +1181,9 @@ class LazyLaurentSeriesRing(LazySeriesRing): ``f != g`` returning ``True`` might not mean that the two series are actually different:: + sage: f = L(lambda n: 0, valuation=0) sage: g = L.zero() - sage: (f != g) is None + sage: f != g True This can be verified by :meth:`~sage.rings.lazy_series.is_nonzero()`, @@ -1195,23 +1203,25 @@ class LazyLaurentSeriesRing(LazySeriesRing): False We additionally provide two other methods of performing comparisons. - The first is returning an :class:`Unknown` and the second uses a check + The first is raising a ``ValueError`` and the second uses a check up to a (user set) finite precision. These behaviors are set using the - options ``use_unknown`` and ``halting_precision``. In particular, + options ``secure`` and ``halting_precision``. In particular, this applies to series that are not specified by a finite number of initial coefficients and a constant for the remaining coefficients. Equality checking will depend on the coefficients which have already been computed. If this information is not enough to - check that two series are different, then if ``L.options.use_unknown`` - is set to ``True``, then we return an :class:`Unknown`:: + check that two series are different, then if ``L.options.secure`` + is set to ``True``, then we raise a ``ValueError``:: - sage: L.options.use_unknown = True + sage: L.options.secure = True sage: f = 1 / (z + z^2); f z^-1 - 1 + z - z^2 + z^3 - z^4 + z^5 + O(z^6) sage: f2 = f * 2 # currently no coefficients computed sage: f3 = f * 3 # currently no coefficients computed - sage: (f2 == f3) is None - True + sage: f2 == f3 + Traceback (most recent call last): + ... + ValueError: undecidable sage: f2 # computes some of the coefficients of f2 2*z^-1 - 2 + 2*z - 2*z^2 + 2*z^3 - 2*z^4 + 2*z^5 + O(z^6) sage: f3 # computes some of the coefficients of f3 @@ -1219,16 +1229,22 @@ class LazyLaurentSeriesRing(LazySeriesRing): sage: f2 == f3 False sage: f2a = f + f - sage: (f2 == f2a) is None - True + sage: f2 == f2a + Traceback (most recent call last): + ... + ValueError: undecidable sage: zf = L(lambda n: 0, valuation=0) - sage: (zf == 0) is None - True + sage: zf == 0 + Traceback (most recent call last): + ... + ValueError: undecidable For boolean checks, an error is raised when it is not known to be nonzero:: sage: bool(zf) - True + Traceback (most recent call last): + ... + ValueError: undecidable If the halting precision is set to a finite number `p` (for unlimited precision, it is set to ``None``), then it will check up to `p` values From 294f994632394137f13431d3fac9ae9c30eecf3a Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Sat, 9 Sep 2023 14:23:13 +0900 Subject: [PATCH 5/8] More Stream_unary inheritance, some missing details, and other polishing. --- src/sage/data_structures/stream.py | 167 +++++++++++++---------------- src/sage/rings/lazy_series.py | 69 ++++++------ 2 files changed, 107 insertions(+), 129 deletions(-) diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index 7038ce2149c..0d400d58fd2 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -179,7 +179,6 @@ def __ne__(self, other): False sage: CS != Stream(-2) False - """ return False @@ -199,21 +198,18 @@ def is_nonzero(self): """ return False - def is_trivially_ne(self, other): - """ - Return if ``self`` is trivially not equal to ``other``. + def is_uninitialized(self): + r""" + Return ``True`` if ``self`` is an uninitialized stream. The default implementation is ``False``. - .. NOTE:: - - This does not check that the streams are equal. - """ - return False + EXAMPLES:: - def is_uninitialized(self): - """ - Return ``True`` if ``self`` is an uninitialized stream. + sage: from sage.data_structures.stream import Stream_zero + sage: zero = Stream_zero() + sage: zero.is_uninitialized() + False """ return False @@ -939,7 +935,6 @@ class Stream_iterator(Stream_inexact): sage: f = Stream_iterator(iter(NonNegativeIntegers()), 1) sage: [f[i] for i in range(10)] [0, 0, 1, 2, 3, 4, 5, 6, 7, 8] - """ def __init__(self, iter, approximate_order, true_order=False): """ @@ -968,6 +963,11 @@ class Stream_function(Stream_inexact): - ``approximate_order`` -- integer; a lower bound for the order of the stream + .. NOTE:: + + We assume for equality that ``function`` is a function in the + mathematical sense. + EXAMPLES:: sage: from sage.data_structures.stream import Stream_function @@ -1000,6 +1000,43 @@ def __init__(self, function, is_sparse, approximate_order, true_order=False): super().__init__(is_sparse, true_order) self._approximate_order = approximate_order + def __hash__(self): + """ + Return the hash of ``self``. + + EXAMPLES:: + + sage: from sage.data_structures.stream import Stream_function + sage: f = Stream_function(lambda n: n, True, 0) + sage: g = Stream_function(lambda n: 1, False, 1) + 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_function + sage: fun = lambda n: n + sage: f = Stream_function(fun, True, 0) + sage: g = Stream_function(fun, False, 0) + sage: h = Stream_function(lambda n: n, False, 0) + sage: f == g + True + sage: f == h + False + """ + return isinstance(other, type(self)) and self.get_coefficient == other.get_coefficient + class Stream_uninitialized(Stream_inexact): r""" @@ -1086,6 +1123,9 @@ class Stream_unary(Stream_inexact): INPUT: - ``series`` -- :class:`Stream` the operator acts on + - ``is_sparse`` -- boolean + - ``true_order`` -- boolean (default: ``False``) if the approximate order + is the actual order EXAMPLES:: @@ -1098,7 +1138,7 @@ class Stream_unary(Stream_inexact): sage: [g[i] for i in range(10)] [0, 4, 8, 12, 16, 20, 24, 28, 32, 36] """ - def __init__(self, series, is_sparse): + def __init__(self, series, is_sparse, true_order=False): """ Initialize ``self``. @@ -1113,7 +1153,7 @@ def __init__(self, series, is_sparse): sage: TestSuite(g).run() """ self._series = series - super().__init__(is_sparse, False) + super().__init__(is_sparse, true_order) def __hash__(self): """ @@ -1350,7 +1390,6 @@ class Stream_zero(Stream): sage: s[5] 0 """ - def __init__(self): """ Initialize ``self``. @@ -1360,7 +1399,6 @@ def __init__(self): sage: from sage.data_structures.stream import Stream_zero sage: s = Stream_zero() sage: TestSuite(s).run() - """ super().__init__(True) self._approximate_order = infinity @@ -1415,7 +1453,7 @@ def __eq__(self, other): def __ne__(self, other): """ - Return whether ``self`` and ``other`` are known to be not equal. + Return whether ``self`` and ``other`` are known to be different. INPUT: @@ -1749,6 +1787,7 @@ def get_coefficient(self, n): and n // k >= self._right._approximate_order and (l := self._left[k]))) + class Stream_dirichlet_invert(Stream_unary): r""" Operator for inverse with respect to Dirichlet convolution of the stream. @@ -2309,7 +2348,7 @@ def stretched_power_restrict_degree(self, i, m, d): ##################################################################### # Unary operations -class Stream_scalar(Stream_inexact): +class Stream_scalar(Stream_unary): """ Base class for operators multiplying a coefficient stream by a scalar. @@ -2318,14 +2357,7 @@ class Stream_scalar(Stream_inexact): - ``series`` -- a :class:`Stream` - ``scalar`` -- a non-zero, non-one scalar - - .. TODO:: - - This does not inherit from :class:`Stream_unary`, because of - the extra argument ``scalar``. However, we could also - override :meth:`Stream_unary.hash`, - :meth:`Stream_unary.__eq__`. Would this be any better? - + - ``is_sparse`` -- boolean """ def __init__(self, series, scalar, is_sparse): """ @@ -2337,10 +2369,10 @@ def __init__(self, series, scalar, is_sparse): sage: f = Stream_function(lambda n: -1, True, 0) sage: g = Stream_rmul(f, 3, True) """ - self._series = series self._scalar = scalar assert scalar, "the scalar must not be equal to 0" - super().__init__(is_sparse, series._true_order) + assert scalar != 1, "the scalar must not be equal to 1" + super().__init__(series, is_sparse, series._true_order) @lazy_attribute def _approximate_order(self): @@ -2423,20 +2455,6 @@ def is_nonzero(self): """ return self._series.is_nonzero() - def is_uninitialized(self): - """ - Return ``True`` if ``self`` is an uninitialized stream. - - EXAMPLES:: - - sage: from sage.data_structures.stream import Stream_uninitialized, Stream_lmul, Stream_function - sage: C = Stream_uninitialized(0) - sage: B = Stream_lmul(C, 2, True) - sage: B.is_uninitialized() - True - """ - return self._series.is_uninitialized() - class Stream_rmul(Stream_scalar): """ @@ -2749,7 +2767,7 @@ def is_nonzero(self): return True -class Stream_map_coefficients(Stream_inexact): +class Stream_map_coefficients(Stream_unary): r""" The stream with ``function`` applied to each non-zero coefficient of ``series``. @@ -2759,6 +2777,11 @@ class Stream_map_coefficients(Stream_inexact): - ``series`` -- a :class:`Stream` - ``function`` -- a function that modifies the elements of the stream + .. NOTE:: + + We assume for equality that ``function`` is a function in the + mathematical sense. + EXAMPLES:: sage: from sage.data_structures.stream import (Stream_map_coefficients, Stream_function) @@ -2780,8 +2803,7 @@ def __init__(self, series, function, is_sparse, approximate_order=None, true_ord sage: TestSuite(g).run(skip="_test_pickling") """ self._function = function - self._series = series - super().__init__(is_sparse, true_order) + super().__init__(series, is_sparse, true_order) if approximate_order is not None: self._approximate_order = approximate_order @@ -2869,20 +2891,6 @@ def __eq__(self, other): return (isinstance(other, type(self)) and self._series == other._series and self._function == other._function) - def is_uninitialized(self): - """ - Return ``True`` if ``self`` is an uninitialized stream. - - EXAMPLES:: - - sage: from sage.data_structures.stream import Stream_uninitialized, Stream_map_coefficients - sage: C = Stream_uninitialized(0) - sage: M = Stream_map_coefficients(C, lambda n: -n, True) - sage: M.is_uninitialized() - True - """ - return self._series.is_uninitialized() - class Stream_shift(Stream): """ @@ -3031,7 +3039,7 @@ def is_uninitialized(self): return self._series.is_uninitialized() -class Stream_truncated(Stream_inexact): +class Stream_truncated(Stream_unary): """ Operator for shifting a non-zero, non-exact stream that has been shifted below its minimal valuation. @@ -3081,9 +3089,8 @@ def __init__(self, series, shift, minimal_valuation): sage: s._approximate_order 3 """ - super().__init__(series._is_sparse, False) + super().__init__(series, series._is_sparse, False) assert isinstance(series, Stream_inexact) - self._series = series # We share self._series._cache but not self._series._approximate order # self._approximate_order cannot be updated by self._series.__getitem__ self._cache = series._cache @@ -3279,22 +3286,8 @@ def is_nonzero(self): start = self._approximate_order - offset return any(self._cache[start:]) - def is_uninitialized(self): - """ - Return ``True`` if ``self`` is an uninitialized stream. - - EXAMPLES:: - - sage: from sage.data_structures.stream import Stream_uninitialized, Stream_truncated - sage: C = Stream_uninitialized(0) - sage: S = Stream_truncated(C, -5, 3) - sage: S.is_uninitialized() - True - """ - return self._series.is_uninitialized() - -class Stream_derivative(Stream_inexact): +class Stream_derivative(Stream_unary): """ Operator for taking derivatives of a non-exact stream. @@ -3302,6 +3295,7 @@ class Stream_derivative(Stream_inexact): - ``series`` -- a :class:`Stream` - ``shift`` -- a positive integer + - ``is_sparse`` -- boolean """ def __init__(self, series, shift, is_sparse): """ @@ -3314,9 +3308,8 @@ def __init__(self, series, shift, is_sparse): sage: f2 = Stream_derivative(f, 2, True) sage: TestSuite(f2).run() """ - self._series = series self._shift = shift - super().__init__(is_sparse, False) + super().__init__(series, is_sparse, False) @lazy_attribute def _approximate_order(self): @@ -3420,17 +3413,3 @@ def is_nonzero(self): True """ return self._series.is_nonzero() - - def is_uninitialized(self): - """ - Return ``True`` if ``self`` is an uninitialized stream. - - EXAMPLES:: - - sage: from sage.data_structures.stream import Stream_uninitialized, Stream_derivative - sage: C = Stream_uninitialized(0) - sage: D = Stream_derivative(C, 1, True) - sage: D.is_uninitialized() - True - """ - return self._series.is_uninitialized() diff --git a/src/sage/rings/lazy_series.py b/src/sage/rings/lazy_series.py index 2c982f6fc80..6ae0146e4d2 100644 --- a/src/sage/rings/lazy_series.py +++ b/src/sage/rings/lazy_series.py @@ -1158,10 +1158,19 @@ def __bool__(self): v = self._coeff_stream._approximate_order return any(self[i] for i in range(v, v + prec)) - def is_nonzero(self): + def is_nonzero(self, proof=False): r""" Return ``True`` if ``self`` is *known* to be nonzero. + INPUT: + + - ``proof`` -- (default: ``False``) if ``True``, this will also return + an index such that ``self`` has a nonzero coefficient + + .. WARNING:: + + If the stream is exactly zero, this will run forever. + EXAMPLES: A series that it not known to be nonzero with no halting precision:: @@ -1197,42 +1206,32 @@ def is_nonzero(self): sage: g.is_nonzero() # checks up to degree 62 = 42 + 20 True sage: L.options._reset() - """ - if self._coeff_stream.is_nonzero(): - return True - if self.parent().options['halting_precision'] is not None: - return bool(self) - return False - - def prove_nonzero(self): - r""" - Generate coefficients until the result is shown to be nonzero - and returns the degree with the first nonzero coefficient. - - .. WARNING:: - - If the stream is exactly zero, this will run forever. - EXAMPLES:: + With a proof:: sage: L. = LazyLaurentSeriesRing(GF(5)) - sage: g = L(lambda n: 0 if n < 50 else 1, valuation=2) - sage: g.prove_nonzero() - 50 + sage: g = L(lambda n: 5 if n < 50 else 1, valuation=2) + sage: g.is_nonzero(proof=True) + (True, 50) - sage: L.zero().prove_nonzero() - Traceback (most recent call last): - ... - ValueError: trivially zero + sage: L.zero().is_nonzero(proof=True) + (False, None) """ - if isinstance(self._coeff_stream, Stream_zero): - raise ValueError("trivially zero") + if proof: + if isinstance(self._coeff_stream, Stream_zero): + return (False, None) - i = self._coeff_stream._approximate_order - while True: - if self[i]: - return i - i += 1 + i = self._coeff_stream._approximate_order + while True: + if self[i]: + return (True, i) + i += 1 + + if self._coeff_stream.is_nonzero(): + return True + if self.parent().options['halting_precision'] is not None: + return bool(self) + return False def is_trivial_zero(self): r""" @@ -1645,7 +1644,7 @@ def _ascii_art_(self): sage: L. = LazyLaurentSeriesRing(e) sage: L.options.display_length = 3 sage: ascii_art(1 / (1 - e[1]*z)) - e[] + e[1]*z + e[1, 1]*z^2 + O(e[]*z^3 + e[] + e[1]*z + e[1, 1]*z^2 + O(e[]*z^3) sage: x = L.undefined(valuation=0) # optional - sage.combinat sage: ascii_art(x + x^2 - 5) # optional - sage.combinat Uninitialized Lazy Series @@ -1655,7 +1654,7 @@ def _ascii_art_(self): if isinstance(self._coeff_stream, Stream_zero): return AsciiArt('0') if self._coeff_stream.is_uninitialized(): - return AsciiArt('Uninitialized Lazy Series') + return AsciiArt(['Uninitialized Lazy Series']) return self._format_series(ascii_art, True) def _unicode_art_(self): @@ -1671,7 +1670,7 @@ def _unicode_art_(self): sage: unicode_art(1 / (1 - e[1]*z)) e[] + e[1]*z + e[1, 1]*z^2 + O(e[]*z^3) sage: x = L.undefined(valuation=0) # optional - sage.combinat - sage: ascii_art(x + x^2 - 5) # optional - sage.combinat + sage: unicode_art(x + x^2 - 5) # optional - sage.combinat Uninitialized Lazy Series sage: L.options._reset() """ @@ -1679,7 +1678,7 @@ def _unicode_art_(self): if isinstance(self._coeff_stream, Stream_zero): return UnicodeArt('0') if self._coeff_stream.is_uninitialized(): - return UnicodeArt('Uninitialized Lazy Series') + return UnicodeArt(['Uninitialized Lazy Series']) return self._format_series(unicode_art, True) def change_ring(self, ring): From 041cea6446525cf12a6572f32487f84e8bf3b4fc Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Sat, 9 Sep 2023 14:24:18 +0900 Subject: [PATCH 6/8] Moving Stream_dirichlet_invert to the unary section. --- src/sage/data_structures/stream.py | 214 ++++++++++++++--------------- 1 file changed, 107 insertions(+), 107 deletions(-) diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index 0d400d58fd2..8ec1b74c1f2 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -1788,113 +1788,6 @@ def get_coefficient(self, n): and (l := self._left[k]))) -class Stream_dirichlet_invert(Stream_unary): - r""" - Operator for inverse with respect to Dirichlet convolution of the stream. - - INPUT: - - - ``series`` -- a :class:`Stream` - - EXAMPLES:: - - sage: from sage.data_structures.stream import (Stream_dirichlet_invert, Stream_function) - sage: f = Stream_function(lambda n: 1, True, 1) - sage: g = Stream_dirichlet_invert(f, True) - sage: [g[i] for i in range(10)] - [0, 1, -1, -1, 0, -1, 1, -1, 0, 0] - sage: [moebius(i) for i in range(10)] # needs sage.libs.pari - [0, 1, -1, -1, 0, -1, 1, -1, 0, 0] - """ - def __init__(self, series, is_sparse): - """ - Initialize. - - TESTS:: - - sage: from sage.data_structures.stream import (Stream_exact, Stream_dirichlet_invert) - sage: f = Stream_exact([0, 0], constant=1) - sage: g = Stream_dirichlet_invert(f, True) - sage: g[1] - Traceback (most recent call last): - ... - ZeroDivisionError: the Dirichlet inverse only exists if the coefficient with index 1 is non-zero - """ - super().__init__(series, is_sparse) - self._zero = ZZ.zero() - - @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_dirichlet_invert - sage: f = Stream_function(lambda n: n, True, 1) - sage: h = Stream_dirichlet_invert(f, True) - sage: h._approximate_order - 1 - sage: [h[i] for i in range(5)] - [0, -2, -8, -12, -48] - """ - # this is the true order, but we want to check first - if self._series._approximate_order > 1: - raise ZeroDivisionError("the Dirichlet inverse only exists if the " - "coefficient with index 1 is non-zero") - self._true_order = True - return 1 - - @lazy_attribute - def _ainv(self): - """ - The inverse of the leading coefficient. - - EXAMPLES:: - - sage: from sage.data_structures.stream import (Stream_exact, Stream_dirichlet_invert) - sage: f = Stream_exact([0, 3], constant=2) - sage: g = Stream_dirichlet_invert(f, True) - sage: g._ainv - 1/3 - - sage: f = Stream_exact([Zmod(6)(5)], constant=2, order=1) - sage: g = Stream_dirichlet_invert(f, True) - sage: g._ainv - 5 - """ - try: - return ~self._series[1] - except TypeError: - return self._series[1].inverse_of_unit() - - def get_coefficient(self, n): - """ - Return the ``n``-th coefficient of ``self``. - - INPUT: - - - ``n`` -- integer; the degree for the coefficient - - EXAMPLES:: - - sage: from sage.data_structures.stream import (Stream_exact, Stream_dirichlet_invert) - sage: f = Stream_exact([0, 3], constant=2) - sage: g = Stream_dirichlet_invert(f, True) - sage: g.get_coefficient(6) - 2/27 - sage: [g[i] for i in range(8)] - [0, 1/3, -2/9, -2/9, -2/27, -2/9, 2/27, -2/9] - """ - if n == 1: - return self._ainv - # TODO: isn't self[k] * l and l * self[k] the same here? - c = sum(self[k] * l for k in divisors(n) - if (k < n - and (l := self._series[n // k]))) - return -c * self._ainv - - class Stream_cauchy_compose(Stream_binary): r""" Return ``f`` composed by ``g``. @@ -2767,6 +2660,113 @@ def is_nonzero(self): return True +class Stream_dirichlet_invert(Stream_unary): + r""" + Operator for inverse with respect to Dirichlet convolution of the stream. + + INPUT: + + - ``series`` -- a :class:`Stream` + + EXAMPLES:: + + sage: from sage.data_structures.stream import (Stream_dirichlet_invert, Stream_function) + sage: f = Stream_function(lambda n: 1, True, 1) + sage: g = Stream_dirichlet_invert(f, True) + sage: [g[i] for i in range(10)] + [0, 1, -1, -1, 0, -1, 1, -1, 0, 0] + sage: [moebius(i) for i in range(10)] # needs sage.libs.pari + [0, 1, -1, -1, 0, -1, 1, -1, 0, 0] + """ + def __init__(self, series, is_sparse): + """ + Initialize. + + TESTS:: + + sage: from sage.data_structures.stream import (Stream_exact, Stream_dirichlet_invert) + sage: f = Stream_exact([0, 0], constant=1) + sage: g = Stream_dirichlet_invert(f, True) + sage: g[1] + Traceback (most recent call last): + ... + ZeroDivisionError: the Dirichlet inverse only exists if the coefficient with index 1 is non-zero + """ + super().__init__(series, is_sparse) + self._zero = ZZ.zero() + + @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_dirichlet_invert + sage: f = Stream_function(lambda n: n, True, 1) + sage: h = Stream_dirichlet_invert(f, True) + sage: h._approximate_order + 1 + sage: [h[i] for i in range(5)] + [0, -2, -8, -12, -48] + """ + # this is the true order, but we want to check first + if self._series._approximate_order > 1: + raise ZeroDivisionError("the Dirichlet inverse only exists if the " + "coefficient with index 1 is non-zero") + self._true_order = True + return 1 + + @lazy_attribute + def _ainv(self): + """ + The inverse of the leading coefficient. + + EXAMPLES:: + + sage: from sage.data_structures.stream import (Stream_exact, Stream_dirichlet_invert) + sage: f = Stream_exact([0, 3], constant=2) + sage: g = Stream_dirichlet_invert(f, True) + sage: g._ainv + 1/3 + + sage: f = Stream_exact([Zmod(6)(5)], constant=2, order=1) + sage: g = Stream_dirichlet_invert(f, True) + sage: g._ainv + 5 + """ + try: + return ~self._series[1] + except TypeError: + return self._series[1].inverse_of_unit() + + def get_coefficient(self, n): + """ + Return the ``n``-th coefficient of ``self``. + + INPUT: + + - ``n`` -- integer; the degree for the coefficient + + EXAMPLES:: + + sage: from sage.data_structures.stream import (Stream_exact, Stream_dirichlet_invert) + sage: f = Stream_exact([0, 3], constant=2) + sage: g = Stream_dirichlet_invert(f, True) + sage: g.get_coefficient(6) + 2/27 + sage: [g[i] for i in range(8)] + [0, 1/3, -2/9, -2/9, -2/27, -2/9, 2/27, -2/9] + """ + if n == 1: + return self._ainv + # TODO: isn't self[k] * l and l * self[k] the same here? + c = sum(self[k] * l for k in divisors(n) + if (k < n + and (l := self._series[n // k]))) + return -c * self._ainv + + class Stream_map_coefficients(Stream_unary): r""" The stream with ``function`` applied to each non-zero coefficient From 297c98051a966eb56d522f0f388f91feba943592 Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Sun, 10 Sep 2023 13:45:19 +0900 Subject: [PATCH 7/8] Better uninitialized checking; fixing doctests. --- src/sage/combinat/species/recursive_species.py | 6 +++--- src/sage/data_structures/stream.py | 11 ++++++++++- src/sage/rings/lazy_series.py | 8 +++++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/sage/combinat/species/recursive_species.py b/src/sage/combinat/species/recursive_species.py index 8fd01494c1a..3cd073f9f9c 100644 --- a/src/sage/combinat/species/recursive_species.py +++ b/src/sage/combinat/species/recursive_species.py @@ -230,7 +230,7 @@ def _gs(self, series_ring, base_ring): sage: F = CombinatorialSpecies() sage: F.generating_series() - Uninitialized Lazy Laurent Series + Uninitialized Lazy Series """ if base_ring not in self._generating_series: self._generating_series[base_ring] = series_ring.undefined(valuation=(0 if self._min is None else self._min)) @@ -247,7 +247,7 @@ def _itgs(self, series_ring, base_ring): sage: F = CombinatorialSpecies() sage: F.isotype_generating_series() - Uninitialized Lazy Laurent Series + Uninitialized Lazy Series """ if base_ring not in self._isotype_generating_series: self._isotype_generating_series[base_ring] = series_ring.undefined(valuation=(0 if self._min is None else self._min)) @@ -264,7 +264,7 @@ def _cis(self, series_ring, base_ring): sage: F = CombinatorialSpecies() sage: F.cycle_index_series() - Uninitialized Lazy Laurent Series + Uninitialized Lazy Series """ if base_ring not in self._cycle_index_series: self._cycle_index_series[base_ring] = series_ring.undefined(valuation=(0 if self._min is None else self._min)) diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index 8ec1b74c1f2..d7f1f939bb7 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -1080,6 +1080,7 @@ def __init__(self, approximate_order, true_order=False): raise ValueError("the valuation must be specified for undefined series") super().__init__(False, true_order) self._approximate_order = approximate_order + self._initializing = False def iterate_coefficients(self): """ @@ -1113,7 +1114,15 @@ def is_uninitialized(self): sage: C.is_uninitialized() True """ - return self._target is None + if self._target is None: + return True + if self._initializing: + return False + # We implement semaphore-like behavior for coupled (undefined) series + self._initializing = True + result = self._target.is_uninitialized() + self._initializing = False + return result class Stream_unary(Stream_inexact): diff --git a/src/sage/rings/lazy_series.py b/src/sage/rings/lazy_series.py index 6ae0146e4d2..0764711956a 100644 --- a/src/sage/rings/lazy_series.py +++ b/src/sage/rings/lazy_series.py @@ -3605,7 +3605,8 @@ def exp(self): coeff_stream = self._coeff_stream # TODO: coefficients should not be checked here, it prevents # us from using self.define in some cases! - if any(coeff_stream[i] for i in range(coeff_stream._approximate_order, 1)): + if ((not coeff_stream.is_uninitialized()) + and any(coeff_stream[i] for i in range(coeff_stream._approximate_order, 1))): raise ValueError("can only compose with a positive valuation series") # WARNING: d_self need not be a proper element of P, e.g. for # multivariate power series @@ -3657,8 +3658,9 @@ def log(self): coeff_stream = self._coeff_stream # TODO: coefficients should not be checked here, it prevents # us from using self.define in some cases! - if (any(coeff_stream[i] for i in range(coeff_stream._approximate_order, 0)) - or coeff_stream[0] != R.one()): + if ((not coeff_stream.is_uninitialized()) + and (any(coeff_stream[i] for i in range(coeff_stream._approximate_order, 0)) + or coeff_stream[0] != R.one())): raise ValueError("can only compose with a positive valuation series") # WARNING: d_self need not be a proper element of P, e.g. for # multivariate power series From 1ad73520c3f745bb663acf17a3d4316ab12f8223 Mon Sep 17 00:00:00 2001 From: Travis Scrimshaw Date: Sun, 10 Sep 2023 13:58:51 +0900 Subject: [PATCH 8/8] Adding a test for the improved uninitialized series. --- src/sage/data_structures/stream.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/sage/data_structures/stream.py b/src/sage/data_structures/stream.py index d7f1f939bb7..c24f2c547df 100644 --- a/src/sage/data_structures/stream.py +++ b/src/sage/data_structures/stream.py @@ -1113,6 +1113,15 @@ def is_uninitialized(self): sage: C = Stream_uninitialized(0) sage: C.is_uninitialized() True + + A more subtle uninitialized series:: + + sage: L. = LazyPowerSeriesRing(QQ) + sage: T = L.undefined(1) + sage: D = L.undefined(0) + sage: T.define(z * exp(T) * D) + sage: T._coeff_stream.is_uninitialized() + True """ if self._target is None: return True